fix(iot,ops): 修复退出检测停滞、TTS多租户重复播报,精简语音通知
Some checks failed
Java CI with Maven / build (11) (push) Has been cancelled
Java CI with Maven / build (17) (push) Has been cancelled
Java CI with Maven / build (8) (push) Has been cancelled

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 <noreply@anthropic.com>
This commit is contained in:
lzh
2026-02-27 13:09:56 +08:00
parent 7c22fe998e
commit 7d1012bba7
6 changed files with 38 additions and 20 deletions

View File

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

View File

@@ -226,12 +226,7 @@ public class BeaconDetectionRuleProcessor {
// 首次丢失
signalLossRedisDAO.recordFirstLoss(deviceId, areaId, System.currentTimeMillis());
// 2. 发送警告
publishTtsEvent(deviceId, "你已离开当前区域," +
(exitConfig.getLossTimeoutMinutes() > 0 ? exitConfig.getLossTimeoutMinutes() + "分钟内工单将自动结算"
: "工单将自动结算"));
// 3. 发布审<E5B883><E5AEA1><EFBFBD>日志
// 2. 发布审计日志
Map<String, Object> data = new HashMap<>();
data.put("firstLossTime", System.currentTimeMillis());
data.put("rssi", window.isEmpty() ? -999 : window.get(window.size() - 1));

View File

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

View File

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

View File

@@ -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;
/**
* 创建按序消息FIFOrightPush 追加到尾部)
* <p>
@@ -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();
}

View File

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