1. 新增动态设备校验Schema生成器,支持从数据库读取字段配置生成校验规则 2. 重构设备增改接口,使用动态校验替代硬编码Schema 3. 调整前端设备表单,适配字段配置并锁定核心系统字段 4. 修复前后端默认字段配置不一致问题 5. 新增字段管理页面防护,锁定核心字段的必填/可见配置
16 KiB
16 KiB
设备字段必填规则与可见性同步设计文档
版本:1.0.0 | 更新日期:2026-06-12
1. 问题概述
1.1 现状
设备字段管理页面(/api/deviceFields)允许用户配置每个字段的:
- 是否必填(
required) - 是否可见(
visible)
但在设备管理页面添加/编辑设备时,这些配置完全不生效。具体表现为:
| 层面 | 当前行为 | 预期行为 |
|---|---|---|
| 后端 Joi Schema | 硬编码写死,不查询数据库 | 从 DeviceField 表动态读取 required 配置 |
| 前端表单渲染 | rackId/position/height 跳过字段配置,硬编码必填 |
跟随字段配置,但对强依赖字段做防护 |
| 前端默认常量 | powerConsumption 与后端不一致 |
与后端 initDeviceFields.js 保持同步 |
1.2 影响范围
- 用户在字段管理页面修改配置后感到困惑(修改不生效)
- 无法灵活控制不同场景下的业务校验规则
powerConsumption等字段的前后端默认值不一致
2. 字段强依赖分析
2.1 系统字段依赖矩阵
| 字段名 | 字段标识 | 系统字段 | 下游依赖场景 | 是否可改必填 | 是否可改可见 |
|---|---|---|---|---|---|
| 设备名称 | name |
是 | 操作日志、工单关联、拓扑图标签、导出文件名 | 否(强制必填) | 否(强制可见) |
| 设备类型 | type |
是 | 搜索筛选、统计分类、3D可视化类型渲染 | 是 | 是(但筛选功能受限) |
| 序列号 | serialNumber |
是 | 设备唯一标识:端口关联(按SN)、盘点扫码匹配、空闲设备恢复、工单关联、导入去重、搜索定位 | 否(强制必填) | 否(强制可见) |
| 所在机柜 | rackId |
是 | 3D可视化定位、机柜容量统计、U位冲突检测、机柜功率计算 | 是 | 是 |
| 位置(U) | position |
是 | 3D可视化定位、U位冲突检测、机柜容量计算 | 否(强制必填) | 是 |
| 高度(U) | height |
是 | 3D可视化渲染、U位占用计算 | 否(强制必填) | 是 |
| 状态 | status |
是 | 搜索筛选、统计卡片、3D可视化颜色 | 是 | 是(但筛选功能受限) |
| 设备型号 | model |
是 | 搜索筛选 | 是 | 是 |
| 功率(W) | powerConsumption |
否 | 机柜功率统计、机房功率监控 | 是 | 是 |
| IP地址 | ipAddress |
否 | 搜索筛选、端口关联 | 是 | 是 |
| 购买日期 | purchaseDate |
否 | 无强依赖 | 是 | 是 |
| 保修到期 | warrantyExpiry |
否 | 无强依赖 | 是 | 是 |
| 描述 | description |
否 | 无强依赖 | 是 | 是 |
| 品牌 | brand |
否 | 无强依赖 | 是 | 是 |
2.2 强依赖字段判定标准
满足以下任一条件的字段,标记为强制锁定字段:
- 唯一标识性:作为设备在系统中的唯一标识,被其他模块硬引用(如
serialNumber) - 显示必要性:无此字段设备无法在其他模块中被识别(如
name) - 物理定位性:无此字段设备无法在3D/平面图中定位(如
position、height)
3. 修复方案
3.1 总体架构
+------------------+ +------------------+ +------------------+
| 字段管理页面 | ----> | DeviceField表 | <---- | 启动初始化 |
| (FieldConfig) | PUT | (数据库) | | (initDeviceFields)|
+------------------+ +------------------+ +------------------+
|
+--------------+--------------+
| | |
v v v
+-----------+ +-----------+ +-----------+
| 设备添加 | | 设备导入 | | 设备编辑 |
| 前端表单 | | 后端导入 | | 后端接口 |
| (Device | | 验证 | | (Joi |
| FormModal)| | (已正常) | | 动态Schema)|
+-----------+ +-----------+ +-----------+
| |
| 读取field.required | 动态查询DeviceField
v v
表单Item rules Joi Schema校验
(强制锁定字段额外防护) (强制锁定字段额外防护)
3.2 方案一:后端 Joi Schema 动态化(核心)
3.2.1 新增文件
backend/validation/dynamicDeviceSchema.js
const Joi = require('joi');
const DeviceField = require('../models/DeviceField');
const DEVICE_TYPES = ['server', 'switch', 'router', 'storage', 'other'];
const DEVICE_STATUS = ['running', 'maintenance', 'offline', 'fault', 'idle'];
// 强制锁定字段列表(不受字段管理配置影响)
const FORCE_REQUIRED_FIELDS = ['name', 'serialNumber', 'position', 'height'];
/**
* 动态生建设备创建Joi Schema
* 从DeviceField表读取字段配置,结合强制锁定字段规则
* @param {boolean} isUpdate - 是否为更新模式
* @returns {Promise<Joi.ObjectSchema>}
*/
async function createDeviceSchema(isUpdate = false) {
const fieldConfigs = await DeviceField.findAll({
order: [['order', 'ASC']],
});
const schemaMap = {};
fieldConfigs.forEach(field => {
let validator;
// 判断是否强制必填
const isRequired = FORCE_REQUIRED_FIELDS.includes(field.fieldName) || field.required;
switch (field.fieldName) {
case 'name':
validator = Joi.string().max(100);
if (isRequired) validator = validator.required();
else validator = validator.allow('', null);
break;
case 'type':
validator = Joi.string().valid(...DEVICE_TYPES);
if (isRequired) validator = validator.required();
break;
case 'serialNumber':
validator = Joi.string().max(100);
if (isRequired) validator = validator.required();
else validator = validator.allow('', null);
break;
case 'rackId':
validator = Joi.string().allow('', null).max(50);
break;
case 'position':
validator = Joi.number().integer().min(1).max(100).allow(null);
break;
case 'height':
validator = Joi.number().integer().min(1).max(50).allow(null);
break;
case 'powerConsumption':
validator = Joi.number().min(0).max(100000).allow(null);
break;
case 'status':
validator = Joi.string().valid(...DEVICE_STATUS).default('offline');
break;
case 'model':
case 'ipAddress':
case 'description':
validator = Joi.string().allow('', null).max(100);
break;
case 'purchaseDate':
case 'warrantyExpiry':
validator = Joi.date().allow(null);
break;
default:
// 自定义字段走宽松校验
validator = Joi.any().allow(null);
}
// 仅在创建模式下(非更新模式)且字段非强制锁定、且数据库配置为required时使用required()
// 更新模式下避免object.min(1)策略与动态required冲突
schemaMap[field.fieldName] = validator;
});
// 补充未在DeviceField表中的字段
schemaMap.customFields = Joi.object().allow(null);
let schema = Joi.object(schemaMap);
if (isUpdate) {
schema = schema.min(1).messages({
'object.min': '至少需要提供一个字段进行更新',
});
}
return schema;
}
module.exports = {
createDeviceSchema,
DEVICE_TYPES,
DEVICE_STATUS,
FORCE_REQUIRED_FIELDS,
};
3.2.2 修改文件
backend/routes/devices.js
- 移除对静态
createDeviceSchema、updateDeviceSchema的引用 router.post('/')中请求到来时调用createDeviceSchema(false)生成动态 schema 进行验证router.put('/:deviceId')中调用createDeviceSchema(true)生成动态 schema
// 替换:
// const { createDeviceSchema, updateDeviceSchema } = require('../validation/deviceSchema');
// 为:
const { createDeviceSchema } = require('../validation/dynamicDeviceSchema');
backend/validation/deviceSchema.js
- 移除
createDeviceSchema、updateDeviceSchema导出 - 保留
batchDeviceIdsSchema、batchStatusSchema、batchMoveSchema、queryDeviceSchema(这些与字段配置无关) DEVICE_TYPES、DEVICE_STATUS枚举可保留供其他模块引用
3.3 方案二:前端表单完整适配字段配置
3.3.1 修改文件
frontend/src/components/device/DeviceFormModal.jsx
修改点 A — 取消 rackId/position/height 的排除(第272~278行):
// 修改前:排除rackId/position/height
const filteredFields = deviceFields.filter(
field =>
field.fieldName !== 'deviceId' &&
field.fieldName !== 'rackId' && // 移除
field.fieldName !== 'position' && // 移除
field.fieldName !== 'height' // 移除
);
// 修改后:只排除deviceId
const filteredFields = deviceFields.filter(
field => field.fieldName !== 'deviceId'
);
修改点 B — "设备位置选择"区块(第326~429行)的 rules 改为动态:
// 修改前:硬编码required: true
<Form.Item
name="rackId"
rules={[{ required: true, message: '请选择机柜' }]}
>...
// 修改后:读取字段配置
const rackField = deviceFields.find(f => f.fieldName === 'rackId');
// ... 使用 rackField?.required 决定是否需要 required 校验
// 强制锁定字段:position/height 即使字段配置为非必填,仍强制必填
const posField = deviceFields.find(f => f.fieldName === 'position');
const isPositionRequired = true; // 强制锁定
<Form.Item
name="position"
rules={isPositionRequired ? [{ required: true, message: '请输入U位' }] : []}
>...
修改点 C — 位置信息如果设为非必填,提交时给警告:
const handleSubmit = values => {
if (!values.position || !values.height) {
Modal.warning({
title: '位置信息不完整',
content: '位置(U位)或高度信息为空,设备将无法在3D视图中准确定位,建议填写完整。',
});
}
// ... 继续提交
};
3.4 方案三:字段管理页面防护
3.4.1 修改文件
frontend/src/pages/FieldConfig.jsx(或对应字段管理页面组件)
对强制锁定字段的"必填"和"可见"开关做禁用处理:
| 字段 | 必填开关 | 可见开关 |
|---|---|---|
name |
禁用,显示"系统核心字段" | 禁用,显示"系统核心字段" |
serialNumber |
禁用,显示"系统核心字段" | 禁用,显示"系统核心字段" |
position |
禁用,显示"3D定位依赖" | 启用 |
height |
禁用,显示"3D定位依赖" | 启用 |
// 伪代码逻辑
const isLockedRequired = ['name', 'serialNumber', 'position', 'height'].includes(field.fieldName);
const isLockedVisible = ['name', 'serialNumber'].includes(field.fieldName);
<Form.Item label="必填">
<Switch
checked={field.required}
disabled={isLockedRequired}
title={isLockedRequired ? '系统核心字段,不可关闭必填' : ''}
/>
</Form.Item>
3.5 方案四:同步默认值
3.5.1 修改文件
frontend/src/constants/deviceManagementConstants.js
| 字段 | 修改前 | 修改后 |
|---|---|---|
deviceId 的 required |
true |
false |
powerConsumption 的 required |
false |
true |
3.5.2 修改文件
backend/validation/deviceSchema.js
- 移除
createDeviceSchema、updateDeviceSchema的module.exports - 保留
batchDeviceIdsSchema、batchStatusSchema、batchMoveSchema、queryDeviceSchema
4. 数据流对比
4.1 修复前数据流
字段管理页面修改 required=true → DeviceField表 更新
|
┌─────────────────────┘
▼ (无读取)
后端 Joi Schema (硬编码) ← 忽略数据库配置,直接拦截请求
前端 DeviceFormModal ← 只读部分字段配置,position等硬编码
4.2 修复后数据流
字段管理页面修改 required=true/false → DeviceField表 更新
|
┌─────────────────────────┴─────────────┐
▼ ▼
设备添加接口 (POST /api/devices) 设备添加弹窗 (DeviceFormModal)
│ │
▼ ▼
createDeviceSchema(false) GET /api/deviceFields
│ │
▼ ▼
动态查询DeviceField表 读取 field.required
强制锁定字段 = required() 强制锁定字段覆盖为必填
其他字段跟随数据库配置 其他字段跟随配置
│ │
▼ ▼
Joi校验 → 通过 → Device.create() Form.Item rules 动态生成
5. 风险与注意事项
5.1 兼容性
- 本方案为新功能(让字段管理配置真正生效),不涉及向后兼容性破坏
- 已有数据库中的
DeviceField配置保持不变 - 已有
Device表中的数据不受影响
5.2 边界情况
| 场景 | 处理策略 |
|---|---|
| DeviceField 表为空(首次部署) | 动态 schema 降级为最小验证集 |
| 用户在字段管理修改后立即添加设备 | 实时查询 DeviceField 表,无需缓存 |
| 高并发场景 | 每次创建设备都查表,建议后续可加内存缓存+过期机制 |
| 自定义字段(非预定义字段) | 走 Joi.any().allow(null) 不限制 |
5.3 性能考量
每次创建/更新设备时多一次 DeviceField.findAll() 查询。由于:
DeviceField表数据量很小(<50 条)- 设备创建/更新频率远低于查询频率
- 无复杂关联查询
该额外查询对性能影响可以忽略不计。
6. 实施计划
| 步骤 | 文件 | 工作量估算 | 说明 |
|---|---|---|---|
| 1 | 新建 dynamicDeviceSchema.js |
~60 行 | 核心动态 schema 生成逻辑 |
| 2 | 修改 devices.js |
~10 行 | 替换静态 schema 引用 |
| 3 | 修改 deviceSchema.js |
~5 行 | 移除已迁移的导出 |
| 4 | 修改 DeviceFormModal.jsx |
~30 行 | 适配字段配置 |
| 5 | 修改 FieldConfig.jsx |
~20 行 | 锁定字段防护 |
| 6 | 修改 deviceManagementConstants.js |
~2 行 | 同步默认值 |
7. 附录
7.1 相关文件清单
| 文件路径 | 操作类型 |
|---|---|
backend/validation/dynamicDeviceSchema.js |
新增 |
backend/routes/devices.js |
修改 |
backend/validation/deviceSchema.js |
修改 |
frontend/src/components/device/DeviceFormModal.jsx |
修改 |
frontend/src/pages/FieldConfig.jsx |
修改 |
frontend/src/constants/deviceManagementConstants.js |
修改 |
7.2 参考
- 后端导入功能的必填验证逻辑(
backend/routes/devices.js第1191~1215行)已正确查询DeviceField表,本设计参考其实现模式 - 设备模型定义:
backend/models/Device.js - 设备字段模型定义:
backend/models/DeviceField.js - 默认字段初始化:
backend/initDeviceFields.js