# Room Status Moment 集成方案 ## 1. 背景 我们需要将一个新的数据库表 `room_status.room_status_moment`(快照表)集成到现有的 Kafka 处理流程中。 该表用于存储每个房间/设备的最新状态。 现有的逻辑(批量插入到 `rcu_action_events`)必须保持不变。 ## 2. 数据库配置 新表位于一个独立的数据库中(可能是 `log_platform`,或者现有数据库中的新模式 `room_status`)。 我们将添加对 `ROOM_STATUS` 独立连接池的支持,以确保灵活性。 **环境变量配置:** ```env # 现有数据库配置 DB_HOST=... ... # 新 Room Status 数据库配置 (如果未提供,默认使用现有数据库,但使用独立的连接池) ROOM_STATUS_DB_HOST=... ROOM_STATUS_DB_PORT=... ROOM_STATUS_DB_USER=... ROOM_STATUS_DB_PASSWORD=... ROOM_STATUS_DB_DATABASE=log_platform <-- SQL 脚本中的目标数据库名 ROOM_STATUS_DB_SCHEMA=room_status ``` ## 3. 字段映射策略 目标表:`room_status.room_status_moment` 唯一键:`(hotel_id, room_id, device_id)` | 源字段 (Kafka) | 目标字段 | 更新逻辑 | | :--- | :--- | :--- | | `hotel_id` | `hotel_id` | 主键/索引键 | | `room_id` | `room_id` | 主键/索引键 | | `device_id` | `device_id` | 主键/索引键 | | `ts_ms` | `ts_ms` | 始终更新为最新值 | | `sys_lock_status` | `sys_lock_status` | 直接映射 (如果存在) | | `device_list` (0x36)
`control_list` (0x0F) | `dev_loops` (JSONB) | **合并策略 (Merge)**:
Key: `001002003` (Type(3)+Addr(3)+Loop(3))
Value: `dev_data` (int)
操作: `old_json || new_json` (旧值合并新值) | | `fault_list` (0x36) | `faulty_device_count` (JSONB) | **替换策略 (Replace)**:
由于 0x36 上报的是完整故障列表,我们直接覆盖该字段。
内容: `{dev_type, dev_addr, dev_loop, error_type, error_data}` 的列表 | | `fault_list` -> item `error_type=1` | `online_status` | 如果 `error_data=1` -> 离线 (0)
如果 `error_data=0` -> 在线 (1)
*需要验证具体的映射约定* | **关于在线状态 (Online Status) 的说明**: 文档描述: "0x01: 0:在线 1:离线"。 表字段 `online_status` 类型为 INT2。 约定:通常 1=在线, 0=离线。 逻辑: - 如果故障类型 0x01, 数据 0 (在线) -> 设置 `online_status` = 1 - 如果故障类型 0x01, 数据 1 (离线) -> 设置 `online_status` = 0 - 否则 -> 不更新 `online_status` ## 4. Upsert 逻辑 (PostgreSQL) 我们将使用 `INSERT ... ON CONFLICT DO UPDATE` 语法。 ```sql INSERT INTO room_status.room_status_moment ( guid, ts_ms, hotel_id, room_id, device_id, sys_lock_status, online_status, dev_loops, faulty_device_count ) VALUES ( $guid, $ts_ms, $hotel_id, $room_id, $device_id, $sys_lock_status, $online_status, $dev_loops::jsonb, $faulty_device_count::jsonb ) ON CONFLICT (hotel_id, room_id, device_id) DO UPDATE SET ts_ms = EXCLUDED.ts_ms, -- 仅在新数据不为空时更新 sys_lock_status sys_lock_status = COALESCE(EXCLUDED.sys_lock_status, room_status.room_status_moment.sys_lock_status), -- 仅在新数据不为空时更新 online_status online_status = COALESCE(EXCLUDED.online_status, room_status.room_status_moment.online_status), -- 合并 dev_loops dev_loops = CASE WHEN EXCLUDED.dev_loops IS NULL THEN room_status.room_status_moment.dev_loops ELSE COALESCE(room_status.room_status_moment.dev_loops, '{}'::jsonb) || EXCLUDED.dev_loops END, -- 如果存在则替换 faulty_device_count faulty_device_count = COALESCE(EXCLUDED.faulty_device_count, room_status.room_status_moment.faulty_device_count) WHERE -- 可选优化:仅在时间戳更新时写入 -- 注意:对于 JSON 合并,很难在不计算的情况下检测是否相等。 -- 我们依赖 ts_ms 的变化来表示数据的“新鲜度”。 EXCLUDED.ts_ms >= room_status.room_status_moment.ts_ms ; ``` ## 5. 架构变更 1. **`src/config/config.js`**: 添加 `roomStatusDb` 配置项。 2. **`src/db/roomStatusManager.js`**: 新增单例类,用于管理 `log_platform` 的数据库连接池。 3. **`src/db/statusBatchProcessor.js`**: 针对 `room_status_moment` 的专用批量处理器。 * **原因**: Upsert 逻辑复杂,且与 `rcu_action_events` 的追加写日志模式不同。 * 它需要在批处理内聚合每个设备的更新,以减少数据库负载(去重)。 4. **`src/processor/statusExtractor.js`**: 辅助工具,用于将 `KafkaPayload` 转换为 `StatusRow` 数据结构。 5. **`src/index.js`**: 挂载新的处理器逻辑。 ## 6. 去重策略 (内存批量聚合) 由于 `room_status_moment` 是快照表,如果我们在 1 秒内收到同一设备的 10 次更新: - 我们只需要写入 **最后一次** 的状态(或合并后的状态)。 - `StatusBatchProcessor` 应该维护一个映射: `Map`。 - 在 Flush 时,将 Map 的值转换为批量 Upsert 操作。 - **约束**: `dev_loops` 的更新如果是针对不同回路的,可能需要累积合并。 - **优化策略**: - 如果 `dev_loops` 是部分更新,我们不能简单地取最后一条消息。 - 但是,在短时间的批处理窗口(例如 500ms)内,我们可以在内存中将它们合并后再发送给数据库。 - 结构: `Map` - 逻辑: `MergedState.dev_loops = Object.assign({}, old.dev_loops, new.dev_loops)` ## 7. 执行计划 1. 添加配置 (Config) 和数据库管理器 (DB Manager)。 2. 实现 `StatusExtractor` (将 Kafka 载荷转换为快照数据)。 3. 实现 `StatusBatchProcessor` (包含内存合并逻辑)。 4. 更新 `processKafkaMessage`,使其同时返回 `LogRows` (现有) 和 `StatusUpdate` (新增)。 5. 在主循环中处理分发。