117 lines
2.8 KiB
Markdown
117 lines
2.8 KiB
Markdown
|
|
# 数据库规范 (Database Specification)
|
|||
|
|
|
|||
|
|
## 1. PostgreSQL G5 连接配置
|
|||
|
|
|
|||
|
|
### 1.1 连接信息
|
|||
|
|
|
|||
|
|
| 配置项 | 值 | 说明 |
|
|||
|
|
|--------|-----|------|
|
|||
|
|
| 主机 | 10.8.8.80 | G5 数据库服务器 |
|
|||
|
|
| 端口 | 5434 | 非标准端口 |
|
|||
|
|
| 数据库 | dbv6 | 目标数据库 |
|
|||
|
|
| 用户 | (从环境变量) | POSTGRES_USER_G5 |
|
|||
|
|
| 密码 | (从环密变量) | POSTGRES_PASSWORD_G5 |
|
|||
|
|
|
|||
|
|
### 1.2 表结构定义
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
CREATE TABLE IF NOT EXISTS public.room_status_moment_g5 (
|
|||
|
|
hotel_id SMALLINT NOT NULL,
|
|||
|
|
room_id TEXT NOT NULL,
|
|||
|
|
device_id VARCHAR(255),
|
|||
|
|
ts_ms BIGINT NOT NULL,
|
|||
|
|
status SMALLINT DEFAULT 1,
|
|||
|
|
|
|||
|
|
-- 主键:确保同一房间只有一行
|
|||
|
|
PRIMARY KEY (hotel_id, room_id)
|
|||
|
|
);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 2. Upsert 操作(批量插入/更新)
|
|||
|
|
|
|||
|
|
### 2.1 SQL 语句结构
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
INSERT INTO room_status_moment_g5
|
|||
|
|
(hotel_id, room_id, device_id, ts_ms, status)
|
|||
|
|
VALUES
|
|||
|
|
($1::smallint, $2, $3, $4, 1),
|
|||
|
|
($5::smallint, $6, $7, $8, 1),
|
|||
|
|
...
|
|||
|
|
ON CONFLICT (hotel_id, room_id)
|
|||
|
|
DO UPDATE SET
|
|||
|
|
ts_ms = EXCLUDED.ts_ms,
|
|||
|
|
status = 1
|
|||
|
|
WHERE EXCLUDED.ts_ms >= room_status_moment_g5.ts_ms;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2.2 类型转换设计决策
|
|||
|
|
|
|||
|
|
| 转换 | 理由 |
|
|||
|
|
|------|------|
|
|||
|
|
| hotel_id: string → ::smallint | G5 表使用 smallint;Kafka 送字符串避免精度问题 |
|
|||
|
|
| room_id: string → text | 支持中文、特殊字符 |
|
|||
|
|
| device_id: string → varchar | 与 G5 schema 兼容 |
|
|||
|
|
| ts_ms: number → bigint | JavaScript number 足以覆盖 64-bit 整数范围 |
|
|||
|
|
|
|||
|
|
## 3. 批量处理实现
|
|||
|
|
|
|||
|
|
### 3.1 HeartbeatDbManager 类
|
|||
|
|
|
|||
|
|
位置: `src/db/heartbeatDbManager.js`
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
export class HeartbeatDbManager {
|
|||
|
|
constructor(pool) {
|
|||
|
|
this.pool = pool;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async upsertBatch(records) {
|
|||
|
|
if (!records || records.length === 0) {
|
|||
|
|
return; // 无需写入
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 构建参数化查询
|
|||
|
|
const valueClauses = [];
|
|||
|
|
const params = [];
|
|||
|
|
|
|||
|
|
records.forEach((record, idx) => {
|
|||
|
|
const baseParamIdx = idx * 4;
|
|||
|
|
valueClauses.push(
|
|||
|
|
`($${baseParamIdx + 1}::smallint, $${baseParamIdx + 2}, $${baseParamIdx + 3}, $${baseParamIdx + 4}, 1)`
|
|||
|
|
);
|
|||
|
|
params.push(
|
|||
|
|
record.hotel_id, // 字符串,::smallint 转换
|
|||
|
|
record.room_id,
|
|||
|
|
record.device_id,
|
|||
|
|
record.ts_ms
|
|||
|
|
);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const query = `
|
|||
|
|
INSERT INTO room_status_moment_g5
|
|||
|
|
(hotel_id, room_id, device_id, ts_ms, status)
|
|||
|
|
VALUES
|
|||
|
|
${valueClauses.join(',')}
|
|||
|
|
ON CONFLICT (hotel_id, room_id) DO UPDATE SET
|
|||
|
|
ts_ms = EXCLUDED.ts_ms,
|
|||
|
|
status = 1
|
|||
|
|
WHERE EXCLUDED.ts_ms >= room_status_moment_g5.ts_ms
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const result = await this.pool.query(query, params);
|
|||
|
|
logger.info(`Batch upsert: ${records.length} records, ${result.rowCount} rows affected`);
|
|||
|
|
return result;
|
|||
|
|
} catch (err) {
|
|||
|
|
logger.error(`Batch upsert failed: ${err.message}`, err);
|
|||
|
|
throw err;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
**上次修订**: 2026-03-11
|