第五部分补充:关键系统设计细节
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. 设备身份与绑定码
设备密钥和绑定码用途不同:设备密钥证明“这台设备是谁”,绑定码授权“一次用户归属操作”。生产实现要求:
- 设备密钥由安全随机数生成,烧录或产线注入,密文/哈希保存,支持轮换和吊销。
- 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) 去重;时间戳只用于事件时间,不用于认证的唯一依据;无效物模型字段进入死信/错误流,不直接污染设备影子。
后端不直接处理上述 MQTT Topic。EMQX Bridge 将其映射到 Kafka:
| Kafka Topic | 方向 | 后端动作 |
|---|---|---|
iot.device.telemetry.up | EMQX → Spring Boot | 订阅、幂等保存遥测、更新在线态 |
iot.device.event.up | EMQX → Spring Boot | 订阅并转换为告警 |
iot.device.command.down | Spring Boot → EMQX | 发布设备控制指令 |
iot.device.command.ack | EMQX → 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. 授权规则
| 接口类型 | 身份 | 附加约束 |
|---|---|---|
| 管理员 API | ADMIN/OPERATOR | 产品创建和审计仅 ADMIN |
| 小程序设备 API | USER | device.ownerId === user.id |
| 设备 MQTT | device | 由 EMQX 验证设备凭据和 Topic ACL;后端仅信任授权 Kafka Topic |
| 企业 API | tenant member/service account | tenant_id 强制过滤 + scope |
前端菜单隐藏只改善体验,后端仍对每个接口授权。审计记录操作主体、动作、对象、结果、来源 IP/客户端和关联 ID;MVP 已实现主体、动作、对象、详情和时间。
8. 当前 MySQL 表与后续归属
| 当前表 | 后续模块/存储 |
|---|---|
iam_user | IAM 服务;二期增加 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,禁止在共享环境自动改表。未来拆服务时按表所有权迁移,不允许多个微服务直接写同一业务表。