From ad270bd936d719f416ae8cb05a12f6064a5551dd Mon Sep 17 00:00:00 2001 From: XuJiacheng Date: Wed, 28 Jan 2026 17:47:05 +0800 Subject: [PATCH] =?UTF-8?q?feat(heartbeat):=20=E6=B7=BB=E5=8A=A0=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E5=8F=B7=E5=AD=97=E6=AE=B5=E5=B9=B6=E5=A4=84=E7=90=86?= =?UTF-8?q?=E4=BA=AE=E5=BA=A6=E5=80=BC-1=E4=B8=BANULL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在心跳事件表中新增 version 字段,用于存储版本号信息 - 将 bright_g 字段的 -1 值映射为数据库中的 NULL,避免语义混淆 - 更新相关文档、数据库迁移脚本和测试用例 --- docs/db-heartbeat-schema.md | 3 ++- docs/kafka-heartbeat-producer.md | 4 +++- scripts/db/010_heartbeat_schema.sql | 2 ++ src/db/databaseManager.js | 4 +++- src/processor/heartbeatProcessor.js | 6 +++++- test/smoke.test.js | 30 +++++++++++++++++++++++++++++ 6 files changed, 45 insertions(+), 4 deletions(-) diff --git a/docs/db-heartbeat-schema.md b/docs/db-heartbeat-schema.md index 040dcd3..d5e6c9d 100644 --- a/docs/db-heartbeat-schema.md +++ b/docs/db-heartbeat-schema.md @@ -34,7 +34,8 @@ | device_count | int2 | 是 | 设备数量/上报设备数量(语义待确认) | | comm_seq | int4 | 是 | 通讯序号(语义待确认) | | insert_card | int2 | 否 | 是否插卡(整数;可为空;不建索引) | -| bright_g | int2 | 否 | 全局亮度值(整数;可为空;不建索引) | +| bright_g | int2 | 否 | 全局亮度值(整数;可为空;若值为 -1 则存 NULL;不建索引) | +| version | int2 | 否 | 版本号(int2;可为空;不建索引) | | elec_address | text[] | 否 | 电力设备地址数组(与 voltage[] 等按下标对齐) | | voltage | double precision[] | 否 | 电压数组 | | ampere | double precision[] | 否 | 电流数组 | diff --git a/docs/kafka-heartbeat-producer.md b/docs/kafka-heartbeat-producer.md index bc6844a..27a4109 100644 --- a/docs/kafka-heartbeat-producer.md +++ b/docs/kafka-heartbeat-producer.md @@ -40,7 +40,8 @@ | electricity | array | [{"address":"add11","voltage":3.2,...}] | 电力设备数组(按原始顺序拆列落库为数组列) | | air_conditioner | array | [{"address":"ac1","state":1,...}] | 空调设备数组(按原始顺序拆列落库为数组列) | | insert_card | number/int | 1 | 是否插卡(整数,可为空) | -| bright_g | number/int | 80 | 全局亮度值(整数,可为空) | +| bright_g | number/int | 80 | 全局亮度值(整数,可为空;若为 -1 则落库为 null) | +| version | number/int | 1 | 版本号(int2/short,可为空) | ## 4. JSON 示例 ```json @@ -60,6 +61,7 @@ "comm_seq": 7, "insert_card": 1, "bright_g": 80, + "version": "1.3.0", "electricity": [ { "address": "add11", diff --git a/scripts/db/010_heartbeat_schema.sql b/scripts/db/010_heartbeat_schema.sql index a9057f3..544d76e 100644 --- a/scripts/db/010_heartbeat_schema.sql +++ b/scripts/db/010_heartbeat_schema.sql @@ -30,6 +30,7 @@ CREATE TABLE IF NOT EXISTS heartbeat.heartbeat_events ( insert_card int2, bright_g int2, + version int4, elec_address text[], air_address text[], @@ -85,6 +86,7 @@ ALTER TABLE heartbeat.heartbeat_events ADD COLUMN IF NOT EXISTS now_temp int2[]; ALTER TABLE heartbeat.heartbeat_events ADD COLUMN IF NOT EXISTS solenoid_valve int2[]; ALTER TABLE heartbeat.heartbeat_events ADD COLUMN IF NOT EXISTS insert_card int2; ALTER TABLE heartbeat.heartbeat_events ADD COLUMN IF NOT EXISTS bright_g int2; +ALTER TABLE heartbeat.heartbeat_events ADD COLUMN IF NOT EXISTS version int2; -- 指定索引 CREATE INDEX IF NOT EXISTS idx_heartbeat_events_hotel_id ON heartbeat.heartbeat_events (hotel_id); diff --git a/src/db/databaseManager.js b/src/db/databaseManager.js index 33d6e90..71eda82 100644 --- a/src/db/databaseManager.js +++ b/src/db/databaseManager.js @@ -420,6 +420,7 @@ class DatabaseManager { 'comm_seq', 'insert_card', 'bright_g', + 'version', 'elec_address', 'air_address', 'voltage', @@ -453,7 +454,8 @@ class DatabaseManager { e.device_count, e.comm_seq, e.insert_card ?? null, - e.bright_g ?? null, + (e.bright_g === -1 || e.bright_g === '-1') ? null : (e.bright_g ?? null), + e.version ?? null, Array.isArray(e.elec_address) ? e.elec_address : null, Array.isArray(e.air_address) ? e.air_address : null, Array.isArray(e.voltage) ? e.voltage : null, diff --git a/src/processor/heartbeatProcessor.js b/src/processor/heartbeatProcessor.js index ce1467f..e44792d 100644 --- a/src/processor/heartbeatProcessor.js +++ b/src/processor/heartbeatProcessor.js @@ -514,6 +514,7 @@ class HeartbeatProcessor { comm_seq: pick(['comm_seq', 'commSeq', 'CommSeq']), insert_card: pick(['insert_card', 'insertCard', 'InsertCard']), bright_g: pick(['bright_g', 'brightG', 'BrightG']), + version: pick(['version', 'Version', 'ver', 'Ver']), extra: pick(['extra', 'Extra']), electricity: pick(['electricity', 'Electricity']), air_conditioner: pick(['air_conditioner', 'airConditioner', 'AirConditioner']), @@ -566,7 +567,9 @@ class HeartbeatProcessor { normalized.device_count = toIntOrUndefined(normalized.device_count); normalized.comm_seq = toIntOrUndefined(normalized.comm_seq); normalized.insert_card = toIntOrUndefined(normalized.insert_card); - normalized.bright_g = toIntOrUndefined(normalized.bright_g); + const bg = toIntOrUndefined(normalized.bright_g); + normalized.bright_g = bg === -1 ? undefined : bg; + normalized.version = toIntOrUndefined(normalized.version); // 其余未知字段塞进 extra(避免丢信息),但不覆盖显式 extra if (!normalized.extra || typeof normalized.extra !== 'object') { @@ -591,6 +594,7 @@ class HeartbeatProcessor { 'comm_seq','commSeq','CommSeq', 'insert_card','insertCard','InsertCard', 'bright_g','brightG','BrightG', + 'version','Version','ver','Ver', 'extra','Extra', 'electricity','Electricity', 'air_conditioner','airConditioner','AirConditioner' diff --git a/test/smoke.test.js b/test/smoke.test.js index e95a31a..0d37716 100644 --- a/test/smoke.test.js +++ b/test/smoke.test.js @@ -24,6 +24,36 @@ describe('HeartbeatProcessor smoke', () => { const payload = { tsMs: 1700000000123, hotelId: 1, roomId: 2, deviceId: 'd', ip: '127.0.0.1', powerState: 1, guestType: 0, cardlessState: 0, serviceMask: 1, pmsState: 1, carbonState: 0, deviceCount: 1, commSeq: 1 }; assert.equal(processor.validateData(payload), true); }); + + it('parses version field', () => { + const processor = new HeartbeatProcessor( + { batchSize: 100, batchTimeout: 1000 }, + { insertHeartbeatEvents: async () => {} } + ); + const payload = { version: 10203 }; + const normalized = processor.normalizeHeartbeat(payload); + assert.equal(normalized.version, 10203); + }); + + it('treats bright_g -1 as undefined', () => { + const processor = new HeartbeatProcessor( + { batchSize: 100, batchTimeout: 1000 }, + { insertHeartbeatEvents: async () => {} } + ); + const payload = { bright_g: -1 }; + const normalized = processor.normalizeHeartbeat(payload); + assert.equal(normalized.bright_g, undefined); + }); + + it('treats bright_g normal value as number', () => { + const processor = new HeartbeatProcessor( + { batchSize: 100, batchTimeout: 1000 }, + { insertHeartbeatEvents: async () => {} } + ); + const payload = { bright_g: 50 }; + const normalized = processor.normalizeHeartbeat(payload); + assert.equal(normalized.bright_g, 50); + }); }); describe('RedisIntegration protocol', () => {