refactor(模型关系): 重构模型关联关系定义位置,从模型文件移至路由文件

This commit is contained in:
zhang1106
2026-03-12 11:19:56 +08:00
parent 2e87654c98
commit 668ea073e6
17 changed files with 1478 additions and 553 deletions
+1 -1
View File
@@ -641,7 +641,7 @@ SOFTWARE.
- **Gitee Issues**: https://gitee.com/zhang96110/idc_assest/issues - **Gitee Issues**: https://gitee.com/zhang96110/idc_assest/issues
- **GitHub Issues**: https://github.com/gituib/idc_assest/issues - **GitHub Issues**: https://github.com/gituib/idc_assest/issues
- 功能建议:提交 Issue 并标注 `feature-request` - 功能建议:提交 Issue 并标注 `feature-request`
- QQ群:1081123775
--- ---
**⭐ 如果这个项目对您有帮助,请给我们一个 Star!** **⭐ 如果这个项目对您有帮助,请给我们一个 Star!**
+4 -3
View File
@@ -1,5 +1,6 @@
{ {
"type": "image", "opacity": 0.8,
"image": null, "blur": 5,
"size": "contain" "value": "/images/bg.jpg",
"type": "image"
} }
+63 -4
View File
@@ -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) { if (!user) {
console.warn('[认证中间件] 用户不存在:', {
userId: decoded.userId,
username: decoded.username,
timestamp: new Date().toISOString()
});
return res.status(401).json({ return res.status(401).json({
success: false, success: false,
message: '用户不存在' message: '用户不存在'
@@ -126,6 +147,11 @@ const authMiddleware = async (req, res, next) => {
} }
if (user.status === 'locked') { if (user.status === 'locked') {
console.warn('[认证中间件] 账户已被锁定:', {
userId: user.userId,
username: user.username,
timestamp: new Date().toISOString()
});
return res.status(403).json({ return res.status(403).json({
success: false, success: false,
message: '账户已被锁定' message: '账户已被锁定'
@@ -133,6 +159,11 @@ const authMiddleware = async (req, res, next) => {
} }
if (user.status === 'inactive') { if (user.status === 'inactive') {
console.warn('[认证中间件] 账户已禁用:', {
userId: user.userId,
username: user.username,
timestamp: new Date().toISOString()
});
return res.status(403).json({ return res.status(403).json({
success: false, success: false,
message: '账户已禁用' message: '账户已禁用'
@@ -143,10 +174,18 @@ const authMiddleware = async (req, res, next) => {
req.userModel = user; req.userModel = user;
next(); next();
} catch (error) { } 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({ return res.status(500).json({
success: false, success: false,
message: '认证失败' message: '认证失败,请稍后重试'
}); });
} }
}; };
@@ -160,7 +199,21 @@ const optionalAuth = async (req, res, next) => {
const decoded = verifyToken(token); const decoded = verifyToken(token);
if (decoded) { 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') { if (user && user.status === 'active') {
req.user = decoded; req.user = decoded;
req.userModel = user; req.userModel = user;
@@ -170,6 +223,12 @@ const optionalAuth = async (req, res, next) => {
next(); next();
} catch (error) { } catch (error) {
// 可选认证失败不影响主流程,仅记录日志
console.warn('[可选认证] 认证失败(已忽略):', {
error: error.message,
url: req?.url,
timestamp: new Date().toISOString()
});
next(); next();
} }
}; };
+33 -28
View File
@@ -3,49 +3,54 @@ const validate = (schema, source = 'body') => {
const data = source === 'query' ? req.query : req.body; const data = source === 'query' ? req.query : req.body;
try { try {
let result; let value;
if (schema.validate && typeof schema.validate === 'function') { if (schema.validate && typeof schema.validate === 'function') {
if (schema.validate.constructor.name === 'AsyncFunction' || const result = schema.validate(data, {
schema.validate.length === 1) { abortEarly: false,
result = await schema.validate(data, { stripUnknown: true,
abortEarly: false, allowUnknown: source === 'query'
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 { } else {
result = schema.validate(data, { value = result;
abortEarly: false,
stripUnknown: true,
allowUnknown: source === 'query'
});
} }
} else if (schema.validateAsync) { } else if (schema.validateAsync) {
result = await schema.validateAsync(data, { value = await schema.validateAsync(data, {
abortEarly: false, abortEarly: false,
stripUnknown: true, stripUnknown: true,
allowUnknown: source === 'query' allowUnknown: source === 'query'
}); });
} else { } else {
result = schema.validate(data, { const result = schema.validate(data, {
abortEarly: false, abortEarly: false,
stripUnknown: true, stripUnknown: true,
allowUnknown: source === 'query' allowUnknown: source === 'query'
}); });
} if (result.error) {
const errorMessages = result.error.details.map(detail => ({
const { error, value } = result; field: detail.path.join('.'),
message: detail.message
if (error) { }));
const errorMessages = error.details.map(detail => ({ return res.status(400).json({
field: detail.path.join('.'), error: '参数验证失败',
message: detail.message details: errorMessages
})); });
}
return res.status(400).json({ value = result.value;
error: '参数验证失败',
details: errorMessages
});
} }
if (source === 'query') { if (source === 'query') {
+1 -9
View File
@@ -1,6 +1,5 @@
const { DataTypes } = require('sequelize'); const { DataTypes } = require('sequelize');
const { sequelize } = require('../db'); const { sequelize } = require('../db');
const Rack = require('./Rack');
const Device = sequelize.define('Device', { const Device = sequelize.define('Device', {
deviceId: { deviceId: {
@@ -28,11 +27,7 @@ const Device = sequelize.define('Device', {
}, },
rackId: { rackId: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: true, allowNull: true
references: {
model: Rack,
key: 'rackId'
}
}, },
position: { position: {
type: DataTypes.INTEGER, 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; module.exports = Device;
-8
View File
@@ -1,7 +1,5 @@
const { DataTypes } = require('sequelize'); const { DataTypes } = require('sequelize');
const { sequelize } = require('../db'); const { sequelize } = require('../db');
const Device = require('./Device');
const NetworkCard = require('./NetworkCard');
const DevicePort = sequelize.define('DevicePort', { const DevicePort = sequelize.define('DevicePort', {
portId: { 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; module.exports = DevicePort;
-4
View File
@@ -1,6 +1,5 @@
const { DataTypes } = require('sequelize'); const { DataTypes } = require('sequelize');
const { sequelize } = require('../db'); const { sequelize } = require('../db');
const Device = require('./Device');
const NetworkCard = sequelize.define('NetworkCard', { const NetworkCard = sequelize.define('NetworkCard', {
nicId: { 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; module.exports = NetworkCard;
+27 -7
View File
@@ -3,13 +3,11 @@ const path = require('path');
const fs = require('fs'); const fs = require('fs');
const router = express.Router(); const router = express.Router();
// 确保上传目录存在
const UPLOAD_DIR = path.join(__dirname, '../uploads'); const UPLOAD_DIR = path.join(__dirname, '../uploads');
if (!fs.existsSync(UPLOAD_DIR)) { if (!fs.existsSync(UPLOAD_DIR)) {
fs.mkdirSync(UPLOAD_DIR, { recursive: true }); fs.mkdirSync(UPLOAD_DIR, { recursive: true });
} }
// 确保背景设置文件存在
const SETTINGS_FILE = path.join(__dirname, '../backgroundSettings.json'); const SETTINGS_FILE = path.join(__dirname, '../backgroundSettings.json');
if (!fs.existsSync(SETTINGS_FILE)) { if (!fs.existsSync(SETTINGS_FILE)) {
fs.writeFileSync(SETTINGS_FILE, JSON.stringify({ fs.writeFileSync(SETTINGS_FILE, JSON.stringify({
@@ -19,7 +17,33 @@ if (!fs.existsSync(SETTINGS_FILE)) {
}, null, 2)); }, 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) => { router.post('/upload', (req, res) => {
try { try {
if (!req.files || !req.files.file) { if (!req.files || !req.files.file) {
@@ -30,14 +54,12 @@ router.post('/upload', (req, res) => {
const fileName = `${Date.now()}_${file.name}`; const fileName = `${Date.now()}_${file.name}`;
const filePath = path.join(UPLOAD_DIR, fileName); const filePath = path.join(UPLOAD_DIR, fileName);
// 保存文件到服务器
file.mv(filePath, (err) => { file.mv(filePath, (err) => {
if (err) { if (err) {
console.error('文件保存失败:', err); console.error('文件保存失败:', err);
return res.status(500).json({ error: '文件保存失败' }); return res.status(500).json({ error: '文件保存失败' });
} }
// 返回文件路径
const fileUrl = `/uploads/${fileName}`; const fileUrl = `/uploads/${fileName}`;
res.json({ path: fileUrl }); res.json({ path: fileUrl });
}); });
@@ -47,7 +69,6 @@ router.post('/upload', (req, res) => {
} }
}); });
// 获取背景设置
router.get('/settings', (req, res) => { router.get('/settings', (req, res) => {
try { try {
const settings = JSON.parse(fs.readFileSync(SETTINGS_FILE, 'utf8')); const settings = JSON.parse(fs.readFileSync(SETTINGS_FILE, 'utf8'));
@@ -58,7 +79,6 @@ router.get('/settings', (req, res) => {
} }
}); });
// 保存背景设置
router.post('/settings', (req, res) => { router.post('/settings', (req, res) => {
try { try {
const settings = req.body; const settings = req.body;
+5
View File
@@ -3,6 +3,11 @@ const router = express.Router();
const { Op } = require('sequelize'); const { Op } = require('sequelize');
const DevicePort = require('../models/DevicePort'); const DevicePort = require('../models/DevicePort');
const Device = require('../models/Device'); 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) => { router.get('/', async (req, res) => {
try { try {
+33 -23
View File
@@ -1,7 +1,7 @@
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const { Op } = require('sequelize'); 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 fs = require('fs');
const path = require('path'); const path = require('path');
const csv = require('csv-parser'); const csv = require('csv-parser');
@@ -12,9 +12,9 @@ const Rack = require('../models/Rack');
const Room = require('../models/Room'); const Room = require('../models/Room');
const DeviceField = require('../models/DeviceField'); const DeviceField = require('../models/DeviceField');
const Ticket = require('../models/Ticket'); const Ticket = require('../models/Ticket');
const DevicePort = require('../models/DevicePort'); // Import DevicePort const DevicePort = require('../models/DevicePort');
const Cable = require('../models/Cable'); // Import Cable const Cable = require('../models/Cable');
const NetworkCard = require('../models/NetworkCard'); // Import NetworkCard const NetworkCard = require('../models/NetworkCard');
const { validateBody, validateQuery } = require('../middleware/validation'); const { validateBody, validateQuery } = require('../middleware/validation');
const { const {
createDeviceSchema, createDeviceSchema,
@@ -25,7 +25,9 @@ const {
queryDeviceSchema queryDeviceSchema
} = require('../validation/deviceSchema'); } = require('../validation/deviceSchema');
// 获取所有设备(支持搜索和筛选) Device.belongsTo(Rack, { foreignKey: 'rackId' });
Rack.hasMany(Device, { foreignKey: 'rackId' });
router.get('/', validateQuery(queryDeviceSchema), async (req, res) => { router.get('/', validateQuery(queryDeviceSchema), async (req, res) => {
try { try {
const { keyword, status, type, rackId, page = 1, pageSize = 10 } = req.query; const { keyword, status, type, rackId, page = 1, pageSize = 10 } = req.query;
@@ -772,6 +774,16 @@ router.post('/import', async (req, res) => {
} catch (error) { } catch (error) {
await t.rollback(); await t.rollback();
console.error('导入设备数据失败:', error); console.error('导入设备数据失败:', error);
// 清理临时文件
try {
if (filePath && fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
}
} catch (fileErr) {
console.error('删除临时文件失败:', fileErr);
}
res.status(500).json({ res.status(500).json({
errors: [{ row: 0, error: error.message || '导入过程中发生未知错误' }] errors: [{ row: 0, error: error.message || '导入过程中发生未知错误' }]
}); });
@@ -1224,7 +1236,22 @@ router.delete('/:deviceId', async (req, res) => {
{ where: { deviceId: deviceId }, transaction: t } { 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({ await Device.destroy({
where: { deviceId: deviceId }, where: { deviceId: deviceId },
transaction: t transaction: t
@@ -1237,23 +1264,6 @@ router.delete('/:deviceId', async (req, res) => {
console.log(`已删除 ${deletedCables} 条相关接线`); 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({ res.status(200).json({
message: '删除成功', message: '删除成功',
deviceId: deviceId, deviceId: deviceId,
+4
View File
@@ -5,6 +5,10 @@ const NetworkCard = require('../models/NetworkCard');
const Device = require('../models/Device'); const Device = require('../models/Device');
const DevicePort = require('../models/DevicePort'); 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) => { router.get('/', async (req, res) => {
try { try {
const { deviceId } = req.query; const { deviceId } = req.query;
+31
View File
@@ -180,6 +180,37 @@ app.use('/api/inventory', inventoryRoutes);
app.use('/uploads', express.static('uploads')); 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) => { app.get('/health', (req, res) => {
res.json({ status: 'ok', message: 'IDC设备管理系统后端服务正常运行' }); res.json({ status: 'ok', message: 'IDC设备管理系统后端服务正常运行' });
}); });
+53 -176
View File
@@ -1,168 +1,61 @@
const Joi = require('joi'); const Joi = require('joi');
const DeviceField = require('../models/DeviceField');
const DEVICE_TYPES = ['server', 'switch', 'router', 'storage', 'other']; const DEVICE_TYPES = ['server', 'switch', 'router', 'storage', 'other'];
const DEVICE_STATUS = ['running', 'maintenance', 'offline', 'fault']; const DEVICE_STATUS = ['running', 'maintenance', 'offline', 'fault'];
const baseFieldSchemas = { const createDeviceSchema = Joi.object({
deviceId: Joi.string() name: Joi.string().required().max(100).messages({
.max(50) 'string.empty': '设备名称不能为空',
.pattern(/^[a-zA-Z0-9_-]+$/) 'string.max': '设备名称不能超过100个字符',
.allow('', null) 'any.required': '设备名称是必填字段'
.messages({ }),
'string.max': '设备ID不能超过50个字符', type: Joi.string().required().valid(...DEVICE_TYPES).messages({
'string.pattern.base': '设备ID只能包含字母、数字、下划线和横线' 'any.only': `设备类型必须是以下之一: ${DEVICE_TYPES.join(', ')}`,
}), 'any.required': '设备类型是必填字段'
}),
name: Joi.string() model: Joi.string().allow('', null).max(100),
.max(100) serialNumber: Joi.string().required().max(100).messages({
.messages({ 'string.empty': '序列号不能为空',
'string.empty': '设备名称不能为空', 'string.max': '序列号不能超过100个字符',
'string.max': '设备名称不能超过100个字符' 'any.required': '序列号是必填字段'
}), }),
rackId: Joi.string().allow('', null).max(50),
type: Joi.string() position: Joi.number().integer().min(1).max(100).allow(null),
.valid(...DEVICE_TYPES) height: Joi.number().integer().min(1).max(50).allow(null),
.messages({ powerConsumption: Joi.number().min(0).max(100000).allow(null),
'string.empty': '设备类型不能为空', ipAddress: Joi.string().allow('', null).max(50),
'any.only': `设备类型必须是以下之一: ${DEVICE_TYPES.join(', ')}` status: Joi.string().valid(...DEVICE_STATUS).default('offline'),
}), purchaseDate: Joi.date().allow(null),
warrantyExpiry: Joi.date().allow(null),
model: Joi.string() description: Joi.string().allow('', null).max(500),
.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个字符'
}),
customFields: Joi.object().allow(null) customFields: Joi.object().allow(null)
}; });
async function buildDynamicSchema(isCreate = true) { const updateDeviceSchema = Joi.object({
const fields = await DeviceField.findAll({ name: Joi.string().max(100).messages({
where: { isSystem: true }, 'string.empty': '设备名称不能为空',
order: [['order', 'ASC']] 'string.max': '设备名称不能超过100个字符'
}); }),
type: Joi.string().valid(...DEVICE_TYPES).messages({
const schemaObj = {}; 'any.only': `设备类型必须是以下之一: ${DEVICE_TYPES.join(', ')}`
}),
fields.forEach(field => { model: Joi.string().allow('', null).max(100),
const baseSchema = baseFieldSchemas[field.fieldName]; serialNumber: Joi.string().max(100).messages({
if (baseSchema) { 'string.max': '序列号不能超过100个字符'
let fieldSchema = baseSchema.clone(); }),
rackId: Joi.string().allow('', null).max(50),
if (field.required && isCreate) { position: Joi.number().integer().min(1).max(100).allow(null),
fieldSchema = fieldSchema.required(); 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),
schemaObj[field.fieldName] = fieldSchema; status: Joi.string().valid(...DEVICE_STATUS),
} purchaseDate: Joi.date().allow(null),
}); warrantyExpiry: Joi.date().allow(null),
description: Joi.string().allow('', null).max(500),
schemaObj.customFields = baseFieldSchemas.customFields; customFields: Joi.object().allow(null)
}).min(1).messages({
return Joi.object(schemaObj).custom((value, helpers) => { 'object.min': '至少需要提供一个字段进行更新'
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 batchDeviceIdsSchema = Joi.object({ const batchDeviceIdsSchema = Joi.object({
deviceIds: Joi.array() deviceIds: Joi.array()
@@ -233,24 +126,10 @@ const queryDeviceSchema = Joi.object({
pageSize: Joi.number() pageSize: Joi.number()
.integer() .integer()
.min(1) .min(1)
.max(100) .max(10000)
.default(10) .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 = { module.exports = {
createDeviceSchema, createDeviceSchema,
updateDeviceSchema, updateDeviceSchema,
@@ -259,7 +138,5 @@ module.exports = {
batchMoveSchema, batchMoveSchema,
queryDeviceSchema, queryDeviceSchema,
DEVICE_TYPES, DEVICE_TYPES,
DEVICE_STATUS, DEVICE_STATUS
getCreateDeviceSchema,
getUpdateDeviceSchema
}; };
+1146 -267
View File
File diff suppressed because it is too large Load Diff
+41 -16
View File
@@ -13,20 +13,27 @@ const CableCreateModal = ({ visible, onClose, onSuccess, sourceDevice }) => {
const [sourcePorts, setSourcePorts] = useState([]); const [sourcePorts, setSourcePorts] = useState([]);
const [targetPorts, setTargetPorts] = useState([]); const [targetPorts, setTargetPorts] = useState([]);
const [fetchingDevices, setFetchingDevices] = useState(false); const [fetchingDevices, setFetchingDevices] = useState(false);
const prevVisibleRef = useRef(false); const devicesRef = useRef([]);
const fetchDevices = useCallback(async (keyword = '') => { const fetchDevices = useCallback(async (keyword = '') => {
try { try {
setFetchingDevices(true); setFetchingDevices(true);
const params = { pageSize: 50 }; const params = { pageSize: 100 };
if (keyword && keyword.trim()) { if (keyword && keyword.trim()) {
params.keyword = keyword.trim(); params.keyword = keyword.trim();
} }
console.log('[CableCreateModal] Fetching devices with params:', params);
const response = await axios.get('/api/devices', { 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) { } catch (error) {
console.error('Failed to fetch devices:', error); console.error('[CableCreateModal] Failed to fetch devices:', error);
message.error('获取设备列表失败'); message.error('获取设备列表失败');
return [];
} finally { } finally {
setFetchingDevices(false); setFetchingDevices(false);
} }
@@ -40,18 +47,32 @@ const CableCreateModal = ({ visible, onClose, onSuccess, sourceDevice }) => {
); );
useEffect(() => { useEffect(() => {
if (visible && !prevVisibleRef.current) { if (visible) {
console.log('[CableCreateModal] Modal opened, sourceDevice:', sourceDevice);
form.resetFields(); form.resetFields();
if (sourceDevice) { setSourcePorts([]);
form.setFieldsValue({ setTargetPorts([]);
sourceDeviceId: sourceDevice.deviceId || sourceDevice.id, setDevices([]);
}); devicesRef.current = [];
fetchDevicePorts(sourceDevice.deviceId || sourceDevice.id, 'source');
} fetchDevices().then(deviceList => {
fetchDevices(); 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?.deviceId, sourceDevice?.id, form, fetchDevices]);
}, [visible, sourceDevice, form, fetchDevices]);
const fetchDevicePorts = async (deviceId, type) => { const fetchDevicePorts = async (deviceId, type) => {
if (!deviceId) { if (!deviceId) {
@@ -138,9 +159,11 @@ const CableCreateModal = ({ visible, onClose, onSuccess, sourceDevice }) => {
<Select <Select
showSearch showSearch
filterOption={false} filterOption={false}
placeholder="搜索设备..." placeholder={fetchingDevices ? '加载中...' : '搜索设备...'}
loading={fetchingDevices}
onSearch={handleDeviceSearch} onSearch={handleDeviceSearch}
onChange={handleSourceDeviceChange} onChange={handleSourceDeviceChange}
notFoundContent={fetchingDevices ? <Spin size="small" /> : '暂无数据'}
> >
{devices.map(device => ( {devices.map(device => (
<Option key={device.deviceId} value={device.deviceId}> <Option key={device.deviceId} value={device.deviceId}>
@@ -167,9 +190,11 @@ const CableCreateModal = ({ visible, onClose, onSuccess, sourceDevice }) => {
<Select <Select
showSearch showSearch
filterOption={false} filterOption={false}
placeholder="搜索设备..." placeholder={fetchingDevices ? '加载中...' : '搜索设备...'}
loading={fetchingDevices}
onSearch={handleDeviceSearch} onSearch={handleDeviceSearch}
onChange={handleTargetDeviceChange} onChange={handleTargetDeviceChange}
notFoundContent={fetchingDevices ? <Spin size="small" /> : '暂无数据'}
> >
{devices.map(device => ( {devices.map(device => (
<Option key={device.deviceId} value={device.deviceId}> <Option key={device.deviceId} value={device.deviceId}>
+11
View File
@@ -110,6 +110,17 @@ export const designTokens = {
date: '#06b6d4', date: '#06b6d4',
textarea: '#64748b', textarea: '#64748b',
}, },
slot: {
empty: '#4b5563',
occupied: '#3b82f6',
warning: '#f59e0b',
error: '#ef4444',
},
metal: {
light: '#9ca3af',
medium: '#6b7280',
dark: '#374151',
},
}, },
shadows: { shadows: {
small: '0 1px 2px 0 rgba(0, 0, 0, 0.05)', small: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
+25 -7
View File
@@ -206,18 +206,36 @@ function CableManagement() {
const fetchDevices = useCallback(async (keyword = '') => { const fetchDevices = useCallback(async (keyword = '') => {
try { try {
setDeviceSearching(true); setDeviceSearching(true);
const params = { pageSize: 50 };
// 并行获取所有设备和交换机设备
const params = { pageSize: 1000 };
if (keyword && keyword.trim()) { if (keyword && keyword.trim()) {
params.keyword = keyword.trim(); params.keyword = keyword.trim();
} }
const response = await axios.get('/api/devices', { params });
const allDevices = response.data.devices || []; const [allResponse, switchResponse] = await Promise.all([
const switches = allDevices.filter(device => device.type === 'switch'); 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); setDevices(allDevices);
setSwitchDevices(switches); setSwitchDevices(switchDevices);
} catch (error) { } catch (error) {
message.error('获取设备列表失败'); message.error('获取设备列表失败');
console.error('获取设备列表失败:', error); console.error('[CableManagement] 获取设备列表失败:', error);
} finally { } finally {
setDeviceSearching(false); setDeviceSearching(false);
} }
@@ -1244,7 +1262,7 @@ function CableManagement() {
rules={[{ required: true, message: '请选择源设备' }]} rules={[{ required: true, message: '请选择源设备' }]}
> >
<Select <Select
placeholder="输入关键词搜索源设备" placeholder="输入关键词搜索交换机"
showSearch showSearch
loading={deviceSearching} loading={deviceSearching}
filterOption={false} filterOption={false}