第五部分补充:关键系统设计细节

1. 领域边界

领域实体/聚合负责不负责
产品Product、ThingModelVersion产品能力定义和版本设备当前值
设备Device、Credential、DeviceShadow身份、归属、运行态用户会话
接入Connection、Telemetry、DeviceEvent协议适配、校验、标准化业务页面
控制Command、CommandAck指令生命周期、超时、重试直接修改历史遥测
身份User、Role、Tenant、Binding认证、授权、归属MQTT 会话
运维Alert、AuditLog、WorkOrder告警、审计、履约产品物模型定义

一期将这些领域放在同一 Spring Boot 单体中,通过 package、应用服务和消息 DTO 保持边界;当吞吐、发布或团队边界稳定后再拆为微服务。

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. 设备身份与绑定码

设备密钥和绑定码用途不同:设备密钥证明“这台设备是谁”,绑定码授权“一次用户归属操作”。生产实现要求:

  1. 设备密钥由安全随机数生成,烧录或产线注入,密文/哈希保存,支持轮换和吊销。
  2. MQTT 鉴权至少校验 tenant/product/device,主题 ACL 限制设备只能访问自身 Topic。
  3. 绑定码不复用设备密钥;可一次性、可过期、可由管理员重置,并对连续失败限流。
  4. 绑定事务以设备记录加锁或条件更新:owner_id IS NULL → owner_id = current_user;受影响行数为零则返回冲突。
  5. 用户侧读取设备时始终追加 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) 去重;时间戳只用于事件时间,不用于认证的唯一依据;无效物模型字段进入死信/错误流,不直接污染设备影子。

后端不直接处理上述 MQTT Topic。EMQX Bridge 将其映射到 Kafka:

Kafka Topic方向后端动作
iot.device.telemetry.upEMQX → Spring Boot订阅、幂等保存遥测、更新在线态
iot.device.event.upEMQX → Spring Boot订阅并转换为告警
iot.device.command.downSpring Boot → EMQX发布设备控制指令
iot.device.command.ackEMQX → Spring Boot订阅并更新指令最终状态

Kafka Key 固定为 deviceId;同设备消息进入同一分区,Consumer Group 为 iot-platform-monolith(可配置)。

5. 指令状态机和一致性

pending ──publish──> sent ──ACK(success)──> succeeded
   │                   ├──ACK(error)─────> failed
   │                   └──deadline───────> timeout
   └──cancel─────────────────────────────> canceled

当前单体先写入 device_command(PENDING),随后由 KafkaCommandPublisher 发布 command.down 并更新为 SENT;KafkaInboundListener 消费 command.ack 后更新为 SUCCEEDED/FAILED。HTTP 请求成功只代表指令进入 Kafka 链路,不代表设备执行成功。

生产增强采用 Transactional Outbox:在同一 MySQL 事务写入 device_command 与 outbox_event;发布器发送成功后标记 Outbox。重复发布由设备/服务端按 commandId 幂等;状态条件更新防止迟到 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. 授权规则

接口类型身份附加约束
管理员 APIADMIN/OPERATOR产品创建和审计仅 ADMIN
小程序设备 APIUSERdevice.ownerId === user.id
设备 MQTTdevice由 EMQX 验证设备凭据和 Topic ACL;后端仅信任授权 Kafka Topic
企业 APItenant member/service accounttenant_id 强制过滤 + scope

前端菜单隐藏只改善体验,后端仍对每个接口授权。审计记录操作主体、动作、对象、结果、来源 IP/客户端和关联 ID;MVP 已实现主体、动作、对象、详情和时间。

8. 当前 MySQL 表与后续归属

当前表后续模块/存储
iam_userIAM 服务;二期增加 tenant/member/role_binding
iot_product产品/物模型模块
iot_device设备资产;后续拆 credential/binding/shadow
device_telemetry初期 MySQL;达到容量阈值后迁移时序存储
device_command控制模块;增加 attempt/outbox
device_alert告警模块;增加 transition/notification
audit_log只追加审计存储

Flyway 管理结构版本,JPA 运行时使用 ddl-auto=validate,禁止在共享环境自动改表。未来拆服务时按表所有权迁移,不允许多个微服务直接写同一业务表。