feat: 修改了bug
This commit is contained in:
@@ -87,7 +87,7 @@ const defaultDeviceFields = [
|
||||
fieldName: 'powerConsumption',
|
||||
displayName: '功率(W)',
|
||||
fieldType: 'number',
|
||||
required: false,
|
||||
required: true,
|
||||
order: 9,
|
||||
visible: true,
|
||||
isSystem: true
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user