fix(M06-C-R1): 校正控制箱与门锁协议

This commit is contained in:
Codex
2026-08-10 16:01:38 +08:00
parent b71b48b3d0
commit dabc656f63
9 changed files with 574 additions and 86 deletions
@@ -2108,10 +2108,23 @@ async function assertIotMessages(pool, context) {
roomId: String(device.roomId),
commandId: '1782120000002',
commandType: 'AddDevice',
payload: { action: 'AddDevice', id: '1782120000002', timeout: 60 },
payload: { action: 'AddDevice', time: 60, id: '1782120000002' },
traceId: 'm06d-pair-test'
});
await service.markPublished(context.tenantId, '1782120000002');
await service.handle(`/devicesend/${device.deviceId}`, Buffer.from(JSON.stringify({
DeviceID: device.deviceId,
id: '1782120000002',
action: 'AddDevice',
result: 'ok',
timestamp: 1782120002
})));
const [pairPendingRows] = await pool.query(
`SELECT status FROM qipai_iot_commands
WHERE tenant_id = ? AND command_id = '1782120000002'`,
[context.tenantId]
);
assert.deepEqual(pairPendingRows, [{ status: 'PUBLISHED' }]);
await service.handle(`/devicesend/${device.deviceId}`, Buffer.from(JSON.stringify({
DeviceID: device.deviceId,
id: '1782120000002',
@@ -2130,6 +2143,144 @@ async function assertIotMessages(pool, context) {
[context.tenantId, device.id]
);
assert.deepEqual(linkRows, [{ subId: 'SUB-AUTO-001', subtype: '14', model: '701C' }]);
for (const [commandId, result, failureCode] of [
['1782120000003', 'busy', 'DEVICE_BUSY'],
['1782120000004', 'unconfirm', 'DEVICE_UNCONFIRM'],
['1782120000005', 'fail', 'DEVICE_FAIL']
]) {
await service.createCommand({
tenantId: context.tenantId,
assetId: String(device.id),
storeId: String(device.storeId),
roomId: String(device.roomId),
commandId,
commandType: 'task',
payload: { action: 'task', id: commandId, minute: 30, type: 1 },
traceId: `m06c-task-${result}`
});
await service.markPublished(context.tenantId, commandId);
await service.handle(`/devicesend/${device.deviceId}`, Buffer.from(JSON.stringify({
DeviceID: device.deviceId, id: commandId, action: 'task', result
})));
const [failedRows] = await pool.query(
`SELECT status, failure_code AS failureCode
FROM qipai_iot_commands WHERE tenant_id = ? AND command_id = ?`,
[context.tenantId, commandId]
);
assert.deepEqual(failedRows, [{ status: 'FAILED', failureCode }]);
}
await service.createCommand({
tenantId: context.tenantId,
assetId: String(device.id),
storeId: String(device.storeId),
roomId: String(device.roomId),
commandId: '1782120000006',
commandType: 'task',
payload: { action: 'task', id: '1782120000006', minute: 30, type: 1 },
traceId: 'm06c-unknown-result'
});
await service.markPublished(context.tenantId, '1782120000006');
await service.handle(`/devicesend/${device.deviceId}`, Buffer.from(JSON.stringify({
DeviceID: device.deviceId,
id: '1782120000006',
action: 'task',
result: 'vendor-new-code'
})));
const [unknownCommandRows] = await pool.query(
`SELECT status, failure_code AS failureCode
FROM qipai_iot_commands
WHERE tenant_id = ? AND command_id = '1782120000006'`,
[context.tenantId]
);
assert.deepEqual(unknownCommandRows, [{
status: 'FAILED', failureCode: 'UNKNOWN_VENDOR_RESULT'
}]);
const [unknownEventRows] = await pool.query(
`SELECT processing_status AS processingStatus,
JSON_UNQUOTE(JSON_EXTRACT(raw_payload, '$.result')) AS rawResult,
JSON_UNQUOTE(JSON_EXTRACT(normalized_payload, '$.result')) AS normalizedResult
FROM qipai_iot_device_events
WHERE tenant_id = ? AND command_id = '1782120000006'`,
[context.tenantId]
);
assert.deepEqual(unknownEventRows, [{
processingStatus: 'PROTOCOL_ERROR',
rawResult: 'vendor-new-code',
normalizedResult: 'UNKNOWN_VENDOR_RESULT'
}]);
const [unknownDeadRows] = await pool.query(
`SELECT error_code AS errorCode
FROM qipai_iot_dead_letters
WHERE tenant_id = ? AND topic = ? AND error_code = 'UNKNOWN_VENDOR_RESULT'`,
[context.tenantId, `/devicesend/${device.deviceId}`]
);
assert.deepEqual(unknownDeadRows, [{ errorCode: 'UNKNOWN_VENDOR_RESULT' }]);
await service.createCommand({
tenantId: context.tenantId,
assetId: String(device.id),
storeId: String(device.storeId),
roomId: String(device.roomId),
commandId: '1782120000007',
commandType: 'CtrlDevice',
payload: {
action: 'CtrlDevice', id: '1782120000007', subtype: '14', subID: 'SUB-AUTO-001',
order: 'setkey', value: '123456'
},
traceId: 'm06c-credential-redaction'
});
const [credentialRows] = await pool.query(
`SELECT JSON_UNQUOTE(JSON_EXTRACT(request_payload, '$.value')) AS value,
JSON_UNQUOTE(JSON_EXTRACT(request_payload, '$.valueHash')) AS valueHash
FROM qipai_iot_commands
WHERE tenant_id = ? AND command_id = '1782120000007'`,
[context.tenantId]
);
assert.equal(credentialRows[0].value, null);
assert.match(credentialRows[0].valueHash, /^[a-f0-9]{64}$/);
const [commandCountRows] = await pool.query(
`SELECT COUNT(*) AS total FROM qipai_iot_commands WHERE tenant_id = ?`,
[context.tenantId]
);
const commandCountBeforeEvents = Number(commandCountRows[0].total);
for (const [event, timestamp] of [['magstate', 1782120010], ['taskfinish', 1782120011]]) {
await service.handle(`/devicesend/${device.deviceId}`, Buffer.from(JSON.stringify({
DeviceID: device.deviceId, event, state: 'close', timestamp
})));
}
const [commandCountAfterRows] = await pool.query(
`SELECT COUNT(*) AS total FROM qipai_iot_commands WHERE tenant_id = ?`,
[context.tenantId]
);
assert.equal(Number(commandCountAfterRows[0].total), commandCountBeforeEvents);
const [nonAckRows] = await pool.query(
`SELECT event_type AS eventType, command_id AS commandId
FROM qipai_iot_device_events
WHERE tenant_id = ? AND event_type IN ('magstate', 'taskfinish')
ORDER BY event_type`,
[context.tenantId]
);
assert.deepEqual(nonAckRows, [
{ eventType: 'magstate', commandId: null },
{ eventType: 'taskfinish', commandId: null }
]);
await service.handle(`/devicewill/${device.deviceId}`, Buffer.from('close'));
const [offlineRows] = await pool.query(
`SELECT status, JSON_UNQUOTE(JSON_EXTRACT(state_snapshot, '$.will')) AS willValue
FROM qipai_devices WHERE tenant_id = ? AND id = ?`,
[context.tenantId, device.id]
);
assert.deepEqual(offlineRows, [{ status: 'OFFLINE', willValue: 'close' }]);
const [willDeadRows] = await pool.query(
`SELECT COUNT(*) AS total FROM qipai_iot_dead_letters
WHERE tenant_id = ? AND topic = ? AND error_code = 'MQTT_PAYLOAD_INVALID'`,
[context.tenantId, `/devicewill/${device.deviceId}`]
);
assert.equal(Number(willDeadRows[0].total), 0);
}
async function assertCleaningTaskTransactions(pool, context) {
@@ -2756,7 +2907,13 @@ try {
,
'door record credential hashing and masking',
'low-battery alert upsert',
'AddDevice ACK creates 701C parent-child topology'
'AddDevice first ACK waits for final subtype and subID',
'AddDevice final ACK creates 701C parent-child topology',
'task busy unconfirm and fail remain failed command states',
'unknown vendor result event dead-letter alert and command failure',
'CtrlDevice credential request hashing and masking',
'magstate and taskfinish remain events rather than ACKs',
'raw devicewill close marks the device offline without invalid-payload dead-letter'
,
'concurrent cleaning claim has one winner',
'cleaning state and event transaction rollback',