第五部分补充:关键系统设计细节
1. 领域边界
| 领域 | 实体/聚合 | 负责 | 不负责 |
|---|---|---|---|
| 产品 | Product、ThingModelVersion | 产品能力定义和版本 | 设备当前值 |
| 设备 | Device、Credential、DeviceShadow | 身份、归属、运行态 | 用户会话 |
| 接入 | Connection、Telemetry、DeviceEvent | 协议适配、校验、标准化 | 业务页面 |
| 控制 | Command、CommandAck | 指令生命周期、超时、重试 | 直接修改历史遥测 |
| 身份 | User、Role、Tenant、Binding | 认证、授权、归属 | MQTT 会话 |
| 运维 | Alert、AuditLog、WorkOrder | 告警、审计、履约 | 产品物模型定义 |
MVP 为便于运行将这些领域放在同一 Node.js 进程中,但 API 和数据结构保持可拆分边界。
2. 设备生命周期
planned → registered → activated → online/offline → disabled → retired
│ │
└─ unbound ──┴─ bound → transfer_pending → bound
registered:平台已经生成设备身份,但设备可能从未联网。activated:设备首次通过身份验证;该动作不能与用户绑定混为一谈。online/offline:瞬时连接状态,应结合 Broker 连接事件和心跳 TTL 判断。bound:设备拥有终端用户归属;后台运维权限不等于所有权。disabled/retired:禁止新连接或指令,但历史数据和审计保留。
当前 MVP 用 status + ownerId + lastSeenAt 表达核心子集,生产库应拆成生命周期状态、连接状态和绑定状态三个字段。
3. 设备身份与绑定码
设备密钥和绑定码用途不同:设备密钥证明“这台设备是谁”,绑定码授权“一次用户归属操作”。生产实现要求:
- 设备密钥由安全随机数生成,烧录或产线注入,密文/哈希保存,支持轮换和吊销。
- MQTT 鉴权至少校验 tenant/product/device,主题 ACL 限制设备只能访问自身 Topic。
- 绑定码不复用设备密钥;可一次性、可过期、可由管理员重置,并对连续失败限流。
- 绑定事务以设备记录加锁或条件更新:
owner_id IS NULL → owner_id = current_user;受影响行数为零则返回冲突。 - 用户侧读取设备时始终追加
owner_id = current_user,对越权对象返回统一的不存在响应。
4. MQTT 主题和消息信封
上行属性: iot/v1/{tenantId}/{productKey}/{deviceId}/property/post
上行事件: iot/v1/{tenantId}/{productKey}/{deviceId}/event/{eventKey}/post
下行指令: iot/v1/{tenantId}/{productKey}/{deviceId}/command/get
指令回执: iot/v1/{tenantId}/{productKey}/{deviceId}/command/reply
统一信封:
{
"messageId": "01J...",
"deviceId": "d-123",
"timestamp": 1789142400000,
"version": "1.0",
"data": { "power": true }
}
服务端以 (device_id, message_id) 去重;时间戳只用于事件时间,不用于认证的唯一依据;无效物模型字段进入死信/错误流,不直接污染设备影子。
5. 指令状态机和一致性
pending ──publish──> sent ──ACK(success)──> succeeded
│ ├──ACK(error)─────> failed
│ └──deadline───────> timeout
└──cancel─────────────────────────────> canceled
写库和发 MQTT 采用 Transactional Outbox:在同一数据库事务写入 command 与 outbox_event;发布器发送成功后标记 Outbox;重复发布由设备/服务端按 commandId 幂等。只有合法 ACK 可以更新最终状态,且状态转换通过条件更新防止迟到 ACK 覆盖取消/超时状态。
MVP backend/server.js:addCommand 对在线设备立即生成 succeeded、离线设备生成 pending。这是联调替身,不是生产 ACK 实现。
6. 设备影子合并规则
影子至少包含:
{
"desired": { "power": true },
"reported": { "power": false },
"delta": { "power": true },
"desiredVersion": 12,
"reportedVersion": 11,
"updatedAt": "2026-09-12T12:00:00+08:00"
}
- 用户控制先写
desired和新版本;设备 ACK/属性上报更新reported。 delta是 desired 与 reported 的差异,不作为独立事实源。- 遥测历史只追加,不随影子覆盖;影子更新必须校验版本,防止乱序消息回退状态。
7. 授权规则
| 接口类型 | 身份 | 附加约束 |
|---|---|---|
| 管理员 API | admin/operator | 产品创建和审计仅 admin |
| 小程序设备 API | user | device.ownerId === user.id |
| 设备遥测 API | device | X-Device-Id + X-Device-Secret,生产迁移 mTLS/MQTT 凭证 |
| 企业 API | tenant member/service account | tenant_id 强制过滤 + scope |
前端菜单隐藏只改善体验,后端仍对每个接口授权。审计记录操作主体、动作、对象、结果、来源 IP/客户端和关联 ID;MVP 已实现主体、动作、对象、详情和时间。
8. JSON MVP 到生产数据库的映射
| MVP 集合 | 生产表/存储 |
|---|---|
| users | iam_user, tenant_member, role_binding |
| products | iot_product, thing_model_version |
| devices | iot_device, device_credential, device_binding, device_shadow |
| telemetry | TimescaleDB hypertable telemetry_point |
| commands | device_command, command_attempt, outbox_event |
| alerts | alert, alert_transition |
| audits | 只追加 audit_log 或日志存储 |
迁移时先冻结 JSON 写入,导入主数据,校验数量/唯一键/绑定关系,再切换连接配置;不直接把 JSON 文件作为数据库备份格式长期维护。