Files
yunrui_asset/docs/superpowers/specs/设备字段必填规则同步设计.md
zhang1106 1b9a5571ae refactor(devices): 实现设备字段动态校验与配置同步
1. 新增动态设备校验Schema生成器,支持从数据库读取字段配置生成校验规则
2. 重构设备增改接口,使用动态校验替代硬编码Schema
3. 调整前端设备表单,适配字段配置并锁定核心系统字段
4. 修复前后端默认字段配置不一致问题
5. 新增字段管理页面防护,锁定核心字段的必填/可见配置
2026-06-12 16:01:41 +08:00

16 KiB
Raw Permalink Blame History

设备字段必填规则与可见性同步设计文档

版本: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 强依赖字段判定标准

满足以下任一条件的字段,标记为强制锁定字段

  1. 唯一标识性:作为设备在系统中的唯一标识,被其他模块硬引用(如 serialNumber
  2. 显示必要性:无此字段设备无法在其他模块中被识别(如 name
  3. 物理定位性:无此字段设备无法在3D/平面图中定位(如 positionheight

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

  • 移除对静态 createDeviceSchemaupdateDeviceSchema 的引用
  • 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

  • 移除 createDeviceSchemaupdateDeviceSchema 导出
  • 保留 batchDeviceIdsSchemabatchStatusSchemabatchMoveSchemaqueryDeviceSchema(这些与字段配置无关)
  • DEVICE_TYPESDEVICE_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

字段 修改前 修改后
deviceIdrequired true false
powerConsumptionrequired false true

3.5.2 修改文件

backend/validation/deviceSchema.js

  • 移除 createDeviceSchemaupdateDeviceSchemamodule.exports
  • 保留 batchDeviceIdsSchemabatchStatusSchemabatchMoveSchemaqueryDeviceSchema

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