feat(heartbeat): 添加版本号字段并处理亮度值-1为NULL

- 在心跳事件表中新增 version 字段,用于存储版本号信息
- 将 bright_g 字段的 -1 值映射为数据库中的 NULL,避免语义混淆
- 更新相关文档、数据库迁移脚本和测试用例
This commit is contained in:
2026-01-28 17:47:05 +08:00
parent 1644ee80bc
commit ad270bd936
6 changed files with 45 additions and 4 deletions

View File

@@ -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[] | 否 | 电流数组 |

View File

@@ -40,7 +40,8 @@
| electricity | array<object> | [{"address":"add11","voltage":3.2,...}] | 电力设备数组(按原始顺序拆列落库为数组列) |
| air_conditioner | array<object> | [{"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",

View File

@@ -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);

View File

@@ -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,

View File

@@ -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'

View File

@@ -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', () => {