feat(M06-C): 完成设备协议适配与消息幂等
This commit is contained in:
@@ -36,6 +36,7 @@ import { WechatPayClient } from '../dist/payments/wechat-pay-client.js';
|
||||
import { ThirdPartyClient } from '../dist/third-party/third-party-client.js';
|
||||
import { ThirdPartyService } from '../dist/third-party/third-party-service.js';
|
||||
import { DeviceRepository } from '../dist/devices/device-repository.js';
|
||||
import { IotMessageService } from '../dist/devices/iot-message-service.js';
|
||||
import {
|
||||
executeMigrationPlan,
|
||||
loadMigrationPlan,
|
||||
@@ -58,6 +59,9 @@ const expectedTables = [
|
||||
'qipai_group_redemptions',
|
||||
'qipai_group_vouchers',
|
||||
'qipai_holiday_calendar',
|
||||
'qipai_iot_commands',
|
||||
'qipai_iot_dead_letters',
|
||||
'qipai_iot_device_events',
|
||||
'qipai_legacy_table_mappings',
|
||||
'qipai_media_assets',
|
||||
'qipai_members',
|
||||
@@ -121,12 +125,13 @@ async function readMigrationVersions(pool) {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT version, name
|
||||
FROM qipai_schema_migrations
|
||||
WHERE version IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
WHERE version IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ORDER BY version`,
|
||||
['2026061601', '2026061802', '2026061803', '2026061804',
|
||||
'2026061805', '2026061806', '2026061807', '2026061808', '2026061809',
|
||||
'2026061810', '2026061811', '2026062012', '2026062013', '2026062014',
|
||||
'2026062015', '2026062216', '2026062217', '2026062218', '2026062219']
|
||||
'2026062015', '2026062216', '2026062217', '2026062218', '2026062219',
|
||||
'2026062220']
|
||||
);
|
||||
return rows;
|
||||
}
|
||||
@@ -1670,6 +1675,60 @@ async function assertDeviceTopology(pool, context) {
|
||||
}]);
|
||||
}
|
||||
|
||||
async function assertIotMessages(pool, context) {
|
||||
const [deviceRows] = await pool.query(
|
||||
`SELECT id, store_id AS storeId, room_id AS roomId, device_id AS deviceId
|
||||
FROM qipai_devices
|
||||
WHERE tenant_id = ? AND device_id = 'M06B_BOX_001'`,
|
||||
[context.tenantId]
|
||||
);
|
||||
const device = deviceRows[0];
|
||||
const service = new IotMessageService(pool);
|
||||
await service.createCommand({
|
||||
tenantId: context.tenantId,
|
||||
assetId: String(device.id),
|
||||
storeId: String(device.storeId),
|
||||
roomId: String(device.roomId),
|
||||
commandId: '1782120000001',
|
||||
commandType: 'ConctolPower',
|
||||
payload: {
|
||||
action: 'ConctolPower', id: '1782120000001', slot1: 'on'
|
||||
},
|
||||
traceId: 'm06c-live-test'
|
||||
});
|
||||
assert.equal(await service.markPublished(context.tenantId, '1782120000001'), true);
|
||||
const payload = Buffer.from(JSON.stringify({
|
||||
DeviceID: device.deviceId,
|
||||
id: '1782120000001',
|
||||
action: 'ConctolPower',
|
||||
result: 'ok',
|
||||
slot1: 'on',
|
||||
timestamp: 1782120000
|
||||
}));
|
||||
await service.handle(`/devicesend/${device.deviceId}`, payload);
|
||||
await service.handle(`/devicesend/${device.deviceId}`, payload);
|
||||
const [commandRows] = await pool.query(
|
||||
`SELECT status, failure_code AS failureCode
|
||||
FROM qipai_iot_commands
|
||||
WHERE tenant_id = ? AND command_id = '1782120000001'`,
|
||||
[context.tenantId]
|
||||
);
|
||||
assert.deepEqual(commandRows, [{ status: 'ACKED', failureCode: '' }]);
|
||||
const [eventRows] = await pool.query(
|
||||
`SELECT receive_count AS receiveCount, processing_status AS processingStatus
|
||||
FROM qipai_iot_device_events
|
||||
WHERE tenant_id = ? AND command_id = '1782120000001'`,
|
||||
[context.tenantId]
|
||||
);
|
||||
assert.deepEqual(eventRows, [{ receiveCount: 2, processingStatus: 'PROCESSED' }]);
|
||||
await service.handle('/invalid/topic', Buffer.from('{bad-json'));
|
||||
const [deadRows] = await pool.query(
|
||||
`SELECT error_code AS errorCode, receive_count AS receiveCount
|
||||
FROM qipai_iot_dead_letters WHERE topic = '/invalid/topic'`
|
||||
);
|
||||
assert.deepEqual(deadRows, [{ errorCode: 'MQTT_TOPIC_INVALID', receiveCount: 1 }]);
|
||||
}
|
||||
|
||||
const config = loadConfig();
|
||||
assert.equal(config.mysql.passwordConfigured, true, 'Live migration test requires a temporary password.');
|
||||
assert.match(
|
||||
@@ -1712,7 +1771,8 @@ try {
|
||||
{ version: '2026062216', name: 'm05b_wechat_refunds' },
|
||||
{ version: '2026062217', name: 'm05c_third_party' },
|
||||
{ version: '2026062218', name: 'm05d_profit_sharing' },
|
||||
{ version: '2026062219', name: 'm06b_device_topology' }
|
||||
{ version: '2026062219', name: 'm06b_device_topology' },
|
||||
{ version: '2026062220', name: 'm06c_iot_messages' }
|
||||
]);
|
||||
await assertTaskDurability(pool);
|
||||
const loginContext = await assertPlatformTenantIsolation(pool);
|
||||
@@ -1730,13 +1790,14 @@ try {
|
||||
await assertThirdPartyDomain(pool, loginContext);
|
||||
await assertProfitSharingDomain(pool, loginContext);
|
||||
await assertDeviceTopology(pool, loginContext);
|
||||
await assertIotMessages(pool, loginContext);
|
||||
await assertLegacyCompatibility(pool);
|
||||
console.log('PASS: first up, verify, tenant isolation and revocable auth checks completed.');
|
||||
|
||||
await executeMigrationPlan(pool, plans.down);
|
||||
assert.deepEqual(await readCoreTables(pool), []);
|
||||
await assertLegacyCompatibility(pool);
|
||||
console.log('PASS: down removed all M01-B through M06-B tables.');
|
||||
console.log('PASS: down removed all M01-B through M06-C tables.');
|
||||
|
||||
await executeMigrationPlan(pool, plans.up);
|
||||
await executeMigrationPlan(pool, plans.verify);
|
||||
@@ -1760,7 +1821,8 @@ try {
|
||||
{ version: '2026062216', name: 'm05b_wechat_refunds' },
|
||||
{ version: '2026062217', name: 'm05c_third_party' },
|
||||
{ version: '2026062218', name: 'm05d_profit_sharing' },
|
||||
{ version: '2026062219', name: 'm06b_device_topology' }
|
||||
{ version: '2026062219', name: 'm06b_device_topology' },
|
||||
{ version: '2026062220', name: 'm06c_iot_messages' }
|
||||
]);
|
||||
await assertLegacyCompatibility(pool);
|
||||
console.log('PASS: second up and verify restored the schema.');
|
||||
@@ -1858,6 +1920,11 @@ try {
|
||||
'control target conflict across control box and smart socket',
|
||||
'Sub-1G parent-child topology',
|
||||
'device status snapshots and maintenance state'
|
||||
,
|
||||
'13-digit IoT command state transition',
|
||||
'QoS 1 duplicate event receive count',
|
||||
'ACK correlation without duplicate side effects',
|
||||
'invalid Topic dead-letter persistence'
|
||||
]
|
||||
}, null, 2));
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user