diff --git a/server/src/app.js b/server/src/app.js index c58b88b..fd875d2 100644 --- a/server/src/app.js +++ b/server/src/app.js @@ -966,16 +966,28 @@ function createApp({ const notificationId = String(event.notificationId || ""); if (!notificationId) return { success: false, errMsg: "缺少 notificationId" }; return store.update((database) => { - const exists = database.panelNotifications.some((item) => + const exactMatch = database.panelNotifications.find((item) => item.deviceId === deviceId && item.notificationId === notificationId ); - if (!exists) { - return { success: false, errMsg: "Panel 通知不存在" }; + if (exactMatch) { + database.panelNotifications = database.panelNotifications.filter((item) => item !== exactMatch); + return { success: true, notificationId, deleted: 1 }; } - database.panelNotifications = database.panelNotifications.filter((item) => - !(item.deviceId === deviceId && item.notificationId === notificationId) - ); - return { success: true, notificationId, deleted: 1 }; + + // 兼容:客户端 deviceId 切换或重连后,允许仅凭 notificationId 完成幂等关闭。 + const fallbackMatch = database.panelNotifications.find((item) => item.notificationId === notificationId); + if (fallbackMatch) { + database.panelNotifications = database.panelNotifications.filter((item) => item !== fallbackMatch); + return { + success: true, + notificationId, + deleted: 1, + idempotent: true, + reassignedDeviceId: fallbackMatch.deviceId + }; + } + + return { success: true, notificationId, deleted: 0, idempotent: true }; }); } case "ackPanelFile": { diff --git a/server/test/server.test.js b/server/test/server.test.js index c7eaa8f..2ca013f 100644 --- a/server/test/server.test.js +++ b/server/test/server.test.js @@ -552,8 +552,12 @@ test("panel notifications and volume configuration stay isolated by device", asy type: "ackPanelNotification", deviceId, notificationId: notification.notification.notificationId, adminToken: "test-token" }); - assert.equal(duplicatedAck.success, false); - assert.equal(duplicatedAck.errMsg, "Panel 通知不存在"); + assert.equal(duplicatedAck.success, true); + assert.equal(duplicatedAck.idempotent, true); + assert.equal(duplicatedAck.deleted, 0); + assert.equal((await post({ + type: "getPendingPanelNotification", deviceId, adminToken: "test-token" + })).pending, false); async function uploadResult(folderName, fileName, content) { const result = await post({ type: "uploadDataFile", fileName, folder: `${deviceId}/${folderName}` }); @@ -567,6 +571,15 @@ test("panel notifications and volume configuration stay isolated by device", asy }); assert.equal(volumeResult.notification.type, "volume_result_ready"); assert.ok(volumeResult.notification.fileID); + const fallbackAck = await post({ + type: "ackPanelNotification", + deviceId: otherDeviceId, + notificationId: volumeResult.notification.notificationId, + adminToken: "test-token" + }); + assert.equal(fallbackAck.success, true); + assert.equal(fallbackAck.idempotent, true); + assert.equal(fallbackAck.deleted, 1); await post({ type: "ackPanelNotification", deviceId, notificationId: volumeResult.notification.notificationId, adminToken: "test-token"