From 5ee039b0bf8aa6576026f36e63e34793fa062c21 Mon Sep 17 00:00:00 2001 From: lzh Date: Thu, 26 Feb 2026 17:13:03 +0800 Subject: [PATCH] =?UTF-8?q?feat(ops,iot):=20=E5=B7=A5=E5=8D=95=E8=AF=AD?= =?UTF-8?q?=E9=9F=B3=E6=92=AD=E6=8A=A5=E5=BE=AA=E7=8E=AF=E6=9C=BA=E5=88=B6?= =?UTF-8?q?=20+=20=E7=BB=9F=E4=B8=80=E6=8C=89=E9=94=AE=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 核心改动: - 新增循环播报机制:DISPATCHED 状态持续播报"工单来啦"直到按键确认 - 统一按键逻辑:confirmKeyId 和 queryKeyId 都路由到同一处理逻辑, 根据工单状态智能判断行为(确认/查询/无工单提示) - ARRIVED/COMPLETED 状态静默不播报,CANCELLED 保留取消播报 - 修复 P0:确认去重后按键不再静默,改为发查询事件给反馈 - 修复 P0:PAUSED 状态(P0打断)时停止被打断工单的循环播报 - 修复 P1:handleCompleted 补全 deviceId 兜底逻辑 - 修复 P1:stopLoop 只移除循环消息,保留非循环消息 Co-Authored-By: Claude Opus 4.6 --- .../processor/ButtonEventRuleProcessor.java | 88 ++++++-------- .../constants/CleanNotificationConstants.java | 10 +- .../listener/CleanOrderEventListener.java | 75 ++++++++---- .../service/voice/TtsQueueConsumer.java | 110 ++++++++++++++++++ .../service/voice/TtsQueueMessage.java | 32 +++++ .../service/voice/VoiceBroadcastService.java | 41 +++++++ 6 files changed, 281 insertions(+), 75 deletions(-) diff --git a/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/rule/clean/processor/ButtonEventRuleProcessor.java b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/rule/clean/processor/ButtonEventRuleProcessor.java index 1c3d40b..f78e028 100644 --- a/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/rule/clean/processor/ButtonEventRuleProcessor.java +++ b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/rule/clean/processor/ButtonEventRuleProcessor.java @@ -74,74 +74,62 @@ public class ButtonEventRuleProcessor { log.debug("[ButtonEvent] 按键解析成功:deviceId={}, buttonId={}", deviceId, buttonId); - // 4. 匹配按键类型并处理 - if (buttonId.equals(buttonConfig.getConfirmKeyId())) { - // 确认键 - handleConfirmButton(deviceId, buttonId); - } else if (buttonId.equals(buttonConfig.getQueryKeyId())) { - // 查询键 - handleQueryButton(deviceId, buttonId); + // 4. 匹配按键类型并处理(确认键和查询键统一路由到同一逻辑) + if (buttonId.equals(buttonConfig.getConfirmKeyId()) + || buttonId.equals(buttonConfig.getQueryKeyId())) { + // 所有已知按键统一走绿色按键逻辑(根据工单状态智能判断行为) + handleGreenButton(deviceId, buttonId); } else { log.debug("[ButtonEvent] 未配置的按键:deviceId={}, buttonId={}", deviceId, buttonId); } } /** - * 处理确认按键 + * 处理绿色按键(统一按键逻辑) *

- * 保洁员按下确认键,确认接收工单 + * 根据当前工单状态智能判断行为: + * - 无工单:发布查询事件(Ops 端播报"没有工单") + * - DISPATCHED:发布确认事件(触发确认状态转换 + 停止循环 + 播报地点) + * - CONFIRMED/ARRIVED:发布查询事件(播报地点) + * - 其他状态:发布查询事件(兜底处理) */ - private void handleConfirmButton(Long deviceId, Integer buttonId) { - - log.info("[ButtonEvent] 确认键按下:deviceId={}, buttonId={}", deviceId, buttonId); - - // 1. 查询设备当前工单 - BadgeDeviceStatusRedisDAO.OrderInfo currentOrder = badgeDeviceStatusRedisDAO.getCurrentOrder(deviceId); - if (currentOrder == null) { - log.warn("[ButtonEvent] 设备无当前工单,跳过确认:deviceId={}", deviceId); - return; - } - - Long orderId = currentOrder.getOrderId(); - - // 2. 防重复检查(短时间内同一工单的确认操作去重) - String dedupKey = String.format("iot:clean:button:dedup:confirm:%s:%s", deviceId, orderId); - Boolean firstTime = stringRedisTemplate.opsForValue() - .setIfAbsent(dedupKey, "1", 10, java.util.concurrent.TimeUnit.SECONDS); - - if (!firstTime) { - log.info("[ButtonEvent] 确认操作重复,跳过:deviceId={}, orderId={}", deviceId, orderId); - return; - } - - // 3. 发布工单确认事件 - publishConfirmEvent(deviceId, orderId, buttonId); - - log.info("[ButtonEvent] 发布工单确认事件:deviceId={}, orderId={}", deviceId, orderId); - } - - /** - * 处理查询按键 - *

- * 保洁员按下查询键,查询当前工单信息 - */ - private void handleQueryButton(Long deviceId, Integer buttonId) { - - log.info("[ButtonEvent] 查询键按下:deviceId={}, buttonId={}", deviceId, buttonId); + private void handleGreenButton(Long deviceId, Integer buttonId) { + log.info("[ButtonEvent] 绿色按键按下:deviceId={}, buttonId={}", deviceId, buttonId); // 1. 查询设备当前工单 BadgeDeviceStatusRedisDAO.OrderInfo currentOrder = badgeDeviceStatusRedisDAO.getCurrentOrder(deviceId); if (currentOrder == null) { + // 无工单 → 发布查询事件(Ops 端播报"没有工单") log.info("[ButtonEvent] 设备无当前工单:deviceId={}", deviceId); - // 发布查询结果事件(无工单) publishQueryEvent(deviceId, null, buttonId, "当前无工单"); return; } - // 2. 发布查询事件 - publishQueryEvent(deviceId, currentOrder.getOrderId(), buttonId, "查询当前工单"); + Long orderId = currentOrder.getOrderId(); + String orderStatus = currentOrder.getStatus(); - log.info("[ButtonEvent] 发布工单查询事件:deviceId={}, orderId={}", deviceId, currentOrder.getOrderId()); + // 2. 根据工单状态智能分派 + if ("DISPATCHED".equals(orderStatus)) { + // DISPATCHED → 发布确认事件(触发确认 + 停止循环 + 播报地点) + // 防重复检查 + String dedupKey = String.format("iot:clean:button:dedup:confirm:%s:%s", deviceId, orderId); + Boolean firstTime = stringRedisTemplate.opsForValue() + .setIfAbsent(dedupKey, "1", 10, java.util.concurrent.TimeUnit.SECONDS); + + if (!Boolean.TRUE.equals(firstTime)) { + // 重复确认不再静默,改为发查询事件给保洁员反馈(播报地点) + log.info("[ButtonEvent] 确认操作重复,转为查询:deviceId={}, orderId={}", deviceId, orderId); + publishQueryEvent(deviceId, orderId, buttonId, "重复确认,查询当前工单"); + return; + } + + publishConfirmEvent(deviceId, orderId, buttonId); + log.info("[ButtonEvent] DISPATCHED状态,发布确认事件:deviceId={}, orderId={}", deviceId, orderId); + } else { + // CONFIRMED / ARRIVED / 其他状态 → 发布查询事件(播报地点) + publishQueryEvent(deviceId, orderId, buttonId, "查询当前工单"); + log.info("[ButtonEvent] {}状态,发布查询事件:deviceId={}, orderId={}", orderStatus, deviceId, orderId); + } } /** diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/constants/CleanNotificationConstants.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/constants/CleanNotificationConstants.java index 3c322b0..1758d07 100644 --- a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/constants/CleanNotificationConstants.java +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/constants/CleanNotificationConstants.java @@ -74,6 +74,12 @@ public class CleanNotificationConstants { // ==================== 新工单播报 ==================== + /** + * 新工单循环播报(铃声式,类似来电提醒) + * 用于 DISPATCHED 状态的持续播报,直到保洁员按键确认 + */ + public static final String NEW_ORDER_RING = "工单来啦"; + /** * 新工单播报(简短版) */ @@ -89,7 +95,7 @@ public class CleanNotificationConstants { * 新工单播报(完整版) * 参数: {areaName} - 区域名称, {orderTitle} - 工单标题(截断) */ - public static final String NEW_ORDER_FULL = "新工单来啦,请按1键进行确认"; + public static final String NEW_ORDER_FULL = "新工单来啦,请按键进行确认"; // ==================== 工单确认播报 ==================== @@ -188,7 +194,7 @@ public class CleanNotificationConstants { /** * 按键查询播报(无工单时) */ - public static final String QUERY_NO_ORDER = "暂无待办工单"; + public static final String QUERY_NO_ORDER = "没有工单"; /** * 按键查询播报(待办数量提示) 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 1a2e894..19b3815 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 @@ -273,10 +273,16 @@ public class CleanOrderEventListener { recordOrderConfirmedLog(orderId, deviceId, event); if (deviceId != null) { - // 发送确认成功语音播报(使用统一模板) + // 1. 停止循环播报 + voiceBroadcastService.stopLoop(deviceId); + + // 2. 播报地点信息 OpsOrderDO order = opsOrderMapper.selectById(orderId); - String confirmMessage = CleanNotificationConstants.VoiceBuilder.buildOrderConfirmed(order); - playVoice(deviceId, confirmMessage, orderId); + String areaName = CleanNotificationConstants.VoiceBuilder.getAreaName( + order != null ? order.getLocation() : null); + String locationMessage = String.format( + CleanNotificationConstants.VoiceTemplate.QUERY_HAS_ORDER, areaName); + playVoice(deviceId, locationMessage, orderId); } log.info("[CleanOrderEventListener] 工单已确认: orderId={}, deviceId={}", orderId, deviceId); @@ -304,11 +310,9 @@ public class CleanOrderEventListener { // 2. 计算并更新响应时长(下发→到岗,排除暂停时间) updateResponseSeconds(orderId); - // 3. 语音播报提醒开始作业 + // 3. 停止循环播报(处理信标自动到岗 DISPATCHED→ARRIVED 场景)+ 静默(不播报) if (deviceId != null) { - OpsOrderDO order = opsOrderMapper.selectById(orderId); - String arrivedMessage = CleanNotificationConstants.VoiceBuilder.buildOrderArrived(order); - playVoice(deviceId, arrivedMessage, orderId); + voiceBroadcastService.stopLoop(deviceId); } // 4. 记录到岗业务日志 @@ -328,11 +332,26 @@ public class CleanOrderEventListener { public void handlePaused(OrderStateChangedEvent event) { Long orderId = event.getOrderId(); - // 记录暂停开始时间到扩展表 + // 1. 停止循环播报(P0打断时,被打断的工单可能正在循环播报"工单来啦") + Long deviceId = event.getPayloadLong("assigneeId"); + if (deviceId == null) { + deviceId = event.getOperatorId(); + } + if (deviceId == null) { + OpsOrderDO order = opsOrderMapper.selectById(orderId); + if (order != null) { + deviceId = order.getAssigneeId(); + } + } + if (deviceId != null) { + voiceBroadcastService.stopLoop(deviceId); + } + + // 2. 记录暂停开始时间到扩展表 recordPauseStartTime(orderId); // 设备状态由 BadgeDeviceStatusEventListener 统一处理 - log.info("[CleanOrderEventListener] 暂停时间已记录: orderId={}", orderId); + log.info("[CleanOrderEventListener] 暂停处理完成: orderId={}, deviceId={}", orderId, deviceId); } /** @@ -342,10 +361,21 @@ public class CleanOrderEventListener { public void handleCompleted(OrderStateChangedEvent event) { Long orderId = event.getOrderId(); - // 获取 deviceId(优先从 payload 获取,其次从工单获取) + // 获取 deviceId(优先从 payload 获取,其次从工单 assigneeId 兜底) Long deviceId = event.getPayloadLong("deviceId"); + if (deviceId == null) { + OpsOrderDO order = opsOrderMapper.selectById(orderId); + if (order != null) { + deviceId = order.getAssigneeId(); + } + } - // 记录完成时间并计算完成时长 + // 兜底:停止循环播报(安全调用) + if (deviceId != null) { + voiceBroadcastService.stopLoop(deviceId); + } + + // 记录完成时间并计算完成时长(静默,不播报) LocalDateTime completedTime = LocalDateTime.now(); OpsOrderCleanExtDO ext = cleanExtMapper.selectByOpsOrderId(orderId); @@ -404,8 +434,9 @@ public class CleanOrderEventListener { updateExt.setCompletedTime(LocalDateTime.now()); cleanExtMapper.insertOnDuplicateKeyUpdate(updateExt); - // 2. 语音播报通知保洁员工单已取消 + // 2. 停止循环播报 + 语音播报通知保洁员工单已取消(取消是异常情况,需要通知) if (deviceId != null) { + voiceBroadcastService.stopLoop(deviceId); playVoice(deviceId, CleanNotificationConstants.VoiceTemplate.ORDER_CANCELLED, orderId); } @@ -446,25 +477,23 @@ public class CleanOrderEventListener { } /** - * 异步执行工单完成后的通知和派单 + * 异步执行工单完成后的派单 *

- * 先发送完成通知,再立即触发下一个任务派发。 - * 语音播报顺序和间隔由 TTS 队列({@link com.viewsh.module.ops.environment.service.voice.TtsQueueConsumer}) - * 按设备维度控制,同一设备前后播报间隔由 ops.tts.queue.interval-ms 配置。 + * 完成状态静默(不播报),直接触发下一个任务派发。 + * 如果下一个任务派发成功,会触发 handleDispatched 启动新的循环播报。 */ @Async("ops-task-executor") public void asyncCompleteAndDispatchNext(Long orderId, Long deviceId) { - // 1. 发送完成通知(入TTS队列) - sendOrderCompletedNotification(orderId, deviceId); + // 静默:不发送完成通知 - // 2. 自动推送下一个任务(新任务通知也入TTS队列,由队列控制播报间隔) + // 自动推送下一个任务(新任务通知会触发 handleDispatched 启动循环播报) cleanOrderService.autoDispatchNextOrder(orderId, deviceId); } // ==================== 通知方法 ==================== /** - * 发送新工单通知(语音播报 + 站内信) + * 发送新工单通知(循环语音播报 + 站内信) */ @Async("ops-task-executor") public void sendNewOrderNotification(Long deviceId, Long orderId) { @@ -477,9 +506,9 @@ public class CleanOrderEventListener { log.info("[新工单通知] deviceId={}, orderId={}", deviceId, orderId); - // 1. 语音播报(使用统一模板构建器) - String voiceMessage = CleanNotificationConstants.VoiceBuilder.buildNewOrder(order, true); - playVoice(deviceId, voiceMessage, orderId); + // 1. 启动循环语音播报"工单来了" + voiceBroadcastService.broadcastLoop(deviceId, + CleanNotificationConstants.VoiceTemplate.NEW_ORDER_RING, orderId); // 2. 发送站内信(暂时发送到管理员) sendNotifyMessage(1L, 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 e418a46..e4c65af 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 @@ -46,6 +46,8 @@ public class TtsQueueConsumer { private static final String LOCK_KEY_PREFIX = "ops:tts:lock:"; + private static final String LOOP_KEY_PREFIX = "ops:tts:loop:"; + @Value("${ops.tts.queue.enabled:true}") private boolean queueEnabled; @@ -200,6 +202,21 @@ public class TtsQueueConsumer { deviceBroadcastLock.put(deviceId, System.currentTimeMillis()); log.info("[TTS队列] 播报成功: deviceId={}, text={}", deviceId, message.getText()); + + // 循环消息:检查 Redis 循环标记,若存在则重新入队 + if (message.isLoopable() && isLoopActive(deviceId)) { + TtsQueueMessage loopMsg = TtsQueueMessage.loopMessage( + deviceId, message.getText(), message.getOrderId()); + try { + String loopJson = objectMapper.writeValueAsString(loopMsg); + redisTemplate.opsForList().rightPush(queueKey, loopJson); + redisTemplate.expire(queueKey, 1, TimeUnit.HOURS); + log.debug("[TTS队列] 循环消息重新入队: deviceId={}", deviceId); + } catch (Exception ex) { + log.warn("[TTS队列] 循环消息重新入队失败: deviceId={}", deviceId, ex); + } + } + return true; } else { // 播报失败,释放锁(允许立即重试) @@ -335,6 +352,99 @@ public class TtsQueueConsumer { } } + // ==================== 循环播报控制 ==================== + + /** + * 启动循环播报标记 + * + * @param deviceId 设备ID + * @param orderId 工单ID + */ + public void startLoop(Long deviceId, Long orderId) { + String loopKey = LOOP_KEY_PREFIX + deviceId; + redisTemplate.opsForValue().set(loopKey, String.valueOf(orderId), 1, TimeUnit.HOURS); + log.info("[TTS队列] 启动循环播报: deviceId={}, orderId={}", deviceId, orderId); + } + + /** + * 停止循环播报 + *

+ * 删除循环标记,并从队列中移除循环消息(保留非循环消息) + * + * @param deviceId 设备ID + */ + public void stopLoop(Long deviceId) { + String loopKey = LOOP_KEY_PREFIX + deviceId; + Boolean deleted = redisTemplate.delete(loopKey); + // 从队列中移除循环消息,保留非循环消息(如取消播报、待办播报等) + int removed = removeLoopMessages(deviceId); + log.info("[TTS队列] 停止循环播报: deviceId={}, loopKeyDeleted={}, removedMessages={}", deviceId, deleted, removed); + } + + /** + * 从队列中移除所有循环消息,保留非循环消息 + * + * @param deviceId 设备ID + * @return 移除的消息数量 + */ + private int removeLoopMessages(Long deviceId) { + String queueKey = getQueueKey(deviceId); + int removedCount = 0; + + try { + Long size = redisTemplate.opsForList().size(queueKey); + if (size == null || size == 0) { + return 0; + } + + // 取出所有消息,过滤掉循环消息后重新入队 + java.util.List allMessages = new java.util.ArrayList<>(); + for (int i = 0; i < size; i++) { + Object msg = redisTemplate.opsForList().leftPop(queueKey); + if (msg == null) { + break; + } + allMessages.add(msg); + } + + // 过滤:保留非循环消息 + for (Object msgObj : allMessages) { + TtsQueueMessage message = parseMessage(msgObj); + if (message != null && message.isLoopable()) { + removedCount++; + } else { + // 非循环消息放回队列 + redisTemplate.opsForList().rightPush(queueKey, msgObj); + } + } + + // 如果队列非空,刷新 TTL + if (removedCount < allMessages.size()) { + redisTemplate.expire(queueKey, 1, TimeUnit.HOURS); + } + + } catch (Exception e) { + log.warn("[TTS队列] 移除循环消息失败,降级为清空队列: deviceId={}", deviceId, e); + // 降级:清空整个队列,确保循环一定停止 + clearQueue(deviceId); + } + + return removedCount; + } + + /** + * 检查循环播报是否激活 + * + * @param deviceId 设备ID + * @return 是否激活 + */ + public boolean isLoopActive(Long deviceId) { + String loopKey = LOOP_KEY_PREFIX + deviceId; + return Boolean.TRUE.equals(redisTemplate.hasKey(loopKey)); + } + + // ==================== 队列查询 ==================== + /** * 获取队列长度 */ 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 7cdad09..3d19f89 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 @@ -88,6 +88,11 @@ public class TtsQueueMessage implements Serializable { */ private Integer maxRetry; + /** + * 是否为循环消息(消费后重新入队,实现持续播报) + */ + private Boolean loopable; + /** * 创建按序消息(FIFO,rightPush 追加到尾部) *

@@ -126,6 +131,33 @@ public class TtsQueueMessage implements Serializable { .build(); } + /** + * 创建循环消息(消费后自动重新入队,实现持续播报) + *

+ * ttsFlag=0x09(紧急通知,带显示),priority=5(普通优先级) + * 消费后如果 Redis 中存在循环标记,则重新入队继续播报 + */ + public static TtsQueueMessage loopMessage(Long deviceId, String text, Long orderId) { + return TtsQueueMessage.builder() + .deviceId(deviceId) + .text(text) + .ttsFlag(TTS_FLAG_URGENT) + .orderId(orderId) + .priority(PRIORITY_NORMAL) + .createTime(System.currentTimeMillis()) + .retryCount(0) + .maxRetry(2) + .loopable(true) + .build(); + } + + /** + * 检查是否为循环消息 + */ + public boolean isLoopable() { + return Boolean.TRUE.equals(loopable); + } + /** * 增加重试次数 */ 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 4b3e9bd..02dbe75 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 @@ -101,6 +101,47 @@ public class VoiceBroadcastService { } } + // ==================== 循环播报模式 ==================== + + /** + * 启动循环播报(消费后自动重新入队,持续播报直到 stopLoop) + *

+ * 用于新工单推送场景:设备持续播报"工单来了"直到保洁员按键确认。 + * + * @param deviceId 设备ID + * @param text 播报文本 + * @param orderId 工单ID + */ + public void broadcastLoop(Long deviceId, String text, Long orderId) { + if (deviceId == null || text == null) { + return; + } + if (queueEnabled) { + // 设置循环标记 + ttsQueueConsumer.startLoop(deviceId, orderId); + // 入队循环消息 + enqueueOrFallback(TtsQueueMessage.loopMessage(deviceId, text, orderId)); + } else { + broadcastDirect(deviceId, text, TtsQueueMessage.TTS_FLAG_URGENT, orderId); + } + } + + /** + * 停止循环播报 + *

+ * 删除循环标记并清空该设备的播报队列。安全调用,无循环标记时也不会报错。 + * + * @param deviceId 设备ID + */ + public void stopLoop(Long deviceId) { + if (deviceId == null) { + return; + } + if (queueEnabled) { + ttsQueueConsumer.stopLoop(deviceId); + } + } + // ==================== 直接播报模式(特殊场景) ==================== /**