4. 设备模型与物模型设计

在IoT多租户SaaS平台中,设备接入只是第一步。真正决定平台能否规模化复用的,是物模型(Thing Model)——它把物理设备抽象成平台可理解、可存储、可编排的数据契约。没有物模型,每个设备都只能靠硬编码解析;有了物模型,平台才能自动完成数据解析、指令下发、告警触发和可视化渲染。本节围绕三个核心问题展开:物模型如何抽象、版本如何管理、多租户下如何隔离与复用。

一、物模型的三个抽象维度:属性、事件、服务

物模型用三类元素描述一个设备“能提供什么、会报告什么、可被要求做什么”。这三类元素不是随意划分,而是对应了IoT数据流的三种方向与语义。

属性(Property)描述设备的状态量,是平台可读、可写、可缓存的数据点。例如智能温控器的“当前温度”“目标温度”“运行模式”。属性有明确的数据类型(int32、float、bool、enum、text、date)、取值范围和单位。属性又分读写权限:只读属性(如当前温度)由设备上报,平台只订阅;读写属性(如目标温度)平台可下发期望值,设备执行后回传实际值,形成“期望值—实际值”的双值模型,这是处理设备离线、指令延迟的关键设计。

事件(Event)描述设备主动上报的离散通知,通常带有时间戳和事件参数。事件分两类:信息类(如“门磁被打开”“滤芯寿命剩余10%”)和告警类(如“高温告警”“通信中断”)。事件与属性的区别在于:属性是持续存在的状态,事件是某一时刻发生的动作或异常。平台对事件的处理通常是“记录+触发规则”,而不是缓存最新值。

服务(Service)描述设备可被调用的能力,即平台向设备下发的异步或同步指令。服务有输入参数和输出参数,例如“设置温度(目标值)→返回(执行结果)”“重启设备→返回(重启耗时)”。服务调用是请求—响应模型,平台需要处理超时、重试和幂等。

三者的关系可以用一张结构图清晰表达。下图展示了一个“智能温控器”物模型的完整抽象,以及属性、事件、服务在数据流中的位置。

智能温控器 物理设备 Device 物模型 Thing Model 产品级数据契约 属性 Property 当前温度 float ℃ 只读 目标温度 float ℃ 读写 运行模式 enum 读写 事件 Event 高温告警 参数:温度值 滤芯寿命低 参数:剩余% 设备重启 参数:原因码 服务 Service 设置温度 入:目标值 出:结果 重启设备 入:无 出:耗时 SaaS平台 解析/存储/规则 多租户引擎 上报属性/事件 下发服务调用 模型驱动解析 数据流方向:设备 → 属性/事件 → 平台(上行);平台 → 服务 → 设备(下行)。属性可双向,事件仅上行,服务仅下行。 物模型是产品级模板,同一产品下所有设备共享同一份模型定义,设备只需上报符合模型的数据即可被平台识别。

二、物模型版本管理:为什么必须做,怎么做

物模型不是一成不变的。产品迭代时,可能新增属性、修改取值范围、废弃旧事件、调整服务参数。如果直接修改线上模型,已接入的旧固件设备会因字段不匹配而解析失败,新设备又可能使用新字段,导致平台数据混乱。因此,物模型必须版本化。

版本管理的核心原则是:模型版本与产品绑定,设备在注册时声明自己遵循的模型版本。平台按设备声明的版本加载对应模型进行解析。具体做法如下:

  • 版本号规则:采用“主版本.次版本”两级,如 v1.0、v1.1、v2.0。主版本变更表示不兼容(删除属性、修改类型、改变语义);次版本变更表示兼容扩展(新增可选属性、新增事件、放宽取值范围)。
  • 兼容性策略:次版本升级时,平台同时加载新旧两个版本的模型定义,旧设备按旧版本解析,新设备按新版本解析,互不影响。主版本升级时,旧版本进入“维护期”,平台仍可解析但不再接受新设备注册到旧版本。
  • 灰度与回滚:新版本模型先在小批量设备上验证,确认解析成功率、指令响应率正常后,再逐步扩大。若发现异常,将产品默认版本回滚到上一稳定版本,已升级设备可继续使用新版本或强制回退。

一个常见的教学示例:某温控器产品 v1.0 只有“当前温度”和“目标温度”两个属性。v1.1 新增“湿度”只读属性,并新增“滤芯寿命低”事件。此时 v1.0 设备上报的数据中不含湿度字段,平台按 v1.0 模型解析,湿度字段缺失不报错;v1.1 设备上报湿度时,平台按 v1.1 模型解析并存储。规则引擎中若引用了“湿度”属性,需判断设备模型版本,避免对 v1.0 设备误触发。

三、多租户下的模型隔离与复用

多租户SaaS平台的核心矛盾是:既要让不同租户的数据和配置严格隔离,又要让公共的设备类型模型能够复用,避免每个租户重复定义。解决思路是分层:平台级公共模型库 + 租户级私有模型 + 租户对公共模型的引用与扩展。

平台级公共模型库由平台运营方维护,收录通用设备类型,如“标准电表”“通用温湿度传感器”“标准PLC网关”。这些模型对所有租户可见、可引用,但租户不能直接修改公共模型,只能“引用”或“派生”。

租户级私有模型是租户基于自身业务定义的模型,仅本租户可见。租户可以:直接创建全新模型;从公共模型派生,继承其属性、事件、服务,再追加租户特有字段;或者引用公共模型,不修改定义,仅在规则和可视化中使用。

隔离机制体现在三个层面:数据隔离——设备数据按租户ID分区存储,查询强制带租户过滤;模型隔离——租户只能看到自己的私有模型和公共模型,不能看到其他租户的私有模型;版本隔离——同一公共模型的不同版本,租户可锁定使用某一版本,避免平台升级公共模型时影响租户业务。

实践建议:在数据库设计上,物模型表增加 tenant_id 字段,公共模型 tenant_id = 0,租户私有模型 tenant_id = 具体租户ID。查询时使用 WHERE tenant_id IN (0, :currentTenantId),即可同时获取公共模型和本租户私有模型,天然实现隔离与复用。

四、一个可执行的物模型定义示例

下面给出一个温控器物模型的 JSON 结构示例(教学简化版),展示属性、事件、服务如何在同一份定义中组织,以及版本字段和租户字段的位置。

{
  "productKey": "thermostat_001",
  "modelVersion": "1.1",
  "tenantId": 0,
  "properties": [
    { "id": "current_temp", "name": "当前温度", "type": "float", "unit": "℃", "access": "r" },
    { "id": "target_temp",  "name": "目标温度", "type": "float", "unit": "℃", "access": "rw", "min": 5, "max": 35 },
    { "id": "mode",         "name": "运行模式", "type": "enum",  "values": ["cool","heat","auto"], "access": "rw" },
    { "id": "humidity",     "name": "湿度",     "type": "float", "unit": "%", "access": "r" }
  ],
  "events": [
    { "id": "high_temp_alarm", "name": "高温告警", "type": "alert", "params": ["current_temp"] },
    { "id": "filter_low",      "name": "滤芯寿命低", "type": "info",  "params": ["remain_percent"] }
  ],
  "services": [
    { "id": "set_temp", "name": "设置温度", "input": ["target_temp"], "output": ["result_code"] },
    { "id": "reboot",   "name": "重启设备", "input": [], "output": ["cost_ms"] }
  ]
}

平台收到设备上报的 {"current_temp": 26.5, "humidity": 48} 时,根据 productKey 和 modelVersion 找到对应模型,逐字段校验类型和范围,写入时序数据库。若收到 high_temp_alarm 事件,则触发规则引擎中配置的告警动作。若平台下发 set_temp 服务,设备执行后返回 result_code=0,平台更新“目标温度”的期望值状态。

五、设计检查清单

在实际项目中,物模型设计完成后,建议用以下清单快速自检:

检查项判断标准常见问题
属性是否区分读写只读属性设备上报,读写属性支持期望值/实际值全部设为读写,导致平台误下发
事件是否带时间戳和参数每个事件有明确参数列表和类型事件无参数,规则引擎无法判断
服务是否有超时和幂等设计服务定义包含超时时间和重试策略重复下发导致设备执行两次
版本号是否与产品绑定设备注册时声明模型版本平台只维护一份模型,旧设备解析失败
租户隔离字段是否完整模型表、设备表、数据表均含 tenant_id查询漏加租户条件,数据越权
公共模型是否可派生租户可继承公共模型并追加字段每个租户重复定义相同模型