refactor(模型关系): 重构模型关联关系定义位置,从模型文件移至路由文件
This commit is contained in:
@@ -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!**
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"type": "image",
|
||||
"image": null,
|
||||
"size": "contain"
|
||||
"opacity": 0.8,
|
||||
"blur": 5,
|
||||
"value": "/images/bg.jpg",
|
||||
"type": "image"
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+33
-23
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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设备管理系统后端服务正常运行' });
|
||||
});
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
+1146
-267
File diff suppressed because it is too large
Load Diff
@@ -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 }) => {
|
||||
<Select
|
||||
showSearch
|
||||
filterOption={false}
|
||||
placeholder="搜索设备..."
|
||||
placeholder={fetchingDevices ? '加载中...' : '搜索设备...'}
|
||||
loading={fetchingDevices}
|
||||
onSearch={handleDeviceSearch}
|
||||
onChange={handleSourceDeviceChange}
|
||||
notFoundContent={fetchingDevices ? <Spin size="small" /> : '暂无数据'}
|
||||
>
|
||||
{devices.map(device => (
|
||||
<Option key={device.deviceId} value={device.deviceId}>
|
||||
@@ -167,9 +190,11 @@ const CableCreateModal = ({ visible, onClose, onSuccess, sourceDevice }) => {
|
||||
<Select
|
||||
showSearch
|
||||
filterOption={false}
|
||||
placeholder="搜索设备..."
|
||||
placeholder={fetchingDevices ? '加载中...' : '搜索设备...'}
|
||||
loading={fetchingDevices}
|
||||
onSearch={handleDeviceSearch}
|
||||
onChange={handleTargetDeviceChange}
|
||||
notFoundContent={fetchingDevices ? <Spin size="small" /> : '暂无数据'}
|
||||
>
|
||||
{devices.map(device => (
|
||||
<Option key={device.deviceId} value={device.deviceId}>
|
||||
|
||||
@@ -110,6 +110,17 @@ export const designTokens = {
|
||||
date: '#06b6d4',
|
||||
textarea: '#64748b',
|
||||
},
|
||||
slot: {
|
||||
empty: '#4b5563',
|
||||
occupied: '#3b82f6',
|
||||
warning: '#f59e0b',
|
||||
error: '#ef4444',
|
||||
},
|
||||
metal: {
|
||||
light: '#9ca3af',
|
||||
medium: '#6b7280',
|
||||
dark: '#374151',
|
||||
},
|
||||
},
|
||||
shadows: {
|
||||
small: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
|
||||
|
||||
@@ -206,18 +206,36 @@ function CableManagement() {
|
||||
const fetchDevices = useCallback(async (keyword = '') => {
|
||||
try {
|
||||
setDeviceSearching(true);
|
||||
const params = { pageSize: 50 };
|
||||
|
||||
// 并行获取所有设备和交换机设备
|
||||
const params = { pageSize: 1000 };
|
||||
if (keyword && keyword.trim()) {
|
||||
params.keyword = keyword.trim();
|
||||
}
|
||||
const response = await axios.get('/api/devices', { params });
|
||||
const allDevices = response.data.devices || [];
|
||||
const switches = allDevices.filter(device => device.type === 'switch');
|
||||
|
||||
const [allResponse, switchResponse] = await Promise.all([
|
||||
axios.get('/api/devices', { params }),
|
||||
axios.get('/api/devices', { params: { ...params, type: 'switch' } })
|
||||
]);
|
||||
|
||||
const allDevices = allResponse.data.devices || [];
|
||||
const switchDevices = switchResponse.data.devices || [];
|
||||
|
||||
// 统计各类型设备数量
|
||||
const typeCount = {};
|
||||
allDevices.forEach(d => {
|
||||
const t = d.type || 'undefined';
|
||||
typeCount[t] = (typeCount[t] || 0) + 1;
|
||||
});
|
||||
console.log('[CableManagement] 设备类型统计:', typeCount);
|
||||
console.log('[CableManagement] All devices:', allDevices.length);
|
||||
console.log('[CableManagement] Switch devices (from API):', switchDevices.length);
|
||||
|
||||
setDevices(allDevices);
|
||||
setSwitchDevices(switches);
|
||||
setSwitchDevices(switchDevices);
|
||||
} catch (error) {
|
||||
message.error('获取设备列表失败');
|
||||
console.error('获取设备列表失败:', error);
|
||||
console.error('[CableManagement] 获取设备列表失败:', error);
|
||||
} finally {
|
||||
setDeviceSearching(false);
|
||||
}
|
||||
@@ -1244,7 +1262,7 @@ function CableManagement() {
|
||||
rules={[{ required: true, message: '请选择源设备' }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="输入关键词搜索源设备"
|
||||
placeholder="输入关键词搜索交换机"
|
||||
showSearch
|
||||
loading={deviceSearching}
|
||||
filterOption={false}
|
||||
|
||||
Reference in New Issue
Block a user