feat: 修改了bug

This commit is contained in:
zhang1106
2026-03-06 13:57:37 +08:00
parent 0bb51ec6ac
commit 0cd0107008
12 changed files with 710 additions and 331 deletions
+1 -1
View File
@@ -87,7 +87,7 @@ const defaultDeviceFields = [
fieldName: 'powerConsumption',
displayName: '功率(W)',
fieldType: 'number',
required: false,
required: true,
order: 9,
visible: true,
isSystem: true
+69 -30
View File
@@ -1,45 +1,84 @@
/**
* 请求参数验证中间件
* 使用Joi进行参数校验
*/
const validate = (schema, source = 'body') => {
return (req, res, next) => {
return async (req, res, next) => {
const data = source === 'query' ? req.query : req.body;
const { error, value } = schema.validate(data, {
abortEarly: false, // 返回所有错误
stripUnknown: true, // 移除未定义的字段
allowUnknown: source === 'query' // 查询参数允许未知字段
});
try {
let result;
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'
});
} else {
result = schema.validate(data, {
abortEarly: false,
stripUnknown: true,
allowUnknown: source === 'query'
});
}
} else if (schema.validateAsync) {
result = await schema.validateAsync(data, {
abortEarly: false,
stripUnknown: true,
allowUnknown: source === 'query'
});
} else {
result = schema.validate(data, {
abortEarly: false,
stripUnknown: true,
allowUnknown: source === 'query'
});
}
if (error) {
const errorMessages = error.details.map(detail => ({
field: detail.path.join('.'),
message: detail.message
}));
const { error, value } = result;
return res.status(400).json({
error: '参数验证失败',
details: errorMessages
if (error) {
const errorMessages = error.details.map(detail => ({
field: detail.path.join('.'),
message: detail.message
}));
return res.status(400).json({
error: '参数验证失败',
details: errorMessages
});
}
if (source === 'query') {
req.query = value;
} else {
req.body = value;
}
next();
} catch (error) {
if (error.details) {
const errorMessages = error.details.map(detail => ({
field: detail.path.join('.'),
message: detail.message
}));
return res.status(400).json({
error: '参数验证失败',
details: errorMessages
});
}
console.error('验证中间件错误:', error);
return res.status(500).json({
error: '验证过程发生错误',
message: error.message
});
}
// 将验证后的值替换到请求对象
if (source === 'query') {
req.query = value;
} else {
req.body = value;
}
next();
};
};
// 验证查询参数
const validateQuery = (schema) => validate(schema, 'query');
// 验证请求体
const validateBody = (schema) => validate(schema, 'body');
module.exports = {
+17 -15
View File
@@ -11,24 +11,24 @@ const Device = sequelize.define('Device', {
},
name: {
type: DataTypes.STRING,
allowNull: false
allowNull: true
},
type: {
type: DataTypes.STRING,
allowNull: false
allowNull: true
},
model: {
type: DataTypes.STRING,
allowNull: false
allowNull: true
},
serialNumber: {
type: DataTypes.STRING,
allowNull: false,
allowNull: true,
unique: true
},
rackId: {
type: DataTypes.STRING,
allowNull: false,
allowNull: true,
references: {
model: Rack,
key: 'rackId'
@@ -36,20 +36,21 @@ const Device = sequelize.define('Device', {
},
position: {
type: DataTypes.INTEGER,
allowNull: false // 设备在机柜中的位置(U数)
allowNull: true
},
height: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 1 // 设备高度(U数)
allowNull: true,
defaultValue: 1
},
powerConsumption: {
type: DataTypes.FLOAT,
allowNull: false
allowNull: true,
defaultValue: 0
},
status: {
type: DataTypes.STRING,
defaultValue: 'running'
defaultValue: 'offline'
},
purchaseDate: {
type: DataTypes.DATE,
@@ -60,15 +61,17 @@ const Device = sequelize.define('Device', {
allowNull: true
},
ipAddress: {
type: DataTypes.STRING
type: DataTypes.STRING,
allowNull: true
},
description: {
type: DataTypes.TEXT
type: DataTypes.TEXT,
allowNull: true
},
customFields: {
type: DataTypes.JSON,
defaultValue: {},
allowNull: false
allowNull: true
}
}, {
tableName: 'devices',
@@ -83,8 +86,7 @@ const Device = sequelize.define('Device', {
]
});
// 关联关系
Device.belongsTo(Rack, { foreignKey: 'rackId' });
Rack.hasMany(Device, { foreignKey: 'rackId' });
module.exports = Device;
module.exports = Device;
+9 -13
View File
@@ -88,33 +88,29 @@ router.post('/config', async (req, res) => {
try {
const fieldConfigs = req.body;
// 验证输入
if (!Array.isArray(fieldConfigs)) {
return res.status(400).json({ error: '输入必须是数组' });
}
// 批量更新字段配置
const updatedFields = [];
for (const config of fieldConfigs) {
// 检查字段是否存在
const existingField = await DeviceField.findOne({ where: { fieldName: config.fieldName } });
if (existingField) {
// 更新现有字段
await existingField.update({
visible: config.visible,
displayName: config.displayName // 同时更新显示名称,以防变化
visible: config.visible !== undefined ? config.visible : existingField.visible,
required: config.required !== undefined ? config.required : existingField.required,
displayName: config.displayName || existingField.displayName,
});
updatedFields.push(existingField);
} else {
// 创建新字段
const newField = await DeviceField.create({
fieldName: config.fieldName,
visible: config.visible,
displayName: config.displayName,
fieldType: config.fieldType || 'text',
required: false, // 默认为非必填
order: 0 // 默认顺序
fieldName: config.fieldName,
visible: config.visible !== undefined ? config.visible : true,
required: config.required !== undefined ? config.required : false,
displayName: config.displayName || config.fieldName,
fieldType: config.fieldType || 'text',
order: config.order || 0,
});
updatedFields.push(newField);
}
+6 -6
View File
@@ -14,8 +14,8 @@ const initDefaultSettings = async () => {
{ settingKey: 'timezone', settingValue: JSON.stringify('Asia/Shanghai'), settingType: 'string', category: 'general', description: '时区设置', isEditable: true },
{ settingKey: 'date_format', settingValue: JSON.stringify('YYYY-MM-DD'), settingType: 'string', category: 'general', description: '日期格式', isEditable: true },
{ settingKey: 'session_timeout', settingValue: JSON.stringify(30), settingType: 'number', category: 'general', description: '登录有效期(分钟)', isEditable: true },
{ settingKey: 'idle_timeout', settingValue: JSON.stringify(10), settingType: 'number', category: 'general', description: '用户空闲超时时间(分钟)', isEditable: true },
{ settingKey: 'idle_warning_time', settingValue: JSON.stringify(10), settingType: 'number', category: 'general', description: '空闲超时前警告时间(秒)', isEditable: false },
{ settingKey: 'idle_timeout', settingValue: JSON.stringify(30), settingType: 'number', category: 'general', description: '用户空闲超时时间(分钟)', isEditable: true },
{ settingKey: 'idle_warning_time', settingValue: JSON.stringify(60), settingType: 'number', category: 'general', description: '空闲超时前警告时间(秒)', isEditable: false },
{ settingKey: 'max_login_attempts', settingValue: JSON.stringify(5), settingType: 'number', category: 'general', description: '最大登录尝试次数', isEditable: true },
{ settingKey: 'maintenance_mode', settingValue: JSON.stringify(false), settingType: 'boolean', category: 'general', description: '维护模式', isEditable: true },
@@ -106,8 +106,8 @@ router.get('/idle-timeout', async (req, res) => {
const timeoutSetting = await SystemSetting.findByPk('idle_timeout');
// 默认配置
const defaultTimeout = 10; // 10分钟
const fixedWarningTime = 10; // 固定10秒警告时间
const defaultTimeout = 30; // 30分钟
const fixedWarningTime = 60; // 固定60秒警告时间
const timeout = timeoutSetting ? JSON.parse(timeoutSetting.settingValue) : defaultTimeout;
@@ -263,8 +263,8 @@ router.post('/reset/:key', async (req, res) => {
timezone: 'Asia/Shanghai',
date_format: 'YYYY-MM-DD',
session_timeout: 30,
idle_timeout: 10,
idle_warning_time: 10,
idle_timeout: 30,
idle_warning_time: 60,
max_login_attempts: 5,
maintenance_mode: false,
frontend_port: 3000,
+158
View File
@@ -64,6 +64,21 @@ const migrations = [
name: '耗材SN序列号字段',
description: '为 consumables、consumable_records、consumable_logs 添加 snList 字段',
migrate: migrateSnList
},
{
name: '设备型号字段可空',
description: '将 devices 表 model 字段改为可空,支持非必填',
migrate: migrateDeviceModelField
},
{
name: '设备字段配置同步',
description: '同步前后端字段必填配置',
migrate: migrateDeviceFieldsConfig
},
{
name: '设备表字段可空',
description: '将设备表所有字段改为可空,由应用层验证控制',
migrate: migrateDeviceFieldsNullable
}
];
@@ -392,6 +407,149 @@ async function migrateSnList() {
}
}
async function migrateDeviceModelField() {
if (!(await tableExists('devices'))) {
console.log(' devices 表不存在,跳过');
return;
}
const dialect = sequelize.getDialect();
if (dialect === 'mysql') {
await sequelize.query(
'ALTER TABLE devices MODIFY COLUMN model VARCHAR(255) NULL'
);
console.log(' devices 表 model 字段已改为可空');
} else if (dialect === 'sqlite') {
const columns = await getTableColumns('devices');
if (columns.includes('model_old')) {
console.log(' model_old 字段已存在,跳过迁移');
return;
}
await sequelize.query('ALTER TABLE devices RENAME COLUMN model TO model_old');
await sequelize.query('ALTER TABLE devices ADD COLUMN model VARCHAR(255)');
await sequelize.query('UPDATE devices SET model = model_old');
await sequelize.query('ALTER TABLE devices DROP COLUMN model_old');
console.log(' devices 表 model 字段已改为可空');
}
}
async function migrateDeviceFieldsConfig() {
const DeviceField = require('../models/DeviceField');
const updates = [
{ fieldName: 'model', required: false },
{ fieldName: 'powerConsumption', required: true },
{ fieldName: 'purchaseDate', required: false },
{ fieldName: 'warrantyExpiry', required: false },
];
for (const update of updates) {
const field = await DeviceField.findOne({ where: { fieldName: update.fieldName } });
if (field && field.required !== update.required) {
await field.update(update);
console.log(` 更新字段 ${update.fieldName}: required=${update.required}`);
} else if (!field) {
console.log(` 字段 ${update.fieldName} 不存在,跳过`);
} else {
console.log(` 字段 ${update.fieldName} 配置已正确,跳过`);
}
}
}
async function migrateDeviceFieldsNullable() {
const dialect = sequelize.getDialect();
if (dialect === 'mysql') {
const alterCommands = [
"ALTER TABLE devices MODIFY COLUMN name VARCHAR(255) NULL",
"ALTER TABLE devices MODIFY COLUMN type VARCHAR(255) NULL",
"ALTER TABLE devices MODIFY COLUMN model VARCHAR(255) NULL",
"ALTER TABLE devices MODIFY COLUMN serialNumber VARCHAR(255) NULL",
"ALTER TABLE devices MODIFY COLUMN rackId VARCHAR(255) NULL",
"ALTER TABLE devices MODIFY COLUMN position INTEGER NULL",
"ALTER TABLE devices MODIFY COLUMN height INTEGER NULL",
"ALTER TABLE devices MODIFY COLUMN powerConsumption FLOAT NULL",
"ALTER TABLE devices MODIFY COLUMN customFields JSON NULL"
];
for (const sql of alterCommands) {
try {
await sequelize.query(sql);
} catch (e) {
if (!e.message.includes('Unknown column')) {
console.log(` 警告: ${e.message}`);
}
}
}
console.log(' devices 表字段已改为可空');
} else if (dialect === 'sqlite') {
const columns = await getTableColumns('devices');
const hasNullableFlag = columns.includes('_nullable_migration_done');
if (hasNullableFlag) {
console.log(' 已完成可空迁移,跳过');
return;
}
await sequelize.query('PRAGMA foreign_keys = OFF');
try {
await sequelize.query('DROP TABLE IF EXISTS devices_new');
await sequelize.query(`
CREATE TABLE devices_new (
deviceId VARCHAR(255) PRIMARY KEY NOT NULL UNIQUE,
name VARCHAR(255),
type VARCHAR(255),
model VARCHAR(255),
serialNumber VARCHAR(255) UNIQUE,
rackId VARCHAR(255),
position INTEGER,
height INTEGER DEFAULT 1,
powerConsumption FLOAT DEFAULT 0,
status VARCHAR(255) DEFAULT 'offline',
purchaseDate DATETIME,
warrantyExpiry DATETIME,
ipAddress VARCHAR(255),
description TEXT,
customFields JSON DEFAULT '{}',
createdAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
_nullable_migration_done INTEGER DEFAULT 1
)
`);
await sequelize.query(`
INSERT INTO devices_new (
deviceId, name, type, model, serialNumber, rackId, position, height,
powerConsumption, status, purchaseDate, warrantyExpiry, ipAddress,
description, customFields, createdAt, updatedAt
)
SELECT
deviceId, name, type, model, serialNumber, rackId, position, height,
powerConsumption, status, purchaseDate, warrantyExpiry, ipAddress,
description, customFields, createdAt, updatedAt
FROM devices
`);
await sequelize.query('DROP TABLE devices');
await sequelize.query('ALTER TABLE devices_new RENAME TO devices');
await sequelize.query('CREATE INDEX IF NOT EXISTS idx_devices_status ON devices(status)');
await sequelize.query('CREATE INDEX IF NOT EXISTS idx_devices_type ON devices(type)');
await sequelize.query('CREATE INDEX IF NOT EXISTS idx_devices_rackId ON devices(rackId)');
await sequelize.query('CREATE INDEX IF NOT EXISTS idx_devices_name ON devices(name)');
console.log(' devices 表字段已改为可空');
} finally {
await sequelize.query('PRAGMA foreign_keys = ON');
}
}
}
// 执行迁移
runMigrations().catch(error => {
console.error('迁移执行失败:', error);
+86 -146
View File
@@ -1,13 +1,10 @@
const Joi = require('joi');
const DeviceField = require('../models/DeviceField');
// 设备类型枚举
const DEVICE_TYPES = ['server', 'switch', 'router', 'storage', 'other'];
// 设备状态枚举
const DEVICE_STATUS = ['running', 'maintenance', 'offline', 'fault'];
// 创建设备验证Schema
const createDeviceSchema = Joi.object({
const baseFieldSchemas = {
deviceId: Joi.string()
.max(50)
.pattern(/^[a-zA-Z0-9_-]+$/)
@@ -18,21 +15,17 @@ const createDeviceSchema = Joi.object({
}),
name: Joi.string()
.required()
.max(100)
.messages({
'string.empty': '设备名称不能为空',
'string.max': '设备名称不能超过100个字符',
'any.required': '设备名称是必填字段'
'string.max': '设备名称不能超过100个字符'
}),
type: Joi.string()
.valid(...DEVICE_TYPES)
.required()
.messages({
'string.empty': '设备类型不能为空',
'any.only': `设备类型必须是以下之一: ${DEVICE_TYPES.join(', ')}`,
'any.required': '设备类型是必填字段'
'any.only': `设备类型必须是以下之一: ${DEVICE_TYPES.join(', ')}`
}),
model: Joi.string()
@@ -44,138 +37,18 @@ const createDeviceSchema = Joi.object({
serialNumber: Joi.string()
.max(100)
.allow('', null)
.messages({
'string.empty': '序列号不能为空',
'string.max': '序列号不能超过100个字符'
}),
rackId: Joi.string()
.required()
.max(50)
.messages({
'string.empty': '机柜ID不能为空',
'string.max': '机柜ID不能超过50个字符',
'any.required': '机柜ID是必填字段'
'string.max': '机柜ID不能超过50个字符'
}),
position: Joi.number()
.integer()
.min(1)
.max(100)
.required()
.messages({
'number.base': '位置必须是数字',
'number.integer': '位置必须是整数',
'number.min': '位置不能小于1',
'number.max': '位置不能大于100',
'any.required': '位置是必填字段'
}),
height: Joi.number()
.integer()
.min(1)
.max(50)
.required()
.messages({
'number.base': '高度必须是数字',
'number.integer': '高度必须是整数',
'number.min': '高度不能小于1',
'number.max': '高度不能大于50',
'any.required': '高度是必填字段'
}),
powerConsumption: Joi.number()
.min(0)
.max(100000)
.default(0)
.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)
}).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': '保修到期日期必须晚于购买日期'
});
// 更新设备验证Schema(所有字段可选)
const updateDeviceSchema = Joi.object({
deviceId: Joi.string()
.max(50)
.pattern(/^[a-zA-Z0-9_-]+$/)
.messages({
'string.max': '设备ID不能超过50个字符',
'string.pattern.base': '设备ID只能包含字母、数字、下划线和横线'
}),
name: Joi.string()
.max(100)
.messages({
'string.max': '设备名称不能超过100个字符'
}),
type: Joi.string()
.valid(...DEVICE_TYPES)
.messages({
'any.only': `设备类型必须是以下之一: ${DEVICE_TYPES.join(', ')}`
}),
model: Joi.string()
.max(100)
.allow('', null),
serialNumber: Joi.string()
.max(100)
.allow('', null),
rackId: Joi.string()
.max(50),
position: Joi.number()
.integer()
.min(1)
@@ -216,27 +89,81 @@ const updateDeviceSchema = Joi.object({
status: Joi.string()
.valid(...DEVICE_STATUS)
.default('offline')
.messages({
'any.only': `状态必须是以下之一: ${DEVICE_STATUS.join(', ')}`
}),
purchaseDate: Joi.date()
.allow(null),
.allow(null)
.messages({
'date.base': '购买日期格式无效'
}),
warrantyExpiry: Joi.date()
.allow(null),
.allow(null)
.messages({
'date.base': '保修到期日期格式无效'
}),
description: Joi.string()
.max(500)
.allow('', null),
.allow('', null)
.messages({
'string.max': '描述不能超过500个字符'
}),
customFields: Joi.object()
.allow(null)
}).min(1).messages({
'object.min': '至少需要提供一个字段进行更新'
});
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': '至少需要提供一个字段进行更新'
});
}
// 批量操作验证Schema
const batchDeviceIdsSchema = Joi.object({
deviceIds: Joi.array()
.items(Joi.string().required())
@@ -249,7 +176,6 @@ const batchDeviceIdsSchema = Joi.object({
})
});
// 批量状态变更验证Schema
const batchStatusSchema = Joi.object({
deviceIds: Joi.array()
.items(Joi.string().required())
@@ -269,7 +195,6 @@ const batchStatusSchema = Joi.object({
})
});
// 批量移动验证Schema
const batchMoveSchema = Joi.object({
deviceIds: Joi.array()
.items(Joi.string().required())
@@ -288,7 +213,6 @@ const batchMoveSchema = Joi.object({
.allow(null)
});
// 查询参数验证Schema
const queryDeviceSchema = Joi.object({
keyword: Joi.string()
.max(100)
@@ -313,6 +237,20 @@ const queryDeviceSchema = Joi.object({
.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,
@@ -321,5 +259,7 @@ module.exports = {
batchMoveSchema,
queryDeviceSchema,
DEVICE_TYPES,
DEVICE_STATUS
DEVICE_STATUS,
getCreateDeviceSchema,
getUpdateDeviceSchema
};