feat: 处理整数溢出并持久化无法处理的数据

- 更新 heartbeatProcessor.js 以处理所有数字溢出类型(int16、int32、int64)并使用二进制补码。
- 防止仅与数据相关的 PostgreSQL 失败抛出个别回退错误。
- 在 databaseManager.js 中添加 insertHeartbeatEventsErrors 方法以存储被拒绝的记录。
- 更新 heartbeatProcessor.js 中的 _emitRejectedRecord 方法,直接将所有无法处理的心跳数据写入 heartbeat_events_errors 数据库。
- 更新 openspec 规范以支持新的溢出和验证回退状态。
- 添加测试文件以验证大整数处理。
This commit is contained in:
2026-03-02 10:49:02 +08:00
parent 58c3880732
commit d0c4940e01
7 changed files with 281 additions and 103 deletions

View File

@@ -22,18 +22,18 @@ class DatabaseManager {
idleTimeoutMillis: this.config.idleTimeoutMillis,
connectionTimeoutMillis: 5000, // 5秒连接超时防止断网时无限等待
});
// 监听连接池错误,防止后端断开导致进程崩溃
this.pool.on('error', (err, client) => {
console.error('[db] 发生未捕获的连接池错误:', err);
// 不抛出,让应用层通过心跳检测发现问题
});
// 测试连接
const client = await this.pool.connect();
client.release();
console.log('数据库连接池创建成功');
// 初始化表结构
await this.initTables();
@@ -570,15 +570,55 @@ class DatabaseManager {
await this.pool.query(singleSql, toRowValues(event));
insertedCount += 1;
} catch (error) {
const connCodes = ['57P03', '08006', '08001', '08003', '08004', '08007'];
if (connCodes.includes(error?.code) || (error?.message && /ECONNREFUSED|ETIMEDOUT|connection/i.test(error.message))) {
throw error;
}
failedRecords.push({ error, record: event });
}
}
if (insertedCount === 0 && failedRecords.length === events.length) {
throw lastError;
// 只有在确定是纯网络断开时(所有单独重试都触发同一级别的连接错误),才向外抛出网络错误以重试批次。
const connCodes = ['57P03', '08006', '08001', '08003', '08004', '08007'];
const isConnError = connCodes.includes(lastError?.code) ||
(lastError?.message && /ECONNREFUSED|ETIMEDOUT|connection/i.test(lastError.message));
if (isConnError) {
throw lastError;
}
}
return { insertedCount, failedRecords, batchError: lastError };
return { insertedCount, failedRecords, batchError: (insertedCount === 0 && failedRecords.length === events.length) ? lastError : null };
}
async insertHeartbeatEventsErrors(errorsList) {
if (!errorsList || errorsList.length === 0) return;
try {
const placeholders = [];
const values = [];
let i = 1;
for (const err of errorsList) {
let hotel_id = Number(err.hotel_id);
if (!Number.isFinite(hotel_id)) hotel_id = null;
let room_id = Number(err.room_id);
if (!Number.isFinite(room_id)) room_id = null;
let original_data = null;
try { original_data = JSON.stringify(err.original_data); } catch { /* ignore */ }
const error_code = String(err.error_code || '');
const error_message = String(err.error_message || '');
placeholders.push(`($${i}, $${i + 1}, $${i + 2}, $${i + 3}, $${i + 4})`);
values.push(hotel_id, room_id, original_data, error_code, error_message);
i += 5;
}
const sql = `INSERT INTO heartbeat.heartbeat_events_errors (hotel_id, room_id, original_data, error_code, error_message) VALUES ${placeholders.join(', ')}`;
await this.pool.query(sql, values);
} catch (e) {
console.warn('Failed to insert into heartbeat_events_errors:', e);
}
}
async insertHeartbeatData(data) {
@@ -586,11 +626,11 @@ class DatabaseManager {
if (!Array.isArray(data)) {
data = [data];
}
if (data.length === 0) {
return;
}
// 构建批量插入语句
const values = data.map(item => [
item.component_id,
@@ -598,7 +638,7 @@ class DatabaseManager {
item.timestamp,
item.data
]);
const query = {
text: `
INSERT INTO heartbeat (component_id, status, timestamp, data)
@@ -606,7 +646,7 @@ class DatabaseManager {
`,
values: values.flat()
};
const res = await this.pool.query(query);
console.log(`成功插入 ${data.length} 条心跳数据`);
return { insertedCount: Number(res?.rowCount ?? data.length) };
@@ -706,11 +746,11 @@ class DatabaseManager {
// 使用 IS DISTINCT FROM 避免无意义更新
const updateColumns = columns.filter(c => !['hotel_id', 'room_id', 'device_id'].includes(c));
const updateSet = updateColumns.map(col => `${col} = EXCLUDED.${col}`).join(', ');
// 构建 WHERE 子句:仅当至少一个字段发生变化,且时间戳未回退时才更新
// 注意room_status.room_status_moment.ts_ms 是 bigintEXCLUDED.ts_ms 也是 bigint
const whereConditions = updateColumns.map(col => `room_status.room_status_moment.${col} IS DISTINCT FROM EXCLUDED.${col}`).join(' OR ');
// 生成批量插入 SQL
// 注意ON CONFLICT (hotel_id, room_id, device_id) 依赖于唯一索引 idx_room_status_unique_device
const values = [];
@@ -753,7 +793,7 @@ class DatabaseManager {
}
}
}
// 不抛出错误只记录日志避免影响主流程Heartbeat History 写入已成功)
console.warn('[db] upsertRoomStatus failed:', error.message);
return { error };
@@ -804,7 +844,7 @@ class DatabaseManager {
`,
values: [componentId]
};
const result = await this.pool.query(query);
return result.rows[0] || null;
} catch (error) {
@@ -824,7 +864,7 @@ class DatabaseManager {
`,
values: [componentId, startTime, endTime]
};
const result = await this.pool.query(query);
return result.rows;
} catch (error) {