feat: 为 G5 room_status 写入添加强制 online_status 为 1 的功能,并确保冲突时始终更新,即使业务字段未变化
This commit is contained in:
@@ -61,3 +61,14 @@
|
|||||||
- **WHEN** 系统写入 `room_status.room_status_moment_g5`
|
- **WHEN** 系统写入 `room_status.room_status_moment_g5`
|
||||||
- **THEN** 应使用 `(hotel_id, room_id)` 作为 `ON CONFLICT` 的冲突键
|
- **THEN** 应使用 `(hotel_id, room_id)` 作为 `ON CONFLICT` 的冲突键
|
||||||
- **AND** 应将 `device_id` 作为普通可更新列写入
|
- **AND** 应将 `device_id` 作为普通可更新列写入
|
||||||
|
|
||||||
|
#### Scenario: G5 room_status 写入时强制 online_status 为 1
|
||||||
|
- **WHEN** 系统向 `room_status.room_status_moment_g5` 插入新行或因冲突执行更新
|
||||||
|
- **THEN** 应将 `online_status` 写为 `1`
|
||||||
|
- **AND** 该规则仅作用于 G5 room_status 路径
|
||||||
|
|
||||||
|
#### Scenario: G5 room_status 每包数据都触发更新时间刷新
|
||||||
|
- **WHEN** 系统收到任意一包属于同一 `(hotel_id, room_id)` 的 G5 room_status 数据
|
||||||
|
- **THEN** 即使业务字段与现存记录完全一致,也应执行一次 `ON CONFLICT DO UPDATE`
|
||||||
|
- **AND** 不应在 G5 room_status 的冲突更新路径上使用“字段无变化则跳过 UPDATE”的 WHERE 条件
|
||||||
|
- **AND** 应依赖数据库触发器自动刷新 `ts_ms`
|
||||||
@@ -785,7 +785,7 @@ class DatabaseManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_buildRoomStatusUpsertQuery(events, target) {
|
_buildRoomStatusUpsertQuery(events, target) {
|
||||||
const { tableName, conflictColumns, includeGuid, tableRef } = target;
|
const { tableName, conflictColumns, includeGuid, tableRef, forceOnlineStatusOnWrite, forceUpdateOnConflict } = target;
|
||||||
const columns = this._getRoomStatusBaseColumns();
|
const columns = this._getRoomStatusBaseColumns();
|
||||||
const uniqueEventsMap = new Map();
|
const uniqueEventsMap = new Map();
|
||||||
|
|
||||||
@@ -804,29 +804,43 @@ class DatabaseManager {
|
|||||||
return { sql: null, values: [], uniqueEvents: [] };
|
return { sql: null, values: [], uniqueEvents: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
const allColumns = includeGuid ? [...columns, 'guid'] : [...columns];
|
const allColumns = forceOnlineStatusOnWrite ? [...columns, 'online_status'] : [...columns];
|
||||||
|
if (includeGuid) {
|
||||||
|
allColumns.push('guid');
|
||||||
|
}
|
||||||
const updateColumns = columns.filter((column) => !['ts_ms', ...conflictColumns].includes(column));
|
const updateColumns = columns.filter((column) => !['ts_ms', ...conflictColumns].includes(column));
|
||||||
const updateSet = updateColumns.map((column) => `${column} = EXCLUDED.${column}`).join(', ');
|
const updateAssignments = updateColumns.map((column) => `${column} = EXCLUDED.${column}`);
|
||||||
|
if (forceOnlineStatusOnWrite) {
|
||||||
|
updateAssignments.push('online_status = 1');
|
||||||
|
}
|
||||||
|
const updateSet = updateAssignments.join(', ');
|
||||||
const whereConditions = updateColumns.map((column) => `${tableRef}.${column} IS DISTINCT FROM EXCLUDED.${column}`).join(' OR ');
|
const whereConditions = updateColumns.map((column) => `${tableRef}.${column} IS DISTINCT FROM EXCLUDED.${column}`).join(' OR ');
|
||||||
|
|
||||||
const values = [];
|
const values = [];
|
||||||
const placeholders = uniqueEvents.map((event, eventIndex) => {
|
const placeholders = uniqueEvents.map((event, eventIndex) => {
|
||||||
const rowValues = this._roomStatusToRowValues(event);
|
const rowValues = this._roomStatusToRowValues(event);
|
||||||
|
if (forceOnlineStatusOnWrite) {
|
||||||
|
rowValues.push(1);
|
||||||
|
}
|
||||||
values.push(...rowValues);
|
values.push(...rowValues);
|
||||||
const start = eventIndex * rowValues.length;
|
const start = eventIndex * rowValues.length;
|
||||||
const params = rowValues.map((_, valueIndex) => `$${start + valueIndex + 1}`).join(', ');
|
const params = rowValues.map((_, valueIndex) => `$${start + valueIndex + 1}`).join(', ');
|
||||||
return includeGuid ? `(${params}, gen_random_uuid())` : `(${params})`;
|
return includeGuid ? `(${params}, gen_random_uuid())` : `(${params})`;
|
||||||
}).join(', ');
|
}).join(', ');
|
||||||
|
|
||||||
|
const whereClause = forceUpdateOnConflict
|
||||||
|
? ''
|
||||||
|
: `
|
||||||
|
WHERE
|
||||||
|
${tableRef}.ts_ms <= EXCLUDED.ts_ms
|
||||||
|
AND (${whereConditions})`;
|
||||||
|
|
||||||
const sql = `
|
const sql = `
|
||||||
INSERT INTO ${tableName} (${allColumns.join(', ')})
|
INSERT INTO ${tableName} (${allColumns.join(', ')})
|
||||||
VALUES ${placeholders}
|
VALUES ${placeholders}
|
||||||
ON CONFLICT (${conflictColumns.join(', ')})
|
ON CONFLICT (${conflictColumns.join(', ')})
|
||||||
DO UPDATE SET
|
DO UPDATE SET
|
||||||
${updateSet}
|
${updateSet}${whereClause}
|
||||||
WHERE
|
|
||||||
${tableRef}.ts_ms <= EXCLUDED.ts_ms
|
|
||||||
AND (${whereConditions})
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
return { sql, values, uniqueEvents };
|
return { sql, values, uniqueEvents };
|
||||||
@@ -876,6 +890,8 @@ class DatabaseManager {
|
|||||||
includeGuid: true,
|
includeGuid: true,
|
||||||
autoCreatePartitions: true,
|
autoCreatePartitions: true,
|
||||||
tableRef: this.config.roomStatusTable ?? 'room_status.room_status_moment',
|
tableRef: this.config.roomStatusTable ?? 'room_status.room_status_moment',
|
||||||
|
forceOnlineStatusOnWrite: false,
|
||||||
|
forceUpdateOnConflict: false,
|
||||||
logPrefix: 'upsertRoomStatus',
|
logPrefix: 'upsertRoomStatus',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -887,6 +903,8 @@ class DatabaseManager {
|
|||||||
includeGuid: false,
|
includeGuid: false,
|
||||||
autoCreatePartitions: false,
|
autoCreatePartitions: false,
|
||||||
tableRef: this.config.roomStatusTable ?? 'room_status.room_status_moment_g5',
|
tableRef: this.config.roomStatusTable ?? 'room_status.room_status_moment_g5',
|
||||||
|
forceOnlineStatusOnWrite: true,
|
||||||
|
forceUpdateOnConflict: true,
|
||||||
logPrefix: 'upsertRoomStatusG5',
|
logPrefix: 'upsertRoomStatusG5',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -589,12 +589,47 @@ describe('DatabaseManager: room_status upsert SQL', () => {
|
|||||||
includeGuid: false,
|
includeGuid: false,
|
||||||
autoCreatePartitions: false,
|
autoCreatePartitions: false,
|
||||||
tableRef: 'room_status.room_status_moment_g5',
|
tableRef: 'room_status.room_status_moment_g5',
|
||||||
|
forceOnlineStatusOnWrite: true,
|
||||||
|
forceUpdateOnConflict: true,
|
||||||
logPrefix: 'upsertRoomStatusG5',
|
logPrefix: 'upsertRoomStatusG5',
|
||||||
});
|
});
|
||||||
assert.match(built.sql, /ON CONFLICT \(hotel_id, room_id\)/);
|
assert.match(built.sql, /ON CONFLICT \(hotel_id, room_id\)/);
|
||||||
assert.doesNotMatch(built.sql, /ts_ms = EXCLUDED\.ts_ms/);
|
assert.doesNotMatch(built.sql, /ts_ms = EXCLUDED\.ts_ms/);
|
||||||
assert.equal(/guid/.test(built.sql), false);
|
assert.equal(/guid/.test(built.sql), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('forces online_status to 1 for g5 room_status insert and update', () => {
|
||||||
|
const dm = new DatabaseManager({ host: 'x', port: 5432, user: 'x', password: 'x', database: 'x', roomStatusTable: 'room_status.room_status_moment_g5' });
|
||||||
|
const built = dm._buildRoomStatusUpsertQuery([buildBasePayload()], {
|
||||||
|
tableName: 'room_status.room_status_moment_g5',
|
||||||
|
conflictColumns: ['hotel_id', 'room_id'],
|
||||||
|
includeGuid: false,
|
||||||
|
autoCreatePartitions: false,
|
||||||
|
tableRef: 'room_status.room_status_moment_g5',
|
||||||
|
forceOnlineStatusOnWrite: true,
|
||||||
|
forceUpdateOnConflict: true,
|
||||||
|
logPrefix: 'upsertRoomStatusG5',
|
||||||
|
});
|
||||||
|
assert.match(built.sql, /INSERT INTO room_status\.room_status_moment_g5 \(.*online_status\)/s);
|
||||||
|
assert.match(built.sql, /online_status = 1/);
|
||||||
|
assert.equal(built.values[built.values.length - 1], 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('always updates g5 room_status on conflict even when business fields are unchanged', () => {
|
||||||
|
const dm = new DatabaseManager({ host: 'x', port: 5432, user: 'x', password: 'x', database: 'x', roomStatusTable: 'room_status.room_status_moment_g5' });
|
||||||
|
const built = dm._buildRoomStatusUpsertQuery([buildBasePayload()], {
|
||||||
|
tableName: 'room_status.room_status_moment_g5',
|
||||||
|
conflictColumns: ['hotel_id', 'room_id'],
|
||||||
|
includeGuid: false,
|
||||||
|
autoCreatePartitions: false,
|
||||||
|
tableRef: 'room_status.room_status_moment_g5',
|
||||||
|
forceOnlineStatusOnWrite: true,
|
||||||
|
forceUpdateOnConflict: true,
|
||||||
|
logPrefix: 'upsertRoomStatusG5',
|
||||||
|
});
|
||||||
|
assert.doesNotMatch(built.sql, /room_status\.room_status_moment_g5\.ts_ms <= EXCLUDED\.ts_ms/);
|
||||||
|
assert.doesNotMatch(built.sql, /IS DISTINCT FROM EXCLUDED/);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('DatabaseManager: insertHeartbeatEventsDual', () => {
|
describe('DatabaseManager: insertHeartbeatEventsDual', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user