Files
Web_BLS_RCUAction_Server/openspec/changes/2026-02-06-room-status-moment/spec.md

116 lines
5.7 KiB
Markdown
Raw Normal View History

# 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. 在主循环中处理分发。