From 668ea073e696b8ec0b33027f8bb2ee2f508137fa Mon Sep 17 00:00:00 2001 From: zhang1106 <849185023@qq.com> Date: Thu, 12 Mar 2026 11:19:56 +0800 Subject: [PATCH] =?UTF-8?q?refactor(=E6=A8=A1=E5=9E=8B=E5=85=B3=E7=B3=BB):?= =?UTF-8?q?=20=E9=87=8D=E6=9E=84=E6=A8=A1=E5=9E=8B=E5=85=B3=E8=81=94?= =?UTF-8?q?=E5=85=B3=E7=B3=BB=E5=AE=9A=E4=B9=89=E4=BD=8D=E7=BD=AE=EF=BC=8C?= =?UTF-8?q?=E4=BB=8E=E6=A8=A1=E5=9E=8B=E6=96=87=E4=BB=B6=E7=A7=BB=E8=87=B3?= =?UTF-8?q?=E8=B7=AF=E7=94=B1=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- backend/backgroundSettings.json | 7 +- backend/middleware/auth.js | 67 +- backend/middleware/validation.js | 61 +- backend/models/Device.js | 10 +- backend/models/DevicePort.js | 8 - backend/models/NetworkCard.js | 4 - backend/routes/background.js | 34 +- backend/routes/devicePorts.js | 5 + backend/routes/devices.js | 56 +- backend/routes/networkCards.js | 4 + backend/server.js | 31 + backend/validation/deviceSchema.js | 229 +-- docs/api/README.md | 1413 ++++++++++++++---- frontend/src/components/CableCreateModal.jsx | 57 +- frontend/src/config/theme.js | 11 + frontend/src/pages/CableManagement.jsx | 32 +- 17 files changed, 1478 insertions(+), 553 deletions(-) diff --git a/README.md b/README.md index db7145f..1620854 100644 --- a/README.md +++ b/README.md @@ -641,7 +641,7 @@ SOFTWARE. - **Gitee Issues**: https://gitee.com/zhang96110/idc_assest/issues - **GitHub Issues**: https://github.com/gituib/idc_assest/issues - 功能建议:提交 Issue 并标注 `feature-request` - +- QQ群:1081123775 --- **⭐ 如果这个项目对您有帮助,请给我们一个 Star!** diff --git a/backend/backgroundSettings.json b/backend/backgroundSettings.json index a3368f8..cb2bf56 100644 --- a/backend/backgroundSettings.json +++ b/backend/backgroundSettings.json @@ -1,5 +1,6 @@ { - "type": "image", - "image": null, - "size": "contain" + "opacity": 0.8, + "blur": 5, + "value": "/images/bg.jpg", + "type": "image" } \ No newline at end of file diff --git a/backend/middleware/auth.js b/backend/middleware/auth.js index b33e950..61d9bdc 100644 --- a/backend/middleware/auth.js +++ b/backend/middleware/auth.js @@ -116,9 +116,30 @@ const authMiddleware = async (req, res, next) => { }); } - const user = await User.findByPk(decoded.userId); + // 验证用户是否存在,增加详细的错误日志 + let user; + try { + user = await User.findByPk(decoded.userId); + } catch (dbError) { + // 数据库查询错误 + console.error('[认证中间件] 数据库查询失败:', { + userId: decoded.userId, + error: dbError.message, + stack: dbError.stack, + timestamp: new Date().toISOString() + }); + return res.status(500).json({ + success: false, + message: '数据库查询失败,请稍后重试' + }); + } if (!user) { + console.warn('[认证中间件] 用户不存在:', { + userId: decoded.userId, + username: decoded.username, + timestamp: new Date().toISOString() + }); return res.status(401).json({ success: false, message: '用户不存在' @@ -126,6 +147,11 @@ const authMiddleware = async (req, res, next) => { } if (user.status === 'locked') { + console.warn('[认证中间件] 账户已被锁定:', { + userId: user.userId, + username: user.username, + timestamp: new Date().toISOString() + }); return res.status(403).json({ success: false, message: '账户已被锁定' @@ -133,6 +159,11 @@ const authMiddleware = async (req, res, next) => { } if (user.status === 'inactive') { + console.warn('[认证中间件] 账户已禁用:', { + userId: user.userId, + username: user.username, + timestamp: new Date().toISOString() + }); return res.status(403).json({ success: false, message: '账户已禁用' @@ -143,10 +174,18 @@ const authMiddleware = async (req, res, next) => { req.userModel = user; next(); } catch (error) { - console.error('认证中间件错误:', error); + // 捕获未预期的错误 + console.error('[认证中间件] 未预期的错误:', { + error: error.message, + stack: error.stack, + url: req?.url, + method: req?.method, + ip: req?.ip, + timestamp: new Date().toISOString() + }); return res.status(500).json({ success: false, - message: '认证失败' + message: '认证失败,请稍后重试' }); } }; @@ -160,7 +199,21 @@ const optionalAuth = async (req, res, next) => { const decoded = verifyToken(token); if (decoded) { - const user = await User.findByPk(decoded.userId); + let user; + try { + user = await User.findByPk(decoded.userId); + } catch (dbError) { + // 数据库错误时不中断请求,仅记录日志 + console.warn('[可选认证] 数据库查询失败:', { + userId: decoded.userId, + error: dbError.message, + timestamp: new Date().toISOString() + }); + // 继续执行,不设置用户信息 + next(); + return; + } + if (user && user.status === 'active') { req.user = decoded; req.userModel = user; @@ -170,6 +223,12 @@ const optionalAuth = async (req, res, next) => { next(); } catch (error) { + // 可选认证失败不影响主流程,仅记录日志 + console.warn('[可选认证] 认证失败(已忽略):', { + error: error.message, + url: req?.url, + timestamp: new Date().toISOString() + }); next(); } }; diff --git a/backend/middleware/validation.js b/backend/middleware/validation.js index 010e081..86fc9ad 100644 --- a/backend/middleware/validation.js +++ b/backend/middleware/validation.js @@ -3,49 +3,54 @@ const validate = (schema, source = 'body') => { const data = source === 'query' ? req.query : req.body; try { - let result; + let value; if (schema.validate && typeof schema.validate === 'function') { - if (schema.validate.constructor.name === 'AsyncFunction' || - schema.validate.length === 1) { - result = await schema.validate(data, { - abortEarly: false, - stripUnknown: true, - allowUnknown: source === 'query' + const result = schema.validate(data, { + abortEarly: false, + stripUnknown: true, + allowUnknown: source === 'query' + }); + + if (result && typeof result.then === 'function') { + value = await result; + } else if (result && result.error) { + const errorMessages = result.error.details.map(detail => ({ + field: detail.path.join('.'), + message: detail.message + })); + return res.status(400).json({ + error: '参数验证失败', + details: errorMessages }); + } else if (result && result.value !== undefined) { + value = result.value; } else { - result = schema.validate(data, { - abortEarly: false, - stripUnknown: true, - allowUnknown: source === 'query' - }); + value = result; } } else if (schema.validateAsync) { - result = await schema.validateAsync(data, { + value = await schema.validateAsync(data, { abortEarly: false, stripUnknown: true, allowUnknown: source === 'query' }); } else { - result = schema.validate(data, { + const result = schema.validate(data, { abortEarly: false, stripUnknown: true, allowUnknown: source === 'query' }); - } - - const { error, value } = result; - - if (error) { - const errorMessages = error.details.map(detail => ({ - field: detail.path.join('.'), - message: detail.message - })); - - return res.status(400).json({ - error: '参数验证失败', - details: errorMessages - }); + if (result.error) { + const errorMessages = result.error.details.map(detail => ({ + field: detail.path.join('.'), + message: detail.message + })); + return res.status(400).json({ + error: '参数验证失败', + details: errorMessages + }); + } + value = result.value; } if (source === 'query') { diff --git a/backend/models/Device.js b/backend/models/Device.js index 0ce0bb1..a7ccc0f 100644 --- a/backend/models/Device.js +++ b/backend/models/Device.js @@ -1,6 +1,5 @@ const { DataTypes } = require('sequelize'); const { sequelize } = require('../db'); -const Rack = require('./Rack'); const Device = sequelize.define('Device', { deviceId: { @@ -28,11 +27,7 @@ const Device = sequelize.define('Device', { }, rackId: { type: DataTypes.STRING, - allowNull: true, - references: { - model: Rack, - key: 'rackId' - } + allowNull: true }, position: { type: DataTypes.INTEGER, @@ -86,7 +81,4 @@ const Device = sequelize.define('Device', { ] }); -Device.belongsTo(Rack, { foreignKey: 'rackId' }); -Rack.hasMany(Device, { foreignKey: 'rackId' }); - module.exports = Device; diff --git a/backend/models/DevicePort.js b/backend/models/DevicePort.js index 6680745..1fb1efa 100644 --- a/backend/models/DevicePort.js +++ b/backend/models/DevicePort.js @@ -1,7 +1,5 @@ const { DataTypes } = require('sequelize'); const { sequelize } = require('../db'); -const Device = require('./Device'); -const NetworkCard = require('./NetworkCard'); const DevicePort = sequelize.define('DevicePort', { portId: { @@ -67,10 +65,4 @@ const DevicePort = sequelize.define('DevicePort', { ] }); -DevicePort.belongsTo(Device, { foreignKey: 'deviceId', as: 'device' }); -Device.hasMany(DevicePort, { foreignKey: 'deviceId', as: 'ports' }); - -DevicePort.belongsTo(NetworkCard, { foreignKey: 'nicId', as: 'networkCard' }); -NetworkCard.hasMany(DevicePort, { foreignKey: 'nicId', as: 'ports' }); - module.exports = DevicePort; diff --git a/backend/models/NetworkCard.js b/backend/models/NetworkCard.js index 4b47b0a..3023e40 100644 --- a/backend/models/NetworkCard.js +++ b/backend/models/NetworkCard.js @@ -1,6 +1,5 @@ const { DataTypes } = require('sequelize'); const { sequelize } = require('../db'); -const Device = require('./Device'); const NetworkCard = sequelize.define('NetworkCard', { nicId: { @@ -63,7 +62,4 @@ const NetworkCard = sequelize.define('NetworkCard', { ] }); -NetworkCard.belongsTo(Device, { foreignKey: 'deviceId', as: 'device' }); -Device.hasMany(NetworkCard, { foreignKey: 'deviceId', as: 'networkCards' }); - module.exports = NetworkCard; diff --git a/backend/routes/background.js b/backend/routes/background.js index 1ec3e40..0987d89 100644 --- a/backend/routes/background.js +++ b/backend/routes/background.js @@ -3,13 +3,11 @@ const path = require('path'); const fs = require('fs'); const router = express.Router(); -// 确保上传目录存在 const UPLOAD_DIR = path.join(__dirname, '../uploads'); if (!fs.existsSync(UPLOAD_DIR)) { fs.mkdirSync(UPLOAD_DIR, { recursive: true }); } -// 确保背景设置文件存在 const SETTINGS_FILE = path.join(__dirname, '../backgroundSettings.json'); if (!fs.existsSync(SETTINGS_FILE)) { fs.writeFileSync(SETTINGS_FILE, JSON.stringify({ @@ -19,7 +17,33 @@ if (!fs.existsSync(SETTINGS_FILE)) { }, null, 2)); } -// 上传图片接口 +router.get('/', (req, res) => { + try { + const settings = JSON.parse(fs.readFileSync(SETTINGS_FILE, 'utf8')); + res.json({ + success: true, + data: settings + }); + } catch (error) { + console.error('读取背景设置失败:', error); + res.status(500).json({ error: '读取背景设置失败' }); + } +}); + +router.put('/', (req, res) => { + try { + const settings = req.body; + fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2)); + res.json({ + success: true, + data: settings + }); + } catch (error) { + console.error('保存背景设置失败:', error); + res.status(500).json({ error: '保存背景设置失败' }); + } +}); + router.post('/upload', (req, res) => { try { if (!req.files || !req.files.file) { @@ -30,14 +54,12 @@ router.post('/upload', (req, res) => { const fileName = `${Date.now()}_${file.name}`; const filePath = path.join(UPLOAD_DIR, fileName); - // 保存文件到服务器 file.mv(filePath, (err) => { if (err) { console.error('文件保存失败:', err); return res.status(500).json({ error: '文件保存失败' }); } - // 返回文件路径 const fileUrl = `/uploads/${fileName}`; res.json({ path: fileUrl }); }); @@ -47,7 +69,6 @@ router.post('/upload', (req, res) => { } }); -// 获取背景设置 router.get('/settings', (req, res) => { try { const settings = JSON.parse(fs.readFileSync(SETTINGS_FILE, 'utf8')); @@ -58,7 +79,6 @@ router.get('/settings', (req, res) => { } }); -// 保存背景设置 router.post('/settings', (req, res) => { try { const settings = req.body; diff --git a/backend/routes/devicePorts.js b/backend/routes/devicePorts.js index 4ca4d5c..d8f6ddd 100644 --- a/backend/routes/devicePorts.js +++ b/backend/routes/devicePorts.js @@ -3,6 +3,11 @@ const router = express.Router(); const { Op } = require('sequelize'); const DevicePort = require('../models/DevicePort'); const Device = require('../models/Device'); +const NetworkCard = require('../models/NetworkCard'); + +DevicePort.belongsTo(Device, { foreignKey: 'deviceId', as: 'device' }); +Device.hasMany(DevicePort, { foreignKey: 'deviceId', as: 'ports' }); +DevicePort.belongsTo(NetworkCard, { foreignKey: 'nicId', as: 'networkCard' }); router.get('/', async (req, res) => { try { diff --git a/backend/routes/devices.js b/backend/routes/devices.js index 8ae7f57..aee7024 100644 --- a/backend/routes/devices.js +++ b/backend/routes/devices.js @@ -1,7 +1,7 @@ const express = require('express'); const router = express.Router(); const { Op } = require('sequelize'); -const { sequelize, dbDialect } = require('../db'); // Import sequelize and dbDialect for transactions +const { sequelize, dbDialect } = require('../db'); const fs = require('fs'); const path = require('path'); const csv = require('csv-parser'); @@ -12,9 +12,9 @@ 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 DevicePort = require('../models/DevicePort'); +const Cable = require('../models/Cable'); +const NetworkCard = require('../models/NetworkCard'); const { validateBody, validateQuery } = require('../middleware/validation'); const { createDeviceSchema, @@ -25,7 +25,9 @@ const { queryDeviceSchema } = require('../validation/deviceSchema'); -// 获取所有设备(支持搜索和筛选) +Device.belongsTo(Rack, { foreignKey: 'rackId' }); +Rack.hasMany(Device, { foreignKey: 'rackId' }); + router.get('/', validateQuery(queryDeviceSchema), async (req, res) => { try { const { keyword, status, type, rackId, page = 1, pageSize = 10 } = req.query; @@ -772,6 +774,16 @@ router.post('/import', async (req, res) => { } catch (error) { await t.rollback(); console.error('导入设备数据失败:', error); + + // 清理临时文件 + try { + if (filePath && fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + } + } catch (fileErr) { + console.error('删除临时文件失败:', fileErr); + } + res.status(500).json({ errors: [{ row: 0, error: error.message || '导入过程中发生未知错误' }] }); @@ -1224,7 +1236,22 @@ router.delete('/:deviceId', async (req, res) => { { where: { deviceId: deviceId }, transaction: t } ); - // 5. 删除设备 (Delete Device) + // 5. 更新机柜功率 (必须在删除设备之前) + if (device.rackId) { + try { + const rack = await Rack.findByPk(device.rackId, { transaction: t }); + if (rack) { + await rack.update({ + currentPower: Math.max(0, rack.currentPower - device.powerConsumption) + }, { transaction: t }); + } + } catch (err) { + console.error('更新机柜功率失败:', err); + throw err; // 重新抛出错误,触发事务回滚 + } + } + + // 6. 删除设备 (Delete Device) await Device.destroy({ where: { deviceId: deviceId }, transaction: t @@ -1237,23 +1264,6 @@ router.delete('/:deviceId', async (req, res) => { 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, diff --git a/backend/routes/networkCards.js b/backend/routes/networkCards.js index 32aa1cb..619ca1c 100644 --- a/backend/routes/networkCards.js +++ b/backend/routes/networkCards.js @@ -5,6 +5,10 @@ const NetworkCard = require('../models/NetworkCard'); const Device = require('../models/Device'); const DevicePort = require('../models/DevicePort'); +NetworkCard.belongsTo(Device, { foreignKey: 'deviceId', as: 'device' }); +Device.hasMany(NetworkCard, { foreignKey: 'deviceId', as: 'networkCards' }); +NetworkCard.hasMany(DevicePort, { foreignKey: 'nicId', as: 'ports' }); + router.get('/', async (req, res) => { try { const { deviceId } = req.query; diff --git a/backend/server.js b/backend/server.js index 135cff6..892ff39 100644 --- a/backend/server.js +++ b/backend/server.js @@ -180,6 +180,37 @@ app.use('/api/inventory', inventoryRoutes); app.use('/uploads', express.static('uploads')); +app.get('/api', (req, res) => { + res.json({ + name: 'IDC设备管理系统 API', + version: '1.0.0', + description: '数据中心设备管理平台后端服务', + endpoints: { + auth: '/api/auth', + rooms: '/api/rooms', + racks: '/api/racks', + devices: '/api/devices', + deviceFields: '/api/deviceFields', + devicePorts: '/api/device-ports', + networkCards: '/api/network-cards', + cables: '/api/cables', + tickets: '/api/tickets', + ticketCategories: '/api/ticket-categories', + ticketFields: '/api/ticket-fields', + consumables: '/api/consumables', + consumableRecords: '/api/consumable-records', + consumableCategories: '/api/consumable-categories', + users: '/api/users', + roles: '/api/roles', + systemSettings: '/api/system-settings', + background: '/api/background', + inventory: '/api/inventory' + }, + health: '/health', + documentation: '/docs/api/README.md' + }); +}); + app.get('/health', (req, res) => { res.json({ status: 'ok', message: 'IDC设备管理系统后端服务正常运行' }); }); diff --git a/backend/validation/deviceSchema.js b/backend/validation/deviceSchema.js index 594bc83..2dd96d9 100644 --- a/backend/validation/deviceSchema.js +++ b/backend/validation/deviceSchema.js @@ -1,168 +1,61 @@ const Joi = require('joi'); -const DeviceField = require('../models/DeviceField'); const DEVICE_TYPES = ['server', 'switch', 'router', 'storage', 'other']; const DEVICE_STATUS = ['running', 'maintenance', 'offline', 'fault']; -const baseFieldSchemas = { - deviceId: Joi.string() - .max(50) - .pattern(/^[a-zA-Z0-9_-]+$/) - .allow('', null) - .messages({ - 'string.max': '设备ID不能超过50个字符', - 'string.pattern.base': '设备ID只能包含字母、数字、下划线和横线' - }), - - name: Joi.string() - .max(100) - .messages({ - 'string.empty': '设备名称不能为空', - 'string.max': '设备名称不能超过100个字符' - }), - - type: Joi.string() - .valid(...DEVICE_TYPES) - .messages({ - 'string.empty': '设备类型不能为空', - 'any.only': `设备类型必须是以下之一: ${DEVICE_TYPES.join(', ')}` - }), - - model: Joi.string() - .max(100) - .allow('', null) - .messages({ - 'string.max': '型号不能超过100个字符' - }), - - serialNumber: Joi.string() - .max(100) - .messages({ - 'string.empty': '序列号不能为空', - 'string.max': '序列号不能超过100个字符' - }), - - rackId: Joi.string() - .max(50) - .messages({ - 'string.empty': '机柜ID不能为空', - 'string.max': '机柜ID不能超过50个字符' - }), - - position: Joi.number() - .integer() - .min(1) - .max(100) - .messages({ - 'number.base': '位置必须是数字', - 'number.integer': '位置必须是整数', - 'number.min': '位置不能小于1', - 'number.max': '位置不能大于100' - }), - - height: Joi.number() - .integer() - .min(1) - .max(50) - .messages({ - 'number.base': '高度必须是数字', - 'number.integer': '高度必须是整数', - 'number.min': '高度不能小于1', - 'number.max': '高度不能大于50' - }), - - powerConsumption: Joi.number() - .min(0) - .max(100000) - .messages({ - 'number.base': '功率必须是数字', - 'number.min': '功率不能小于0', - 'number.max': '功率不能超过100000' - }), - - ipAddress: Joi.string() - .ip({ version: ['ipv4', 'ipv6'] }) - .allow('', null) - .messages({ - 'string.ip': 'IP地址格式无效' - }), - - status: Joi.string() - .valid(...DEVICE_STATUS) - .default('offline') - .messages({ - 'any.only': `状态必须是以下之一: ${DEVICE_STATUS.join(', ')}` - }), - - purchaseDate: Joi.date() - .allow(null) - .messages({ - 'date.base': '购买日期格式无效' - }), - - warrantyExpiry: Joi.date() - .allow(null) - .messages({ - 'date.base': '保修到期日期格式无效' - }), - - description: Joi.string() - .max(500) - .allow('', null) - .messages({ - 'string.max': '描述不能超过500个字符' - }), - +const createDeviceSchema = Joi.object({ + name: Joi.string().required().max(100).messages({ + 'string.empty': '设备名称不能为空', + 'string.max': '设备名称不能超过100个字符', + 'any.required': '设备名称是必填字段' + }), + type: Joi.string().required().valid(...DEVICE_TYPES).messages({ + 'any.only': `设备类型必须是以下之一: ${DEVICE_TYPES.join(', ')}`, + 'any.required': '设备类型是必填字段' + }), + model: Joi.string().allow('', null).max(100), + serialNumber: Joi.string().required().max(100).messages({ + 'string.empty': '序列号不能为空', + 'string.max': '序列号不能超过100个字符', + 'any.required': '序列号是必填字段' + }), + rackId: Joi.string().allow('', null).max(50), + position: Joi.number().integer().min(1).max(100).allow(null), + height: Joi.number().integer().min(1).max(50).allow(null), + powerConsumption: Joi.number().min(0).max(100000).allow(null), + ipAddress: Joi.string().allow('', null).max(50), + status: Joi.string().valid(...DEVICE_STATUS).default('offline'), + purchaseDate: Joi.date().allow(null), + warrantyExpiry: Joi.date().allow(null), + description: Joi.string().allow('', null).max(500), customFields: Joi.object().allow(null) -}; +}); -async function buildDynamicSchema(isCreate = true) { - const fields = await DeviceField.findAll({ - where: { isSystem: true }, - order: [['order', 'ASC']] - }); - - const schemaObj = {}; - - fields.forEach(field => { - const baseSchema = baseFieldSchemas[field.fieldName]; - if (baseSchema) { - let fieldSchema = baseSchema.clone(); - - if (field.required && isCreate) { - fieldSchema = fieldSchema.required(); - } - - schemaObj[field.fieldName] = fieldSchema; - } - }); - - schemaObj.customFields = baseFieldSchemas.customFields; - - return Joi.object(schemaObj).custom((value, helpers) => { - if (value.purchaseDate && value.warrantyExpiry) { - const purchase = new Date(value.purchaseDate); - const warranty = new Date(value.warrantyExpiry); - if (warranty <= purchase) { - return helpers.error('date.warrantyAfterPurchase'); - } - } - return value; - }).messages({ - 'date.warrantyAfterPurchase': '保修到期日期必须晚于购买日期' - }); -} - -async function getCreateDeviceSchema() { - return buildDynamicSchema(true); -} - -async function getUpdateDeviceSchema() { - const schema = await buildDynamicSchema(false); - return schema.min(1).messages({ - 'object.min': '至少需要提供一个字段进行更新' - }); -} +const updateDeviceSchema = Joi.object({ + name: Joi.string().max(100).messages({ + 'string.empty': '设备名称不能为空', + 'string.max': '设备名称不能超过100个字符' + }), + type: Joi.string().valid(...DEVICE_TYPES).messages({ + 'any.only': `设备类型必须是以下之一: ${DEVICE_TYPES.join(', ')}` + }), + model: Joi.string().allow('', null).max(100), + serialNumber: Joi.string().max(100).messages({ + 'string.max': '序列号不能超过100个字符' + }), + rackId: Joi.string().allow('', null).max(50), + position: Joi.number().integer().min(1).max(100).allow(null), + height: Joi.number().integer().min(1).max(50).allow(null), + powerConsumption: Joi.number().min(0).max(100000).allow(null), + ipAddress: Joi.string().allow('', null).max(50), + status: Joi.string().valid(...DEVICE_STATUS), + purchaseDate: Joi.date().allow(null), + warrantyExpiry: Joi.date().allow(null), + description: Joi.string().allow('', null).max(500), + customFields: Joi.object().allow(null) +}).min(1).messages({ + 'object.min': '至少需要提供一个字段进行更新' +}); const batchDeviceIdsSchema = Joi.object({ deviceIds: Joi.array() @@ -233,24 +126,10 @@ const queryDeviceSchema = Joi.object({ pageSize: Joi.number() .integer() .min(1) - .max(100) + .max(10000) .default(10) }); -const createDeviceSchema = { - validate: async (data, options = {}) => { - const schema = await getCreateDeviceSchema(); - return schema.validate(data, options); - } -}; - -const updateDeviceSchema = { - validate: async (data, options = {}) => { - const schema = await getUpdateDeviceSchema(); - return schema.validate(data, options); - } -}; - module.exports = { createDeviceSchema, updateDeviceSchema, @@ -259,7 +138,5 @@ module.exports = { batchMoveSchema, queryDeviceSchema, DEVICE_TYPES, - DEVICE_STATUS, - getCreateDeviceSchema, - getUpdateDeviceSchema + DEVICE_STATUS }; diff --git a/docs/api/README.md b/docs/api/README.md index ac051f2..f5f4e7f 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -25,6 +25,7 @@ - [角色管理接口](#角色管理接口) - [系统设置接口](#系统设置接口) - [背景配置接口](#背景配置接口) +- [盘点管理接口](#盘点管理接口) - [健康检查接口](#健康检查接口) - [错误码说明](#错误码说明) @@ -210,23 +211,23 @@ GET /api/rooms ```json { - "success": true, - "data": [ - { - "roomId": "room001", - "name": "A区机房", - "location": "一楼东侧", - "area": 500, - "description": "主要服务器机房", - "status": "active", - "createdAt": "2024-01-01T00:00:00.000Z", - "updatedAt": "2024-01-01T00:00:00.000Z" - } - ], - "message": "操作成功" + "roomId": "room001", + "name": "A区机房", + "location": "一楼东侧", + "area": 500, + "description": "主要服务器机房", + "status": "active", + "createdAt": "2024-01-01T00:00:00.000Z", + "updatedAt": "2024-01-01T00:00:00.000Z" } ``` +### 获取单个机房 + +```http +GET /api/rooms/:roomId +``` + ### 创建机房 ```http @@ -288,35 +289,43 @@ GET /api/racks | 参数名 | 类型 | 描述 | |--------|------|------| | roomId | string | 按机房ID筛选 | +| status | string | 按状态筛选 | | keyword | string | 按名称搜索 | +| page | number | 页码 | +| pageSize | number | 每页数量 | **响应示例:** ```json { - "success": true, - "data": [ + "racks": [ { "rackId": "rack001", "name": "机柜A1", "height": 42, - "powerRating": 5000, - "RoomId": "room001", + "maxPower": 5000, + "currentPower": 1200, + "roomId": "room001", "Room": { "roomId": "room001", "name": "A区机房" }, "Devices": [], - "deviceCount": 5, - "usedHeight": 10, + "status": "active", "createdAt": "2024-01-01T00:00:00.000Z", "updatedAt": "2024-01-01T00:00:00.000Z" } ], - "message": "操作成功" + "total": 100 } ``` +### 获取单个机柜 + +```http +GET /api/racks/:rackId +``` + ### 创建机柜 ```http @@ -327,22 +336,23 @@ POST /api/racks | 参数名 | 类型 | 必填 | 描述 | |--------|------|------|------| -| rackId | string | 是 | 机柜ID(唯一标识) | +| rackId | string | 否 | 机柜ID(留空自动生成) | | name | string | 是 | 机柜名称 | | height | number | 否 | 高度(U),默认42 | -| powerRating | number | 否 | 额定功率(W) | -| RoomId | string | 是 | 所属机房ID | +| maxPower | number | 否 | 额定功率(W) | +| roomId | string | 是 | 所属机房ID | +| status | string | 否 | 状态(active/maintenance/inactive) | | description | string | 否 | 描述 | **请求示例:** ```json { - "rackId": "rack002", "name": "机柜A2", "height": 42, - "powerRating": 5000, - "RoomId": "room001", + "maxPower": 5000, + "roomId": "room001", + "status": "active", "description": "核心交换机机柜" } ``` @@ -361,39 +371,36 @@ DELETE /api/racks/:rackId **说明:** 删除机柜前需确保机柜下无设备 -### 获取机柜详情 +### 获取机柜导入模板 ```http -GET /api/racks/:rackId +GET /api/racks/import-template ``` -**响应示例:** +**响应:** 返回Excel模板文件 -```json -{ - "success": true, - "data": { - "rackId": "rack001", - "name": "机柜A1", - "height": 42, - "powerRating": 5000, - "RoomId": "room001", - "Room": {...}, - "Devices": [ - { - "deviceId": "dev001", - "name": "Web服务器01", - "rackPosition": 1, - "height": 2 - } - ], - "createdAt": "2024-01-01T00:00:00.000Z", - "updatedAt": "2024-01-01T00:00:00.000Z" - }, - "message": "操作成功" -} +### 导出机柜数据 + +```http +GET /api/racks/export ``` +**响应:** 返回Excel文件 + +### 导入机柜数据 + +```http +POST /api/racks/import +``` + +**Content-Type**: `multipart/form-data` + +**请求参数:** + +| 参数名 | 类型 | 必填 | 描述 | +|--------|------|------|------| +| file | File | 是 | Excel格式的机柜数据文件 | + --- ## 设备管理接口 @@ -409,9 +416,9 @@ GET /api/devices | 参数名 | 类型 | 描述 | |--------|------|------| | rackId | string | 按机柜ID筛选 | -| deviceType | string | 按设备类型筛选 | +| type | string | 按设备类型筛选 | | status | string | 按状态筛选 | -| keyword | string | 按名称/IP搜索 | +| keyword | string | 按名称/IP/序列号搜索 | | page | number | 页码,默认1 | | pageSize | number | 每页数量,默认10 | @@ -419,44 +426,46 @@ GET /api/devices ```json { - "success": true, - "data": { - "devices": [ - { - "deviceId": "dev001", - "name": "Web服务器01", - "deviceType": "服务器", - "manufacturer": "Dell", - "model": "R740", - "rackPosition": 1, - "height": 2, - "ipAddress": "192.168.1.100", - "macAddress": "00:1B:44:11:3A:B7", - "status": "运行中", - "purchaseDate": "2023-01-01", - "warrantyDate": "2026-01-01", - "description": "主要Web应用服务器", - "RackId": "rack001", - "Rack": { - "rackId": "rack001", - "name": "机柜A1", - "Room": { - "roomId": "room001", - "name": "A区机房" - } - }, - "createdAt": "2024-01-01T00:00:00.000Z", - "updatedAt": "2024-01-01T00:00:00.000Z" - } - ], - "total": 100, - "page": 1, - "pageSize": 10 - }, - "message": "操作成功" + "devices": [ + { + "deviceId": "dev001", + "name": "Web服务器01", + "type": "server", + "model": "R740", + "serialNumber": "SN123456", + "rackId": "rack001", + "position": 1, + "height": 2, + "powerConsumption": 500, + "ipAddress": "192.168.1.100", + "status": "running", + "purchaseDate": "2023-01-01", + "warrantyExpiry": "2026-01-01", + "description": "主要Web应用服务器", + "Rack": { + "rackId": "rack001", + "name": "机柜A1", + "Room": { + "roomId": "room001", + "name": "A区机房" + } + }, + "createdAt": "2024-01-01T00:00:00.000Z", + "updatedAt": "2024-01-01T00:00:00.000Z" + } + ], + "total": 100, + "page": 1, + "pageSize": 10 } ``` +### 获取单个设备 + +```http +GET /api/devices/:deviceId +``` + ### 创建设备 ```http @@ -467,19 +476,19 @@ POST /api/devices | 参数名 | 类型 | 必填 | 描述 | |--------|------|------|------| -| deviceId | string | 是 | 设备ID(唯一标识) | +| deviceId | string | 否 | 设备ID(留空自动生成) | | name | string | 是 | 设备名称 | -| deviceType | string | 是 | 设备类型(服务器/网络设备/存储设备/其他) | -| manufacturer | string | 否 | 厂商 | +| type | string | 是 | 设备类型(server/switch/router/storage/other) | | model | string | 否 | 型号 | -| RackId | string | 否 | 所属机柜ID | -| rackPosition | number | 否 | 机柜位置(从1开始) | +| serialNumber | string | 是 | 序列号 | +| rackId | string | 否 | 所属机柜ID | +| position | number | 否 | 机柜位置(从1开始) | | height | number | 否 | 占用高度(U) | +| powerConsumption | number | 否 | 功耗(W) | | ipAddress | string | 否 | IP地址 | -| macAddress | string | 否 | MAC地址 | -| status | string | 否 | 状态(运行中/已关机/维护中/故障) | +| status | string | 否 | 状态(running/maintenance/offline/fault) | | purchaseDate | string | 否 | 购买日期(YYYY-MM-DD) | -| warrantyDate | string | 否 | 保修日期(YYYY-MM-DD) | +| warrantyExpiry | string | 否 | 保修到期日期(YYYY-MM-DD) | | description | string | 否 | 描述 | | customFields | object | 否 | 自定义字段值 | @@ -487,16 +496,16 @@ POST /api/devices ```json { - "deviceId": "dev002", "name": "数据库服务器", - "deviceType": "服务器", - "manufacturer": "HP", + "type": "server", "model": "DL380", - "RackId": "rack001", - "rackPosition": 3, + "serialNumber": "SN789012", + "rackId": "rack001", + "position": 3, "height": 2, + "powerConsumption": 600, "ipAddress": "192.168.1.101", - "status": "运行中", + "status": "running", "customFields": { "cpuModel": "Intel Xeon E5-2680", "memorySize": "64GB" @@ -516,10 +525,109 @@ PUT /api/devices/:deviceId DELETE /api/devices/:deviceId ``` -### 批量导入设备 +### 批量删除设备 ```http -POST /api/devices/batch-import +DELETE /api/devices/batch-delete +``` + +**请求参数:** + +| 参数名 | 类型 | 必填 | 描述 | +|--------|------|------|------| +| deviceIds | array | 是 | 设备ID列表 | + +**请求示例:** + +```json +{ + "deviceIds": ["dev001", "dev002", "dev003"] +} +``` + +### 删除所有设备 + +```http +DELETE /api/devices/delete-all +``` + +### 批量上线设备 + +```http +PUT /api/devices/batch-online +``` + +**请求参数:** + +| 参数名 | 类型 | 必填 | 描述 | +|--------|------|------|------| +| deviceIds | array | 是 | 设备ID列表 | + +### 批量下线设备 + +```http +PUT /api/devices/batch-offline +``` + +**请求参数:** + +| 参数名 | 类型 | 必填 | 描述 | +|--------|------|------|------| +| deviceIds | array | 是 | 设备ID列表 | + +### 批量变更设备状态 + +```http +PUT /api/devices/batch-status +``` + +**请求参数:** + +| 参数名 | 类型 | 必填 | 描述 | +|--------|------|------|------| +| deviceIds | array | 是 | 设备ID列表 | +| status | string | 是 | 目标状态(running/maintenance/offline/fault) | + +### 批量移动设备 + +```http +PUT /api/devices/batch-move +``` + +**请求参数:** + +| 参数名 | 类型 | 必填 | 描述 | +|--------|------|------|------| +| deviceIds | array | 是 | 设备ID列表 | +| targetRackId | string | 是 | 目标机柜ID | +| startPosition | number | 否 | 起始位置 | + +### 获取设备导入模板 + +```http +GET /api/devices/import-template +``` + +**响应:** 返回CSV模板文件 + +### 导出设备数据 + +```http +GET /api/devices/export +``` + +**查询参数:** + +| 参数名 | 类型 | 描述 | +|--------|------|------| +| deviceIds | string/array | 指定导出的设备ID | + +**响应:** 返回CSV文件 + +### 导入设备数据 + +```http +POST /api/devices/import ``` **Content-Type**: `multipart/form-data` @@ -528,12 +636,36 @@ POST /api/devices/batch-import | 参数名 | 类型 | 必填 | 描述 | |--------|------|------|------| -| file | File | 是 | Excel或CSV格式的设备数据文件 | +| csvFile | File | 是 | CSV格式的设备数据文件 | -**文件格式要求:** -- 支持 .xlsx, .xls, .csv 格式 -- 第一行为表头 -- 必需字段:deviceId, name, deviceType +### 增强导出设备数据 + +```http +GET /api/devices/enhanced-export +``` + +**查询参数:** + +| 参数名 | 类型 | 描述 | +|--------|------|------| +| deviceIds | string/array | 指定导出的设备ID | +| format | string | 导出格式(csv/json) | +| fields | string | JSON格式的字段列表 | +| fieldLabels | string | JSON格式的字段标签映射 | + +### 获取设备的工单列表 + +```http +GET /api/devices/:deviceId/tickets +``` + +**查询参数:** + +| 参数名 | 类型 | 描述 | +|--------|------|------| +| status | string | 按状态筛选 | +| page | number | 页码 | +| pageSize | number | 每页数量 | --- @@ -555,11 +687,11 @@ GET /api/deviceFields "id": 1, "fieldName": "cpuModel", "displayName": "CPU型号", - "fieldType": "text", - "isRequired": false, + "fieldType": "string", + "required": false, "defaultValue": "", "options": null, - "sortOrder": 1, + "order": 1, "isSystem": false, "createdAt": "2024-01-01T00:00:00.000Z", "updatedAt": "2024-01-01T00:00:00.000Z" @@ -581,11 +713,12 @@ POST /api/deviceFields |--------|------|------|------| | fieldName | string | 是 | 字段名(英文,唯一) | | displayName | string | 是 | 显示名称(中文) | -| fieldType | string | 是 | 字段类型(text/number/date/select) | -| isRequired | boolean | 否 | 是否必填,默认false | +| fieldType | string | 是 | 字段类型(string/number/date/select/boolean/textarea) | +| required | boolean | 否 | 是否必填,默认false | | defaultValue | string | 否 | 默认值 | -| options | string | 否 | 选项(逗号分隔,select类型使用) | -| sortOrder | number | 否 | 排序顺序 | +| options | array | 否 | 选项(select类型使用) | +| order | number | 否 | 排序顺序 | +| visible | boolean | 否 | 是否可见,默认true | ### 更新设备字段 @@ -616,6 +749,23 @@ GET /api/device-ports | 参数名 | 类型 | 描述 | |--------|------|------| | deviceId | string | 按设备ID筛选 | +| status | string | 按状态筛选(free/occupied/fault) | +| portType | string | 按端口类型筛选 | +| portSpeed | string | 按端口速率筛选 | +| page | number | 页码 | +| pageSize | number | 每页数量 | + +### 获取设备的所有端口 + +```http +GET /api/device-ports/device/:deviceId +``` + +### 获取单个端口详情 + +```http +GET /api/device-ports/:portId +``` ### 创建端口 @@ -627,25 +777,52 @@ POST /api/device-ports | 参数名 | 类型 | 必填 | 描述 | |--------|------|------|------| +| portId | string | 否 | 端口ID(留空自动生成) | | deviceId | string | 是 | 所属设备ID | +| nicId | string | 否 | 所属网卡ID | | portName | string | 是 | 端口名称 | -| portType | string | 是 | 端口类型(RJ45/SFP/SFP+/QSFP等) | -| speed | string | 否 | 速率(10M/100M/1G/10G/25G/40G/100G) | -| status | string | 否 | 状态(active/inactive) | +| portType | string | 否 | 端口类型(RJ45/SFP/SFP+/QSFP等),默认RJ45 | +| portSpeed | string | 否 | 速率(10M/100M/1G/10G/25G/40G/100G),默认1G | +| status | string | 否 | 状态(free/occupied/fault),默认free | +| vlanId | string | 否 | VLAN ID | | description | string | 否 | 描述 | +### 批量创建端口 + +```http +POST /api/device-ports/batch +``` + +**请求参数:** + +| 参数名 | 类型 | 必填 | 描述 | +|--------|------|------|------| +| ports | array | 是 | 端口数据列表 | + ### 更新端口 ```http -PUT /api/device-ports/:id +PUT /api/device-ports/:portId ``` ### 删除端口 ```http -DELETE /api/device-ports/:id +DELETE /api/device-ports/:portId ``` +### 批量删除端口 + +```http +DELETE /api/device-ports/batch +``` + +**请求参数:** + +| 参数名 | 类型 | 必填 | 描述 | +|--------|------|------|------| +| portIds | array | 是 | 端口ID列表 | + --- ## 网卡接口 @@ -662,6 +839,30 @@ GET /api/network-cards |--------|------|------| | deviceId | string | 按设备ID筛选 | +### 获取设备的所有网卡 + +```http +GET /api/network-cards/device/:deviceId +``` + +### 获取设备的网卡及端口信息 + +```http +GET /api/network-cards/device/:deviceId/with-ports +``` + +### 获取单个网卡详情 + +```http +GET /api/network-cards/:nicId +``` + +### 获取网卡的端口列表 + +```http +GET /api/network-cards/:nicId/ports +``` + ### 创建网卡 ```http @@ -672,25 +873,29 @@ POST /api/network-cards | 参数名 | 类型 | 必填 | 描述 | |--------|------|------|------| +| nicId | string | 否 | 网卡ID(留空自动生成) | | deviceId | string | 是 | 所属设备ID | | name | string | 是 | 网卡名称 | -| macAddress | string | 否 | MAC地址 | -| ipAddress | string | 否 | IP地址 | -| portIds | array | 否 | 绑定的端口ID列表 | | description | string | 否 | 描述 | +| slotNumber | number | 否 | 插槽号 | +| model | string | 否 | 型号 | +| manufacturer | string | 否 | 厂商 | +| status | string | 否 | 状态(normal/fault),默认normal | ### 更新网卡 ```http -PUT /api/network-cards/:id +PUT /api/network-cards/:nicId ``` ### 删除网卡 ```http -DELETE /api/network-cards/:id +DELETE /api/network-cards/:nicId ``` +**说明:** 删除网卡前需确保网卡下无端口 + --- ## 线缆接口 @@ -705,9 +910,46 @@ GET /api/cables | 参数名 | 类型 | 描述 | |--------|------|------| -| fromRackId | string | 按源机柜筛选 | -| toRackId | string | 按目标机柜筛选 | +| sourceDeviceId | string | 按源设备筛选 | +| targetDeviceId | string | 按目标设备筛选 | | status | string | 按状态筛选 | +| cableType | string | 按线缆类型筛选 | +| page | number | 页码 | +| pageSize | number | 每页数量 | + +### 获取设备的所有接线 + +```http +GET /api/cables/device/:deviceId +``` + +### 获取机柜内所有设备的接线 + +```http +GET /api/cables/rack/:rackId +``` + +### 检查接线冲突 + +```http +POST /api/cables/check-conflict +``` + +**请求参数:** + +| 参数名 | 类型 | 必填 | 描述 | +|--------|------|------|------| +| sourceDeviceId | string | 是 | 源设备ID | +| sourcePort | string | 是 | 源端口名称 | +| targetDeviceId | string | 是 | 目标设备ID | +| targetPort | string | 是 | 目标端口名称 | +| excludeCableId | string | 否 | 排除的线缆ID(用于编辑时) | + +### 获取单个线缆详情 + +```http +GET /api/cables/:cableId +``` ### 创建线缆 @@ -719,29 +961,53 @@ POST /api/cables | 参数名 | 类型 | 必填 | 描述 | |--------|------|------|------| -| cableId | string | 是 | 线缆ID(唯一标识) | -| name | string | 是 | 线缆名称 | -| cableType | string | 是 | 线缆类型(光纤/网线/电源线等) | -| fromRackId | string | 是 | 源机柜ID | -| toRackId | string | 是 | 目标机柜ID | -| fromPortId | string | 否 | 源端口ID | -| toPortId | string | 否 | 目标端口ID | -| length | number | 否 | 长度(米) | -| status | string | 否 | 状态(active/inactive) | +| cableId | string | 否 | 线缆ID(留空自动生成) | +| sourceDeviceId | string | 是 | 源设备ID | +| sourcePort | string | 是 | 源端口名称 | +| targetDeviceId | string | 是 | 目标设备ID | +| targetPort | string | 是 | 目标端口名称 | +| cableType | string | 否 | 线缆类型(ethernet/fiber/optical等),默认ethernet | +| cableLength | number | 否 | 长度(米) | +| status | string | 否 | 状态(normal/fault),默认normal | | description | string | 否 | 描述 | +| force | boolean | 否 | 是否强制创建(覆盖已有连接) | + +### 批量创建线缆 + +```http +POST /api/cables/batch +``` + +**请求参数:** + +| 参数名 | 类型 | 必填 | 描述 | +|--------|------|------|------| +| cables | array | 是 | 线缆数据列表 | ### 更新线缆 ```http -PUT /api/cables/:id +PUT /api/cables/:cableId ``` ### 删除线缆 ```http -DELETE /api/cables/:id +DELETE /api/cables/:cableId ``` +### 批量删除线缆 + +```http +DELETE /api/cables/batch +``` + +**请求参数:** + +| 参数名 | 类型 | 必填 | 描述 | +|--------|------|------|------| +| cableIds | array | 是 | 线缆ID列表 | + --- ## 工单管理接口 @@ -756,10 +1022,14 @@ GET /api/tickets | 参数名 | 类型 | 描述 | |--------|------|------| -| status | string | 按状态筛选(待处理/处理中/已完成/已关闭) | -| priority | string | 按优先级筛选(高/中/低) | -| categoryId | string | 按分类筛选 | -| assigneeId | string | 按负责人筛选 | +| status | string | 按状态筛选(pending/in_progress/completed/closed) | +| priority | string | 按优先级筛选(urgent/high/medium/low) | +| faultCategory | string | 按故障分类筛选 | +| deviceId | string | 按设备筛选 | +| reporterId | string | 按报修人筛选 | +| startDate | string | 开始日期 | +| endDate | string | 结束日期 | +| keyword | string | 关键词搜索 | | page | number | 页码 | | pageSize | number | 每页数量 | @@ -767,42 +1037,48 @@ GET /api/tickets ```json { - "success": true, - "data": { - "tickets": [ - { - "ticketId": "ticket001", - "title": "服务器故障", - "description": "Web服务器无法访问", - "status": "处理中", - "priority": "高", - "categoryId": "cat001", - "Category": { - "categoryId": "cat001", - "name": "硬件故障" - }, - "assigneeId": "user001", - "Assignee": { - "userId": "user001", - "username": "admin" - }, - "requesterId": "user002", - "Requester": { - "userId": "user002", - "username": "operator" - }, - "createdAt": "2024-01-01T00:00:00.000Z", - "updatedAt": "2024-01-01T00:00:00.000Z" - } - ], - "total": 50, - "page": 1, - "pageSize": 10 - }, - "message": "操作成功" + "total": 50, + "tickets": [ + { + "ticketId": "TKT001", + "title": "服务器故障", + "description": "Web服务器无法访问", + "status": "in_progress", + "priority": "high", + "faultCategory": "硬件故障", + "deviceId": "dev001", + "deviceName": "Web服务器01", + "serialNumber": "SN123456", + "reporterId": "user001", + "reporterName": "张三", + "createdAt": "2024-01-01T00:00:00.000Z", + "updatedAt": "2024-01-01T00:00:00.000Z" + } + ], + "page": 1, + "pageSize": 10 } ``` +### 获取工单统计 + +```http +GET /api/tickets/stats +``` + +**查询参数:** + +| 参数名 | 类型 | 描述 | +|--------|------|------| +| startDate | string | 开始日期 | +| endDate | string | 结束日期 | + +### 获取单个工单详情 + +```http +GET /api/tickets/:ticketId +``` + ### 创建工单 ```http @@ -813,13 +1089,19 @@ POST /api/tickets | 参数名 | 类型 | 必填 | 描述 | |--------|------|------|------| -| title | string | 是 | 工单标题 | -| description | string | 是 | 工单描述 | -| categoryId | string | 是 | 工单分类ID | -| priority | string | 是 | 优先级(高/中/低) | -| assigneeId | string | 否 | 指派用户ID | -| deviceId | string | 否 | 关联设备ID | -| customFields | object | 否 | 自定义字段值 | +| deviceId | string | 否 | 关联设备ID(与deviceName+serialNumber二选一) | +| deviceName | string | 否 | 设备名称(手动输入时) | +| serialNumber | string | 否 | 序列号(手动输入时) | +| title | string | 否 | 工单标题 | +| faultCategory | string | 是 | 故障分类 | +| faultSubCategory | string | 否 | 故障子分类 | +| priority | string | 否 | 优先级(urgent/high/medium/low),默认medium | +| description | string | 是 | 问题描述 | +| expectedCompletionDate | string | 否 | 期望完成日期 | +| reporterId | string | 否 | 报修人ID | +| reporterName | string | 否 | 报修人姓名 | +| attachments | array | 否 | 附件列表 | +| tags | array | 否 | 标签列表 | ### 更新工单 @@ -827,18 +1109,87 @@ POST /api/tickets PUT /api/tickets/:ticketId ``` +### 更新工单状态 + +```http +PUT /api/tickets/:ticketId/status +``` + +**请求参数:** + +| 参数名 | 类型 | 必填 | 描述 | +|--------|------|------|------| +| status | string | 是 | 目标状态 | +| resolution | string | 否 | 解决方案(完成时填写) | +| operatorId | string | 否 | 操作人ID | +| operatorName | string | 否 | 操作人姓名 | +| operatorRole | string | 否 | 操作人角色 | + +### 处理工单 + +```http +PUT /api/tickets/:ticketId/process +``` + +**请求参数:** + +| 参数名 | 类型 | 必填 | 描述 | +|--------|------|------|------| +| solution | string | 否 | 解决方案 | +| result | string | 否 | 处理结果(resolved/unresolved) | +| notes | string | 否 | 备注 | +| usedParts | string | 否 | 使用配件 | +| operatorId | string | 否 | 操作人ID | +| operatorName | string | 否 | 操作人姓名 | + +### 添加工单操作记录 + +```http +POST /api/tickets/:ticketId/operations +``` + +**请求参数:** + +| 参数名 | 类型 | 必填 | 描述 | +|--------|------|------|------| +| operationType | string | 是 | 操作类型 | +| operationDescription | string | 是 | 操作描述 | +| operationSteps | array | 否 | 操作步骤 | +| spareParts | array | 否 | 使用配件 | +| duration | number | 否 | 耗时(分钟) | +| result | string | 否 | 结果 | +| notes | string | 否 | 备注 | +| operatorId | string | 否 | 操作人ID | +| operatorName | string | 否 | 操作人姓名 | +| operatorRole | string | 否 | 操作人角色 | + +### 获取工单操作记录 + +```http +GET /api/tickets/:ticketId/operations +``` + ### 删除工单 ```http DELETE /api/tickets/:ticketId ``` -### 获取工单操作记录 +### 评价工单 ```http -GET /api/tickets/:ticketId/operations +POST /api/tickets/:ticketId/evaluate ``` +**请求参数:** + +| 参数名 | 类型 | 必填 | 描述 | +|--------|------|------|------| +| evaluation | string | 是 | 评价内容 | +| evaluationRating | number | 是 | 评分(1-5) | +| operatorId | string | 否 | 操作人ID | +| operatorName | string | 否 | 操作人姓名 | + --- ## 工单分类接口 @@ -930,38 +1281,46 @@ GET /api/consumables | 参数名 | 类型 | 描述 | |--------|------|------| -| categoryId | string | 按分类筛选 | +| category | string | 按分类筛选 | +| status | string | 按状态筛选 | | keyword | string | 按名称搜索 | -| lowStock | boolean | 仅显示库存不足 | +| page | number | 页码 | +| pageSize | number | 每页数量 | **响应示例:** ```json { - "success": true, - "data": [ + "total": 100, + "consumables": [ { "consumableId": "cons001", "name": "硬盘", - "categoryId": "cat001", - "Category": { - "categoryId": "cat001", - "name": "存储设备" - }, + "category": "存储设备", "specification": "1TB SSD", "unit": "个", - "stock": 100, + "currentStock": 100, "minStock": 10, + "maxStock": 200, "unitPrice": 500, - "description": "固态硬盘", + "supplier": "供应商A", + "location": "仓库1", + "status": "active", "createdAt": "2024-01-01T00:00:00.000Z", "updatedAt": "2024-01-01T00:00:00.000Z" } ], - "message": "操作成功" + "page": 1, + "pageSize": 10 } ``` +### 获取单个耗材 + +```http +GET /api/consumables/:id +``` + ### 创建耗材 ```http @@ -972,26 +1331,220 @@ POST /api/consumables | 参数名 | 类型 | 必填 | 描述 | |--------|------|------|------| -| consumableId | string | 是 | 耗材ID(唯一标识) | +| consumableId | string | 否 | 耗材ID(留空自动生成) | | name | string | 是 | 耗材名称 | -| categoryId | string | 是 | 分类ID | +| category | string | 是 | 分类 | | specification | string | 否 | 规格 | | unit | string | 否 | 单位 | -| stock | number | 否 | 库存数量,默认0 | +| currentStock | number | 否 | 当前库存,默认0 | | minStock | number | 否 | 最低库存预警值 | +| maxStock | number | 否 | 最高库存值 | | unitPrice | number | 否 | 单价 | +| supplier | string | 否 | 供应商 | +| location | string | 否 | 存放位置 | +| status | string | 否 | 状态(active/inactive),默认active | | description | string | 否 | 描述 | ### 更新耗材 ```http -PUT /api/consumables/:consumableId +PUT /api/consumables/:id ``` ### 删除耗材 ```http -DELETE /api/consumables/:consumableId +DELETE /api/consumables/:id +``` + +### 导入耗材 + +```http +POST /api/consumables/import +``` + +**请求参数:** + +| 参数名 | 类型 | 必填 | 描述 | +|--------|------|------|------| +| items | array | 是 | 耗材数据列表 | +| operator | string | 否 | 操作人 | + +### 获取耗材分类列表 + +```http +GET /api/consumables/categories/list +``` + +### 获取低库存耗材 + +```http +GET /api/consumables/low-stock +``` + +### 获取耗材统计汇总 + +```http +GET /api/consumables/statistics/summary +``` + +### 获取出入库记录 + +```http +GET /api/consumables/inout/records +``` + +**查询参数:** + +| 参数名 | 类型 | 描述 | +|--------|------|------| +| page | number | 页码 | +| pageSize | number | 每页数量 | + +### 快速出入库 + +```http +POST /api/consumables/quick-inout +``` + +**请求参数:** + +| 参数名 | 类型 | 必填 | 描述 | +|--------|------|------|------| +| consumableId | string | 是 | 耗材ID | +| type | string | 是 | 类型(in/out) | +| quantity | number | 是 | 数量 | +| operator | string | 否 | 操作人 | +| reason | string | 否 | 原因 | +| notes | string | 否 | 备注 | +| snList | array | 否 | SN列表 | + +### 出入库操作 + +```http +POST /api/consumables/inout +``` + +**请求参数:** + +| 参数名 | 类型 | 必填 | 描述 | +|--------|------|------|------| +| consumableId | string | 是 | 耗材ID | +| type | string | 是 | 类型(in/out) | +| quantity | number | 是 | 数量 | +| operator | string | 否 | 操作人 | +| reason | string | 否 | 原因 | +| recipient | string | 否 | 领用人 | +| notes | string | 否 | 备注 | +| snList | array | 否 | SN列表 | + +### 库存调整 + +```http +POST /api/consumables/adjust +``` + +**请求参数:** + +| 参数名 | 类型 | 必填 | 描述 | +|--------|------|------|------| +| consumableId | string | 是 | 耗材ID | +| adjustType | string | 是 | 调整类型(add/subtract/set) | +| quantity | number | 是 | 数量 | +| operator | string | 否 | 操作人 | +| reason | string | 否 | 原因 | +| notes | string | 否 | 备注 | + +### 获取耗材操作日志 + +```http +GET /api/consumables/logs +``` + +**查询参数:** + +| 参数名 | 类型 | 描述 | +|--------|------|------| +| consumableId | string | 按耗材筛选 | +| operationType | string | 按操作类型筛选(多个用逗号分隔) | +| startDate | string | 开始日期 | +| endDate | string | 结束日期 | +| page | number | 页码 | +| pageSize | number | 每页数量 | + +### 导出耗材日志 + +```http +GET /api/consumables/logs/export +``` + +**查询参数:** + +| 参数名 | 类型 | 描述 | +|--------|------|------| +| consumableId | string | 按耗材筛选 | +| operationType | string | 按操作类型筛选 | +| startDate | string | 开始日期 | +| endDate | string | 结束日期 | + +### 导入耗材日志 + +```http +POST /api/consumables/logs/import +``` + +**请求参数:** + +| 参数名 | 类型 | 必填 | 描述 | +|--------|------|------|------| +| logs | array | 是 | 日志数据列表 | +| operator | string | 否 | 操作人 | + +### 修改日志记录 + +```http +PUT /api/consumables/logs/:id +``` + +**请求参数:** + +| 参数名 | 类型 | 必填 | 描述 | +|--------|------|------|------| +| reason | string | 否 | 原因 | +| notes | string | 否 | 备注 | +| operator | string | 否 | 操作人 | +| modificationReason | string | 否 | 修改原因 | + +### 获取日志修改历史 + +```http +GET /api/consumables/logs/:id/history +``` + +### 获取归档记录列表 + +```http +GET /api/consumables/archives +``` + +**查询参数:** + +| 参数名 | 类型 | 描述 | +|--------|------|------| +| keyword | string | 关键词搜索 | +| page | number | 页码 | +| pageSize | number | 每页数量 | + +### 获取归档记录详情 + +```http +GET /api/consumables/archives/:archiveId +``` + +### 根据SN查询耗材 + +```http +GET /api/consumables/by-sn/:sn ``` --- @@ -1081,35 +1634,56 @@ GET /api/users | 参数名 | 类型 | 描述 | |--------|------|------| -| keyword | string | 按用户名/邮箱搜索 | +| username | string | 按用户名搜索 | +| realName | string | 按姓名搜索 | | status | string | 按状态筛选 | +| page | number | 页码 | +| pageSize | number | 每页数量 | **响应示例:** ```json { "success": true, - "data": [ - { - "userId": "user001", - "username": "admin", - "email": "admin@example.com", - "phone": "13800138000", - "status": "active", - "Roles": [ - { - "roleId": "role001", - "roleName": "管理员" - } - ], - "createdAt": "2024-01-01T00:00:00.000Z", - "updatedAt": "2024-01-01T00:00:00.000Z" - } - ], - "message": "操作成功" + "data": { + "total": 50, + "page": 1, + "pageSize": 10, + "users": [ + { + "userId": "user001", + "username": "admin", + "email": "admin@example.com", + "phone": "13800138000", + "realName": "管理员", + "status": "active", + "roles": [ + { + "roleId": "role001", + "roleName": "管理员", + "roleCode": "admin" + } + ], + "createdAt": "2024-01-01T00:00:00.000Z", + "updatedAt": "2024-01-01T00:00:00.000Z" + } + ] + } } ``` +### 获取所有用户(简要信息) + +```http +GET /api/users/all +``` + +### 获取单个用户 + +```http +GET /api/users/:userId +``` + ### 创建用户 ```http @@ -1120,12 +1694,14 @@ POST /api/users | 参数名 | 类型 | 必填 | 描述 | |--------|------|------|------| -| userId | string | 是 | 用户ID(唯一标识) | | username | string | 是 | 用户名 | | password | string | 是 | 密码 | | email | string | 否 | 邮箱 | | phone | string | 否 | 电话 | +| realName | string | 否 | 真实姓名 | +| status | string | 否 | 状态(active/inactive/pending),默认active | | roleIds | array | 否 | 角色ID列表 | +| remark | string | 否 | 备注 | ### 更新用户 @@ -1133,13 +1709,26 @@ POST /api/users PUT /api/users/:userId ``` +**请求参数:** + +| 参数名 | 类型 | 必填 | 描述 | +|--------|------|------|------| +| username | string | 否 | 用户名 | +| email | string | 否 | 邮箱 | +| phone | string | 否 | 电话 | +| realName | string | 否 | 真实姓名 | +| status | string | 否 | 状态 | +| roleIds | array | 否 | 角色ID列表 | +| remark | string | 否 | 备注 | +| newPassword | string | 否 | 新密码 | + ### 删除用户 ```http DELETE /api/users/:userId ``` -### 修改密码 +### 重置密码 ```http PUT /api/users/:userId/password @@ -1149,9 +1738,40 @@ PUT /api/users/:userId/password | 参数名 | 类型 | 必填 | 描述 | |--------|------|------|------| -| oldPassword | string | 是 | 旧密码 | | newPassword | string | 是 | 新密码 | +### 上传头像 + +```http +POST /api/users/:userId/avatar +``` + +**Content-Type**: `multipart/form-data` + +**请求参数:** + +| 参数名 | 类型 | 必填 | 描述 | +|--------|------|------|------| +| avatar | File | 是 | 头像图片文件(JPG/PNG/GIF/WebP) | + +### 删除头像 + +```http +DELETE /api/users/:userId/avatar +``` + +### 审核通过用户 + +```http +PUT /api/users/:userId/approve +``` + +### 拒绝用户注册 + +```http +PUT /api/users/:userId/reject +``` + --- ## 角色管理接口 @@ -1171,7 +1791,9 @@ GET /api/roles { "roleId": "role001", "roleName": "管理员", + "roleCode": "admin", "description": "系统管理员,拥有所有权限", + "status": "active", "Permissions": [ { "permissionId": "perm001", @@ -1198,6 +1820,7 @@ POST /api/roles |--------|------|------|------| | roleId | string | 是 | 角色ID(唯一标识) | | roleName | string | 是 | 角色名称 | +| roleCode | string | 否 | 角色代码 | | description | string | 否 | 描述 | | permissionIds | array | 否 | 权限ID列表 | @@ -1294,6 +1917,217 @@ PUT /api/background --- +## 盘点管理接口 + +### 获取盘点计划列表 + +```http +GET /api/inventory/plans +``` + +**查询参数:** + +| 参数名 | 类型 | 描述 | +|--------|------|------| +| status | string | 按状态筛选(draft/pending/in_progress/completed) | +| keyword | string | 关键词搜索 | +| page | number | 页码 | +| pageSize | number | 每页数量 | + +### 获取单个盘点计划 + +```http +GET /api/inventory/plans/:planId +``` + +### 创建盘点计划 + +```http +POST /api/inventory/plans +``` + +**请求参数:** + +| 参数名 | 类型 | 必填 | 描述 | +|--------|------|------|------| +| name | string | 是 | 计划名称 | +| type | string | 否 | 盘点类型(full/partial),默认full | +| description | string | 否 | 描述 | +| scheduledDate | string | 否 | 计划执行日期 | +| targetRooms | array | 否 | 目标机房ID列表 | +| targetRacks | array | 否 | 目标机柜ID列表 | + +### 更新盘点计划 + +```http +PUT /api/inventory/plans/:planId +``` + +### 删除盘点计划 + +```http +DELETE /api/inventory/plans/:planId +``` + +### 启动盘点计划 + +```http +POST /api/inventory/plans/:planId/start +``` + +### 完成盘点计划 + +```http +POST /api/inventory/plans/:planId/complete +``` + +### 获取盘点任务详情 + +```http +GET /api/inventory/tasks/:taskId +``` + +### 更新盘点任务 + +```http +PUT /api/inventory/tasks/:taskId +``` + +**请求参数:** + +| 参数名 | 类型 | 必填 | 描述 | +|--------|------|------|------| +| assignedTo | string | 否 | 指派人员ID | +| status | string | 否 | 任务状态 | + +### 盘点记录核查 + +```http +POST /api/inventory/records/:recordId/check +``` + +**请求参数:** + +| 参数名 | 类型 | 必填 | 描述 | +|--------|------|------|------| +| actualSerialNumber | string | 否 | 实际序列号 | +| actualRackId | string | 否 | 实际机柜ID | +| actualPosition | number | 否 | 实际位置 | +| status | string | 是 | 核查状态(normal/abnormal/not_found) | +| remark | string | 否 | 备注 | +| photoUrl | string | 否 | 照片URL | + +### 获取盘点记录列表 + +```http +GET /api/inventory/records +``` + +**查询参数:** + +| 参数名 | 类型 | 描述 | +|--------|------|------| +| planId | string | 按计划筛选 | +| taskId | string | 按任务筛选 | +| status | string | 按状态筛选 | +| page | number | 页码 | +| pageSize | number | 每页数量 | + +### 获取盘点统计 + +```http +GET /api/inventory/stats/dashboard +``` + +### 快速添加设备(盘点时) + +```http +POST /api/inventory/quick-add-device +``` + +**请求参数:** + +| 参数名 | 类型 | 必填 | 描述 | +|--------|------|------|------| +| planId | string | 是 | 盘点计划ID | +| taskId | string | 否 | 盘点任务ID | +| serialNumber | string | 是 | 序列号 | +| name | string | 否 | 设备名称 | +| type | string | 否 | 设备类型 | +| roomId | string | 否 | 机房ID | +| rackId | string | 否 | 机柜ID | +| position | number | 否 | 位置 | +| model | string | 否 | 型号 | +| brand | string | 否 | 品牌 | +| height | number | 否 | 高度(U) | +| powerConsumption | number | 否 | 功耗(W) | +| ipAddress | string | 否 | IP地址 | +| purchaseDate | string | 否 | 购买日期 | +| warrantyExpiry | string | 否 | 保修到期日期 | +| description | string | 否 | 描述 | +| remark | string | 否 | 备注 | + +### 获取暂存设备列表 + +```http +GET /api/inventory/pending-devices +``` + +**查询参数:** + +| 参数名 | 类型 | 描述 | +|--------|------|------| +| status | string | 按状态筛选(pending/synced) | +| planId | string | 按盘点计划筛选 | +| roomId | string | 按机房筛选 | +| keyword | string | 关键词搜索 | +| page | number | 页码 | +| pageSize | number | 每页数量 | + +### 获取暂存设备统计 + +```http +GET /api/inventory/pending-devices/stats +``` + +### 获取暂存设备详情 + +```http +GET /api/inventory/pending-devices/:pendingId +``` + +### 更新暂存设备 + +```http +PUT /api/inventory/pending-devices/:pendingId +``` + +### 删除暂存设备 + +```http +DELETE /api/inventory/pending-devices/:pendingId +``` + +### 同步暂存设备到设备管理 + +```http +POST /api/inventory/pending-devices/:pendingId/sync +``` + +### 批量同步暂存设备 + +```http +POST /api/inventory/pending-devices/batch-sync +``` + +**请求参数:** + +| 参数名 | 类型 | 必填 | 描述 | +|--------|------|------|------| +| pendingIds | array | 是 | 暂存设备ID列表 | + +--- + ## 健康检查接口 ### 服务状态检查 @@ -1307,30 +2141,7 @@ GET /health ```json { "status": "ok", - "message": "IDC设备管理系统后端服务正常运行", - "timestamp": "2024-01-01T00:00:00.000Z", - "version": "1.0.0", - "uptime": 3600 -} -``` - -### 数据库连接检查 - -```http -GET /api/health/db -``` - -**响应示例:** - -```json -{ - "success": true, - "data": { - "status": "connected", - "type": "mysql", - "responseTime": "5ms" - }, - "message": "数据库连接正常" + "message": "IDC设备管理系统后端服务正常运行" } ``` @@ -1344,7 +2155,7 @@ GET /api/health/db | 401 | UNAUTHORIZED | 未授权访问,Token无效或过期 | | 403 | FORBIDDEN | 禁止访问,权限不足 | | 404 | NOT_FOUND | 资源不存在 | -| 409 | CONFLICT | 资源冲突(如重复ID) | +| 409 | CONFLICT | 资源冲突(如重复ID、端口占用) | | 422 | VALIDATION_ERROR | 数据验证失败 | | 500 | INTERNAL_ERROR | 服务器内部错误 | | 503 | SERVICE_UNAVAILABLE | 服务暂不可用 | @@ -1387,6 +2198,15 @@ GET /api/health/db } ``` +**资源冲突:** +```json +{ + "error": "端口已被占用", + "conflict": true, + "existingCable": {...} +} +``` + --- ## 接口汇总 @@ -1397,43 +2217,102 @@ GET /api/health/db | /api/auth/register | POST | 用户注册 | | /api/auth/me | GET | 获取当前用户信息 | | /api/rooms | GET/POST | 机房列表/创建 | -| /api/rooms/:id | PUT/DELETE | 机房更新/删除 | +| /api/rooms/:roomId | GET/PUT/DELETE | 机房详情/更新/删除 | | /api/racks | GET/POST | 机柜列表/创建 | -| /api/racks/:id | GET/PUT/DELETE | 机柜详情/更新/删除 | +| /api/racks/:rackId | GET/PUT/DELETE | 机柜详情/更新/删除 | +| /api/racks/import-template | GET | 获取机柜导入模板 | +| /api/racks/export | GET | 导出机柜数据 | +| /api/racks/import | POST | 导入机柜数据 | | /api/devices | GET/POST | 设备列表/创建 | -| /api/devices/:id | PUT/DELETE | 设备更新/删除 | -| /api/devices/batch-import | POST | 批量导入设备 | +| /api/devices/:deviceId | GET/PUT/DELETE | 设备详情/更新/删除 | +| /api/devices/batch-delete | DELETE | 批量删除设备 | +| /api/devices/delete-all | DELETE | 删除所有设备 | +| /api/devices/batch-online | PUT | 批量上线设备 | +| /api/devices/batch-offline | PUT | 批量下线设备 | +| /api/devices/batch-status | PUT | 批量变更设备状态 | +| /api/devices/batch-move | PUT | 批量移动设备 | +| /api/devices/import-template | GET | 获取设备导入模板 | +| /api/devices/export | GET | 导出设备数据 | +| /api/devices/import | POST | 导入设备数据 | +| /api/devices/enhanced-export | GET | 增强导出设备数据 | +| /api/devices/:deviceId/tickets | GET | 获取设备的工单列表 | | /api/deviceFields | GET/POST | 设备字段列表/创建 | | /api/deviceFields/:id | PUT/DELETE | 设备字段更新/删除 | | /api/device-ports | GET/POST | 端口列表/创建 | -| /api/device-ports/:id | PUT/DELETE | 端口更新/删除 | +| /api/device-ports/device/:deviceId | GET | 获取设备的所有端口 | +| /api/device-ports/:portId | GET/PUT/DELETE | 端口详情/更新/删除 | +| /api/device-ports/batch | POST/DELETE | 批量创建/删除端口 | | /api/network-cards | GET/POST | 网卡列表/创建 | -| /api/network-cards/:id | PUT/DELETE | 网卡更新/删除 | +| /api/network-cards/device/:deviceId | GET | 获取设备的所有网卡 | +| /api/network-cards/device/:deviceId/with-ports | GET | 获取设备的网卡及端口信息 | +| /api/network-cards/:nicId | GET/PUT/DELETE | 网卡详情/更新/删除 | +| /api/network-cards/:nicId/ports | GET | 获取网卡的端口列表 | | /api/cables | GET/POST | 线缆列表/创建 | -| /api/cables/:id | PUT/DELETE | 线缆更新/删除 | +| /api/cables/device/:deviceId | GET | 获取设备的所有接线 | +| /api/cables/rack/:rackId | GET | 获取机柜内所有设备的接线 | +| /api/cables/check-conflict | POST | 检查接线冲突 | +| /api/cables/:cableId | GET/PUT/DELETE | 线缆详情/更新/删除 | +| /api/cables/batch | POST/DELETE | 批量创建/删除线缆 | | /api/tickets | GET/POST | 工单列表/创建 | -| /api/tickets/:id | PUT/DELETE | 工单更新/删除 | -| /api/tickets/:id/operations | GET | 工单操作记录 | +| /api/tickets/stats | GET | 工单统计 | +| /api/tickets/:ticketId | GET/PUT/DELETE | 工单详情/更新/删除 | +| /api/tickets/:ticketId/status | PUT | 更新工单状态 | +| /api/tickets/:ticketId/process | PUT | 处理工单 | +| /api/tickets/:ticketId/operations | GET/POST | 工单操作记录/添加操作记录 | +| /api/tickets/:ticketId/evaluate | POST | 评价工单 | | /api/ticket-categories | GET/POST | 工单分类列表/创建 | | /api/ticket-categories/:id | PUT/DELETE | 工单分类更新/删除 | | /api/ticket-fields | GET/POST | 工单字段列表/创建 | | /api/ticket-fields/:id | PUT/DELETE | 工单字段更新/删除 | | /api/consumables | GET/POST | 耗材列表/创建 | -| /api/consumables/:id | PUT/DELETE | 耗材更新/删除 | +| /api/consumables/:id | GET/PUT/DELETE | 耗材详情/更新/删除 | +| /api/consumables/import | POST | 导入耗材 | +| /api/consumables/categories/list | GET | 获取耗材分类列表 | +| /api/consumables/low-stock | GET | 获取低库存耗材 | +| /api/consumables/statistics/summary | GET | 获取耗材统计汇总 | +| /api/consumables/inout/records | GET | 获取出入库记录 | +| /api/consumables/quick-inout | POST | 快速出入库 | +| /api/consumables/inout | POST | 出入库操作 | +| /api/consumables/adjust | POST | 库存调整 | +| /api/consumables/logs | GET | 获取耗材操作日志 | +| /api/consumables/logs/export | GET | 导出耗材日志 | +| /api/consumables/logs/import | POST | 导入耗材日志 | +| /api/consumables/logs/:id | PUT | 修改日志记录 | +| /api/consumables/logs/:id/history | GET | 获取日志修改历史 | +| /api/consumables/archives | GET | 获取归档记录列表 | +| /api/consumables/archives/:archiveId | GET | 获取归档记录详情 | +| /api/consumables/by-sn/:sn | GET | 根据SN查询耗材 | | /api/consumable-categories | GET/POST | 耗材分类列表/创建 | | /api/consumable-categories/:id | PUT/DELETE | 耗材分类更新/删除 | | /api/consumable-records | GET/POST | 耗材记录列表/创建 | | /api/users | GET/POST | 用户列表/创建 | -| /api/users/:id | PUT/DELETE | 用户更新/删除 | -| /api/users/:id/password | PUT | 修改密码 | +| /api/users/all | GET | 获取所有用户(简要信息) | +| /api/users/:userId | GET/PUT/DELETE | 用户详情/更新/删除 | +| /api/users/:userId/password | PUT | 重置密码 | +| /api/users/:userId/avatar | POST/DELETE | 上传/删除头像 | +| /api/users/:userId/approve | PUT | 审核通过用户 | +| /api/users/:userId/reject | PUT | 拒绝用户注册 | | /api/roles | GET/POST | 角色列表/创建 | -| /api/roles/:id | PUT/DELETE | 角色更新/删除 | +| /api/roles/:roleId | PUT/DELETE | 角色更新/删除 | | /api/system-settings | GET/PUT | 系统设置获取/更新 | | /api/background | GET/PUT | 背景配置获取/更新 | +| /api/inventory/plans | GET/POST | 盘点计划列表/创建 | +| /api/inventory/plans/:planId | GET/PUT/DELETE | 盘点计划详情/更新/删除 | +| /api/inventory/plans/:planId/start | POST | 启动盘点计划 | +| /api/inventory/plans/:planId/complete | POST | 完成盘点计划 | +| /api/inventory/tasks/:taskId | GET/PUT | 盘点任务详情/更新 | +| /api/inventory/records | GET | 盘点记录列表 | +| /api/inventory/records/:recordId/check | POST | 盘点记录核查 | +| /api/inventory/stats/dashboard | GET | 盘点统计 | +| /api/inventory/quick-add-device | POST | 快速添加设备 | +| /api/inventory/pending-devices | GET | 暂存设备列表 | +| /api/inventory/pending-devices/stats | GET | 暂存设备统计 | +| /api/inventory/pending-devices/:pendingId | GET/PUT/DELETE | 暂存设备详情/更新/删除 | +| /api/inventory/pending-devices/:pendingId/sync | POST | 同步暂存设备 | +| /api/inventory/pending-devices/batch-sync | POST | 批量同步暂存设备 | | /health | GET | 服务健康检查 | -| /api/health/db | GET | 数据库健康检查 | --- -**文档版本:** 1.2.0 -**最后更新:** 2026-02-05 +**文档版本:** 2.0.0 +**最后更新:** 2026-03-11 diff --git a/frontend/src/components/CableCreateModal.jsx b/frontend/src/components/CableCreateModal.jsx index 1a19852..518fd7f 100644 --- a/frontend/src/components/CableCreateModal.jsx +++ b/frontend/src/components/CableCreateModal.jsx @@ -13,20 +13,27 @@ const CableCreateModal = ({ visible, onClose, onSuccess, sourceDevice }) => { const [sourcePorts, setSourcePorts] = useState([]); const [targetPorts, setTargetPorts] = useState([]); const [fetchingDevices, setFetchingDevices] = useState(false); - const prevVisibleRef = useRef(false); + const devicesRef = useRef([]); const fetchDevices = useCallback(async (keyword = '') => { try { setFetchingDevices(true); - const params = { pageSize: 50 }; + const params = { pageSize: 100 }; if (keyword && keyword.trim()) { params.keyword = keyword.trim(); } + console.log('[CableCreateModal] Fetching devices with params:', params); const response = await axios.get('/api/devices', { params }); - setDevices(response.data.devices || []); + console.log('[CableCreateModal] API response:', response.data); + const deviceList = response.data.devices || []; + console.log('[CableCreateModal] Device list:', deviceList.length, 'devices'); + setDevices(deviceList); + devicesRef.current = deviceList; + return deviceList; } catch (error) { - console.error('Failed to fetch devices:', error); + console.error('[CableCreateModal] Failed to fetch devices:', error); message.error('获取设备列表失败'); + return []; } finally { setFetchingDevices(false); } @@ -40,18 +47,32 @@ const CableCreateModal = ({ visible, onClose, onSuccess, sourceDevice }) => { ); useEffect(() => { - if (visible && !prevVisibleRef.current) { + if (visible) { + console.log('[CableCreateModal] Modal opened, sourceDevice:', sourceDevice); form.resetFields(); - if (sourceDevice) { - form.setFieldsValue({ - sourceDeviceId: sourceDevice.deviceId || sourceDevice.id, - }); - fetchDevicePorts(sourceDevice.deviceId || sourceDevice.id, 'source'); - } - fetchDevices(); + setSourcePorts([]); + setTargetPorts([]); + setDevices([]); + devicesRef.current = []; + + fetchDevices().then(deviceList => { + console.log('[CableCreateModal] Devices fetched:', deviceList.length); + const sourceDeviceId = sourceDevice?.deviceId || sourceDevice?.id; + console.log('[CableCreateModal] sourceDeviceId:', sourceDeviceId); + if (sourceDeviceId && deviceList.length > 0) { + const deviceExists = deviceList.some(d => d.deviceId === sourceDeviceId); + console.log('[CableCreateModal] Device exists in list:', deviceExists); + if (deviceExists) { + console.log('[CableCreateModal] Setting form value:', sourceDeviceId); + form.setFieldsValue({ + sourceDeviceId: sourceDeviceId, + }); + fetchDevicePorts(sourceDeviceId, 'source'); + } + } + }); } - prevVisibleRef.current = visible; - }, [visible, sourceDevice, form, fetchDevices]); + }, [visible, sourceDevice?.deviceId, sourceDevice?.id, form, fetchDevices]); const fetchDevicePorts = async (deviceId, type) => { if (!deviceId) { @@ -138,9 +159,11 @@ const CableCreateModal = ({ visible, onClose, onSuccess, sourceDevice }) => { : '暂无数据'} > {devices.map(device => (