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 => (