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
+4 -3
View File
@@ -1,5 +1,6 @@
{
"type": "image",
"image": null,
"size": "contain"
"opacity": 0.8,
"blur": 5,
"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) {
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();
}
};
+33 -28
View File
@@ -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 -9
View File
@@ -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;
-8
View File
@@ -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;
-4
View File
@@ -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;
+27 -7
View File
@@ -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;
+5
View File
@@ -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
View File
@@ -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,
+4
View File
@@ -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;
+31
View File
@@ -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设备管理系统后端服务正常运行' });
});
+53 -176
View File
@@ -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
};