From 7d1012bba7d951300f8ff0ad84513237e0cef0bf Mon Sep 17 00:00:00 2001 From: lzh Date: Fri, 27 Feb 2026 13:09:56 +0800 Subject: [PATCH] =?UTF-8?q?fix(iot,ops):=20=E4=BF=AE=E5=A4=8D=E9=80=80?= =?UTF-8?q?=E5=87=BA=E6=A3=80=E6=B5=8B=E5=81=9C=E6=BB=9E=E3=80=81TTS?= =?UTF-8?q?=E5=A4=9A=E7=A7=9F=E6=88=B7=E9=87=8D=E5=A4=8D=E6=92=AD=E6=8A=A5?= =?UTF-8?q?=EF=BC=8C=E7=B2=BE=E7=AE=80=E8=AF=AD=E9=9F=B3=E9=80=9A=E7=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 蓝牙信号缺失补偿:设备属性上报不含 bluetoothDevices 时注入 null, 避免 RSSI 滑动窗口因无数据停滞导致退出检测延迟 2. TTS 多租户去重:TtsQueueMessage 携带 tenantId,processSingleQueue 过滤非当前租户消息,解决 @TenantJob 导致同一播报被不同租户重复下发 3. 循环播报日志精简:仅在 broadcastLoop 启动时记录一次 TTS_SENT, 后续重复播报不再写入 ops_business_event_log 4. 移除离岗 TTS 警告和入队语音播报,减少不必要的设备干扰 Co-Authored-By: Claude Opus 4.6 --- .../rule/clean/CleanRuleProcessorManager.java | 7 +++++++ .../processor/BeaconDetectionRuleProcessor.java | 7 +------ .../listener/CleanOrderEventListener.java | 14 ++------------ .../service/voice/TtsQueueConsumer.java | 16 ++++++++++++++-- .../service/voice/TtsQueueMessage.java | 10 ++++++++++ .../service/voice/VoiceBroadcastService.java | 4 ++++ 6 files changed, 38 insertions(+), 20 deletions(-) diff --git a/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/rule/clean/CleanRuleProcessorManager.java b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/rule/clean/CleanRuleProcessorManager.java index 74b1ace..b46274f 100644 --- a/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/rule/clean/CleanRuleProcessorManager.java +++ b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/rule/clean/CleanRuleProcessorManager.java @@ -70,6 +70,13 @@ public class CleanRuleProcessorManager { // 属性上报:直接遍历 key-value data.forEach((identifier, value) -> processDataSafely(deviceId, identifier, value)); + + // 4. 蓝牙信号缺失补偿:当设备上报了属性但不含 bluetoothDevices 时, + // 主动注入一次 null 调用,使 BeaconDetectionRuleProcessor 能写入 -999(信号缺失), + // 避免退出检测窗口因无数据而停滞 + if (!data.containsKey("bluetoothDevices")) { + beaconDetectionRuleProcessor.processPropertyChange(deviceId, "bluetoothDevices", null); + } } } diff --git a/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/rule/clean/processor/BeaconDetectionRuleProcessor.java b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/rule/clean/processor/BeaconDetectionRuleProcessor.java index 18ebcbf..04286bb 100644 --- a/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/rule/clean/processor/BeaconDetectionRuleProcessor.java +++ b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/rule/clean/processor/BeaconDetectionRuleProcessor.java @@ -226,12 +226,7 @@ public class BeaconDetectionRuleProcessor { // 首次丢失 signalLossRedisDAO.recordFirstLoss(deviceId, areaId, System.currentTimeMillis()); - // 2. 发送警告 - publishTtsEvent(deviceId, "你已离开当前区域," + - (exitConfig.getLossTimeoutMinutes() > 0 ? exitConfig.getLossTimeoutMinutes() + "分钟内工单将自动结算" - : "工单将自动结算")); - - // 3. 发布审���日志 + // 2. 发布审计日志 Map data = new HashMap<>(); data.put("firstLossTime", System.currentTimeMillis()); data.put("rssi", window.isEmpty() ? -999 : window.get(window.size() - 1)); diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/listener/CleanOrderEventListener.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/listener/CleanOrderEventListener.java index 6ee3aaa..1d4f9c4 100644 --- a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/listener/CleanOrderEventListener.java +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/listener/CleanOrderEventListener.java @@ -242,18 +242,8 @@ public class CleanOrderEventListener { } if (deviceId != null) { - try { - // 获取等待中的任务列表 - 从 MySQL 读取确保包含刚入队的任务 - var waitingTasks = orderQueueService.getWaitingTasksByUserIdFromDb(deviceId); - int queueCount = waitingTasks != null ? waitingTasks.size() : 0; - - // 发送待办增加通知 - sendQueuedOrderNotification(deviceId, queueCount, orderId); - - log.info("[CleanOrderEventListener] 入队语音播报已发送: deviceId={}, queueCount={}", deviceId, queueCount); - } catch (Exception e) { - log.warn("[CleanOrderEventListener] 播报入队语音失败: deviceId={}, orderId={}", deviceId, orderId); - } + // TODO 入队语音播报暂时关闭,后续按需开启 + log.info("[CleanOrderEventListener] 工单入队: deviceId={}, orderId={}", deviceId, orderId); } } diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/voice/TtsQueueConsumer.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/voice/TtsQueueConsumer.java index bf685a1..755c834 100644 --- a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/voice/TtsQueueConsumer.java +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/voice/TtsQueueConsumer.java @@ -8,6 +8,7 @@ import com.viewsh.module.ops.infrastructure.log.enumeration.EventLevel; import com.viewsh.module.ops.infrastructure.log.recorder.EventLogRecord; import com.viewsh.module.ops.infrastructure.log.recorder.EventLogRecorder; import cn.hutool.core.map.MapUtil; +import com.viewsh.framework.tenant.core.context.TenantContextHolder; import jakarta.annotation.PreDestroy; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; @@ -185,6 +186,17 @@ public class TtsQueueConsumer { return false; } + // 多租户过滤:@TenantJob 会为每个租户执行一次,但 TTS 队列是全局的(非租户隔离)。 + // 只有消息所属租户才应处理,避免同一消息被不同租户重复播报。 + Long currentTenantId = TenantContextHolder.getTenantId(); + if (message.getTenantId() != null && currentTenantId != null + && !message.getTenantId().equals(currentTenantId)) { + // 不是当前租户的消息,放回队列头部,释放锁让正确的租户处理 + redisTemplate.opsForList().leftPush(queueKey, messageObj); + redisTemplate.delete(lockKey); + return false; + } + // 检查消息是否过期 if (message.isExpired()) { log.info("[TTS队列] 消息已过期: deviceId={}, text={}", @@ -301,8 +313,8 @@ public class TtsQueueConsumer { iotDeviceControlApi.invokeService(reqDTO); - // 记录日志 - if (message.getOrderId() != null) { + // 记录日志(循环消息只在启动时记录一次,重复播报不再写日志) + if (message.getOrderId() != null && !message.isLoopable()) { eventLogRecorder.info("clean", EventDomain.DEVICE, "TTS_SENT", "语音播报: " + message.getText(), message.getOrderId(), message.getDeviceId(), null); } diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/voice/TtsQueueMessage.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/voice/TtsQueueMessage.java index 3d19f89..dd14174 100644 --- a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/voice/TtsQueueMessage.java +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/voice/TtsQueueMessage.java @@ -5,6 +5,8 @@ import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import com.viewsh.framework.tenant.core.context.TenantContextHolder; + import java.io.Serializable; /** @@ -93,6 +95,11 @@ public class TtsQueueMessage implements Serializable { */ private Boolean loopable; + /** + * 租户ID(用于多租户 Job 场景下匹配正确的租户上下文) + */ + private Long tenantId; + /** * 创建按序消息(FIFO,rightPush 追加到尾部) *

@@ -109,6 +116,7 @@ public class TtsQueueMessage implements Serializable { .createTime(System.currentTimeMillis()) .retryCount(0) .maxRetry(2) + .tenantId(TenantContextHolder.getTenantId()) .build(); } @@ -128,6 +136,7 @@ public class TtsQueueMessage implements Serializable { .createTime(System.currentTimeMillis()) .retryCount(0) .maxRetry(3) + .tenantId(TenantContextHolder.getTenantId()) .build(); } @@ -148,6 +157,7 @@ public class TtsQueueMessage implements Serializable { .retryCount(0) .maxRetry(2) .loopable(true) + .tenantId(TenantContextHolder.getTenantId()) .build(); } diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/voice/VoiceBroadcastService.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/voice/VoiceBroadcastService.java index 02dbe75..751d484 100644 --- a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/voice/VoiceBroadcastService.java +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/voice/VoiceBroadcastService.java @@ -121,6 +121,10 @@ public class VoiceBroadcastService { ttsQueueConsumer.startLoop(deviceId, orderId); // 入队循环消息 enqueueOrFallback(TtsQueueMessage.loopMessage(deviceId, text, orderId)); + // 记录一次循环开始日志(后续重复播报不再写日志) + if (orderId != null) { + recordLog(deviceId, text, orderId, true, null); + } } else { broadcastDirect(deviceId, text, TtsQueueMessage.TTS_FLAG_URGENT, orderId); }