116 lines
5.7 KiB
Markdown
116 lines
5.7 KiB
Markdown
|
|
# 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) <br> `control_list` (0x0F) | `dev_loops` (JSONB) | **合并策略 (Merge)**: <br> Key: `001002003` (Type(3)+Addr(3)+Loop(3)) <br> Value: `dev_data` (int) <br> 操作: `old_json || new_json` (旧值合并新值) |
|
|||
|
|
| `fault_list` (0x36) | `faulty_device_count` (JSONB) | **替换策略 (Replace)**: <br> 由于 0x36 上报的是完整故障列表,我们直接覆盖该字段。<br> 内容: `{dev_type, dev_addr, dev_loop, error_type, error_data}` 的列表 |
|
|||
|
|
| `fault_list` -> item `error_type=1` | `online_status` | 如果 `error_data=1` -> 离线 (0) <br> 如果 `error_data=0` -> 在线 (1) <br> *需要验证具体的映射约定* |
|
|||
|
|
|
|||
|
|
**关于在线状态 (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<Key(hotel,room,device), LatestData>`。
|
|||
|
|
- 在 Flush 时,将 Map 的值转换为批量 Upsert 操作。
|
|||
|
|
- **约束**: `dev_loops` 的更新如果是针对不同回路的,可能需要累积合并。
|
|||
|
|
- **优化策略**:
|
|||
|
|
- 如果 `dev_loops` 是部分更新,我们不能简单地取最后一条消息。
|
|||
|
|
- 但是,在短时间的批处理窗口(例如 500ms)内,我们可以在内存中将它们合并后再发送给数据库。
|
|||
|
|
- 结构: `Map<Key, MergedState>`
|
|||
|
|
- 逻辑: `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. 在主循环中处理分发。
|