设备影子
推荐阅读:
- 《设备接入(概述)》 — 了解 IotDeviceMessage 消息格式
- 《设备接入(MQTT 协议)》 — 了解 MQTT 主题与连接认证
- 《阿里云物联网平台 —— 设备影子》 (opens new window)
设备影子(Device Shadow),由 yudao-module-iot-biz 后端模块实现:IotDeviceShadowController 提供管理接口,IotDeviceShadowService 负责影子读写、设备上行处理。
平台为每个设备保存一份状态文档:管理后台写入期望状态(desired),设备上报实际状态(reported)。设备离线时,期望状态先保存在影子中,设备上线后再主动拉取并执行,从而解耦「下发指令」与「设备在线」。例如:
- 设备离线时,修改开关、运行模式等属性,设备上线后主动拉取并执行
- 对比期望值与上报值,确认设备是否已执行到位
如果想先跑通一遍,可以直接看「5. 快速验证」小节。
# 1. 影子概述
# 1.1 影子文档
每个设备有且只有一份影子,在首次访问时(管理后台查看、下发期望、切换开关,或设备首次发送影子消息)自动创建,默认开启。
| 字段 | 说明 |
|---|---|
desired | 期望状态,Key 为物模型属性标识符(identifier),由管理后台写入 |
reported | 上报状态,Key 同上,由设备上报写入 |
metadata | 元数据,记录每个属性的最后更新时间:{ desired: { key: { timestamp } }, reported: { ... } } |
version | 版本号,desired、reported 共用:每次成功更新 + 1,清空影子后归 0 |
shadowTime | 影子最后更新时间 |
status | 影子开关,参见 CommonStatusEnum 枚举(0 开启、1 关闭) |
注意时间单位
metadata 中的 timestamp、shadowTime(设备协议中为 timestamp)均为 epoch 秒;而物模型 date 类型的属性值,是毫秒时间戳。
# 1.2 工作流程
- 下发期望:管理后台填写期望值,合并写入
desired,version+ 1 - 推送影子:平台记录了设备所在网关时,尝试向设备推送完整影子;否则仅保存
- 拉取期望:设备上线后,主动
get获取最新desired并执行;收到完整推送时,也可直接执行 - 上报状态:设备执行后,通过
update上报reported - 对比差异:管理后台对比
desired与reported,高亮不一致的属性
重要
① 平台没有离线消息队列,不会在设备上线时自动重放期望值。设备每次连接成功后,都应主动 get 一次影子。
② 平台不会自动清除 desired。设备上报的 reported 与 desired 一致后,差异高亮自然消失;需要清空时,使用「重置影子」。
# 1.3 规则与限制
① 属性数:desired、reported 合并后各自最多 128 个属性,超过时拒绝本次更新。
② 并发更新:按 version 乐观锁写入数据库,冲突时基于最新影子重新合并,最多尝试 3 次(含首次),仍冲突则返回「版本冲突」。
③ 影子开关:关闭后,params 解析通过的设备影子消息被忽略(不继续处理、不回复),管理后台也不能下发期望值(提示「设备影子未启用」);仍可查看、重置影子。未传 params 时仍回复 400,params 解析失败时仍可能回复 500。
④ 删除设备:删除设备时,一并删除其影子。
⑤ 缓存:仅管理后台的影子查询使用 Redis 缓存(60 秒过期,写入后删除),设备消息处理和所有写入都以数据库为准。
⑥ 与属性上报相互独立:设备的属性上报(thing.property.post,包括 数据解析脚本 解析出的)只更新「物模型数据」,不会写入影子的 reported;设备影子的 update 也不会保存属性数据。同样,下发期望只写影子并推送影子文档,不会发送 thing.property.set 属性设置指令。需要影子对比的设备,要单独发送影子消息。
⑦ 合并与数据类型:
desired、reported按顶层属性合并:本次提交的属性整体覆盖同名旧值(结构体、数组也是整体替换,不做深度合并),未提交的属性保留。- 不支持删除单个属性。需要清空时,使用「重置影子」或设备发送
version = -1,两者都会同时清空desired和reported。 - 后端只校验属性数,不按物模型校验属性标识符和数据类型,值按 JSON 原样保存。管理后台的属性对比表只展示物模型中定义的属性,其它属性只能在「原始影子文档」中看到。
# 2. 表结构
对应 iot_device_shadow 表,每个设备一行,desired、reported、metadata 以 JSON 文本保存影子状态。
已包含 creator/create_time/updater/update_time/deleted/tenant_id 等通用字段
CREATE TABLE IF NOT EXISTS `iot_device_shadow` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`device_id` bigint NOT NULL COMMENT '设备编号',
`product_id` bigint NOT NULL COMMENT '产品编号',
`product_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '产品标识',
`device_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '设备名称',
`reported` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '上报状态',
`desired` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '期望状态',
`metadata` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '元数据',
`version` bigint NOT NULL DEFAULT 0 COMMENT '版本号',
`shadow_time` bigint NOT NULL DEFAULT 0 COMMENT '文档最后更新时间(epoch 秒)',
`status` tinyint NOT NULL DEFAULT 0 COMMENT '状态',
`creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `uk_device_id`(`device_id` ASC) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'IoT 设备影子表';
① device_id 为设备编号,关联 iot_device 表的 id 字段。唯一索引 uk_device_id 保证每个设备只有一份影子。
② product_id、product_key、device_name 冗余存储设备信息,方便查询和展示。
③ reported、desired、metadata 以 JSON 文本存储在 text 字段中,对应 IotDeviceShadowDO 的 Map 字段。结构见「1.1 影子文档」。
④ version 为影子版本号,用于乐观锁;shadow_time 为影子最后更新时间(epoch 秒)。
⑤ status 为影子开关,参见 CommonStatusEnum 枚举,默认 0 开启。
# 3. 管理后台
对应 [IoT 物联网 -> 设备接入 -> 设备管理] 菜单,进入设备详情页的「设备影子」Tab。对应前端项目的文件:
- Vue3:
src/views/iot/device/device/detail/DeviceDetailsShadow.vue - Vben Antd / Ele:
apps/web-antd、apps/web-ele下的src/views/iot/device/device/detail/modules/shadow.vue - Admin Uniapp:
src/pages-iot/device/device/shadow/index.vue(独立页面)

# 3.1 页面操作
① 顶部信息栏:展示版本号、最后更新时间,以及「影子开关」。
② 属性对比表:按产品物模型的属性,逐行展示期望值、上报值,以及各自的更新时间(来自 metadata)。有期望值、且与上报值不一致的行会高亮:比较时区分 JSON 类型(例如期望 1,上报 true 或 "1" 仍算不一致),结构体、数组按内容比较。没有期望值的行不高亮。
③ 下发期望状态:仅列出访问模式为「读写」「只写」的属性,按物模型数据类型提供输入方式:
| 数据类型 | 输入方式 | 期望值 |
|---|---|---|
bool、enum | 下拉选择 | 物模型定义的取值,例如 0 / 1 |
int、float、double | 数字输入 | 数字,int 取整 |
date | 日期时间选择 | 毫秒时间戳(数字) |
struct、array | JSON 输入 | JSON 对象、JSON 数组 |
text 等其它 | 文本输入 | 字符串 |
只需填写要修改的属性,未填写的属性保持不变;页面不能把已有的期望值改为空。下发成功后,会尝试推送给设备,详见「4.5 期望状态推送」小节。
④ 重置影子:清空 desired、reported、metadata,version 归 0。
⑤ 原始影子文档:展开后可查看完整的影子 JSON。
# 3.2 接口
由 IotDeviceShadowController 提供:
| 接口 | 说明 | 权限 |
|---|---|---|
GET /iot/device/shadow/get | 获取设备影子 | iot:device:shadow-query |
PUT /iot/device/shadow/update-desired | 更新期望状态 | iot:device:shadow-update |
PUT /iot/device/shadow/update-status | 更新影子开关状态 | iot:device:shadow-update |
DELETE /iot/device/shadow/reset | 重置设备影子 | iot:device:shadow-update |
# 4. 设备协议
# 4.1 消息格式
设备影子使用统一的 IotDeviceMessage 消息格式(见 《设备接入(概述)》 的「2.1 消息格式」):
- 外层
method固定为thing.shadow.update params.method为内层方法:get获取影子、update上报状态
以 MQTT 协议为例,对应主题如下(设备需提前订阅回复主题,或直接订阅 /sys/{productKey}/{deviceName}/#):
| 方向 | MQTT 主题 | 说明 |
|---|---|---|
| 上行 | /sys/{productKey}/{deviceName}/thing/shadow/update | 设备发送 get、update 请求 |
| 下行 | /sys/{productKey}/{deviceName}/thing/shadow/update_reply | 平台回复、期望状态推送 |
① 下文示例使用 JSON 序列化,并省略了 id、reportTime、deviceId 等后端自动填充的字段。
② 建议设备在请求中携带 requestId,回复会原样带回,便于与请求配对。
③ 影子的回复、推送都依赖下行通道。HTTP、CoAP 等短连接协议不支持下行推送,因此无法收到影子回复。
④ 影子消息只走各协议的标准消息格式:使用 《消息透传与数据解析》 的产品,数据解析脚本不能输出 thing.shadow.update,影子的回复、推送也不经过脚本编码。
⑤ 影子的请求与回复会记录在设备详情的「设备消息」Tab 中,便于排查。
# 4.2 获取影子(get)
请求消息:
{
"requestId": "1001",
"method": "thing.shadow.update",
"params": {
"method": "get"
}
}
回复消息:
{
"requestId": "1001",
"method": "thing.shadow.update",
"code": 0,
"msg": "成功",
"data": {
"state": {
"desired": {
"PowerSwitch": 1
},
"reported": {
"PowerSwitch": 0
}
},
"version": 3,
"timestamp": 1792980000
}
}
data.state 包含 desired、reported,version 为影子当前版本,timestamp 为影子最后更新时间(epoch 秒)。回复中不包含 metadata。
# 4.3 上报状态(update)
请求消息:
{
"requestId": "1002",
"method": "thing.shadow.update",
"params": {
"method": "update",
"state": {
"reported": {
"PowerSwitch": 1
}
},
"version": 3
}
}
① state.reported 为本次上报的属性,合并到影子的 reported 中:只覆盖本次上报的属性,其它属性保留。合并规则见「1.3 规则与限制」的 ⑦。建议按物模型的数据类型上报(例如 bool 属性上报数字 0 / 1),否则管理后台的差异高亮不会消失。
② version 为设备已知的影子版本,可选:
- 携带时:必须等于影子当前版本,否则回复
409,data.version为当前版本。设备应先get同步,再重新上报。 - 不携带时:平台基于最新影子强制合并。写入时仍按
version乐观锁保护,冲突时自动重试,仍冲突则回复409。
注意:携带的 version 须等于当前版本;管理后台下发期望值也会使 version + 1。
回复消息:
{
"requestId": "1002",
"method": "thing.shadow.update",
"code": 0,
"msg": "成功",
"data": {
"version": 4,
"timestamp": 1792980060
}
}
# 4.4 清空影子(version = -1)
params.version 为 -1 时,清空影子的 desired、reported、metadata,version 归 0,此时不需要传 state:
{
"requestId": "1003",
"method": "thing.shadow.update",
"params": {
"method": "update",
"version": -1
}
}
成功时,回复 code 为 0、data.version 为 0。效果与管理后台的「重置影子」相同。
# 4.5 期望状态推送
管理后台下发期望状态成功后,如果平台记录了设备所在的网关(设备最近一次上行消息所在的网关,设备离线后不清除),会尝试通过 update_reply 主题推送完整影子,data 结构与「4.2 获取影子」的回复相同,requestId 由平台生成。
推送不保证送达(发送失败仅记录日志),设备不在线时也不会重发,但期望状态已保存在影子中。设备下次上线后,通过 get 获取即可。
推荐的设备端处理流程:
- 连接成功后,订阅
update_reply主题,并发送一次get - 收到
get回复或推送后,执行desired中的期望值 - 执行完成后,携带最新
version发送update上报reported - 收到
409时,重新get后再上报
# 4.6 错误码
影子回复的 code、msg,参见 IotDeviceShadowErrorCodeEnum 枚举。失败时 data 仍包含 version、timestamp,便于设备重新同步:
| code | msg | 说明 |
|---|---|---|
0 | 成功 | |
400 | JSON 格式错误 | 未传 params |
401 | 缺少 method | 未传 params.method |
402 | 缺少 state | update 未传 state(version = -1 除外) |
404 | 缺少 reported | 未传 state.reported |
405 | reported 为空 | state.reported 为空对象 |
406 | 无效内层 method | params.method 不是 get、update |
408 | 影子属性数超过限制 | 合并后 reported 超过 128 个属性 |
409 | 影子版本冲突 | 版本不一致或重试后仍冲突,data.version 为当前版本 |
500 | 服务端异常 | 平台处理异常 |
另外,影子关闭时,params 解析通过的影子消息被直接忽略,不会回复;params 为空或解析失败时,仍可能按上表回复 400、500。
# 5. 快速验证
下面以 MQTT 直连设备为例,使用 MQTTX (opens new window) 模拟设备,走一遍「下发期望 → 设备拉取 → 设备上报 → 差异消失」。
请将主题中的
{productKey}、{deviceName}替换为实际值,PowerSwitch替换为你的属性标识符。
① 准备物模型属性:在设备所属产品的「物模型(功能定义)」中,确认有一个访问模式为「读写」的属性。下文以 bool 类型的 PowerSwitch 为例。
② 连接设备:按 《设备接入(MQTT 协议)》 的「4.1 连接认证」连接设备,并订阅 /sys/{productKey}/{deviceName}/thing/shadow/update_reply 主题(或直接订阅 /sys/{productKey}/{deviceName}/#)。
③ 打开设备影子:进入设备详情的「设备影子」Tab,确认「影子开关」为开启。为便于对照下文的版本号,可以先点击「重置影子」,此时版本号为 0。
④ 下发期望:在「下发期望状态」中,将 PowerSwitch 选择为 1,点击「下发期望状态」。如下图所示:

- 版本号变为
1,PowerSwitch行的期望值为1、上报值为-,该行高亮 - 设备在线时,MQTTX 在
update_reply主题收到一条完整影子推送,data.state.desired.PowerSwitch为1
⑤ 设备拉取期望:发布到 /sys/{productKey}/{deviceName}/thing/shadow/update 主题:
{
"requestId": "2001",
"method": "thing.shadow.update",
"params": {
"method": "get"
}
}
回复的 data.state.desired 中可以看到 PowerSwitch: 1,data.version 为 1。即使错过了 ④ 的推送(例如设备当时离线),也能通过 get 拿到期望值。
⑥ 设备上报执行结果:携带 ⑤ 回复中的 data.version,发布到同一主题:
{
"requestId": "2002",
"method": "thing.shadow.update",
"params": {
"method": "update",
"state": {
"reported": {
"PowerSwitch": 1
}
},
"version": 1
}
}
回复 code 为 0、data.version 为 2。如果回复 409,说明期间影子又被更新过,重新 get 后再上报。
⑦ 查看差异消失:回到「设备影子」Tab,点击「刷新」。PowerSwitch 的上报值变为 1,高亮消失。在「设备消息」Tab 中,可以看到上述 thing.shadow.update 的请求与回复。
友情提示:
- 如果 ⑦ 仍然高亮,检查上报值与期望值的 JSON 类型是否一致,例如不要用
true或"1"上报期望值1。 - 只发
thing.property.post属性上报,不会改变影子的上报值;影子的update也不会改变「物模型数据」Tab 的属性值。 - 验证完成后,可以点击「重置影子」清空测试数据。