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); }