From 0c91cbf75c19b21a54d9b838ef26841ded04ad8f Mon Sep 17 00:00:00 2001 From: lzh Date: Tue, 20 Jan 2026 16:29:16 +0800 Subject: [PATCH] =?UTF-8?q?refactor(ops):=20=E9=87=8D=E6=9E=84=E8=AF=AD?= =?UTF-8?q?=E9=9F=B3=E6=92=AD=E6=8A=A5=E6=9C=8D=E5=8A=A1=E4=B8=BA=E7=BB=9F?= =?UTF-8?q?=E4=B8=80=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除 VoiceBroadcastDeduplicationService(去重服务) - 新增 VoiceBroadcastService 作为 TTS 统一入口 - broadcast(deviceId, text): 同步播报 - broadcast(deviceId, text, volume): 带音量播报 - broadcastAsync(): 异步播报 - 简化设计:接受 deviceId 参数,不实现复杂去重逻辑 Co-Authored-By: Claude Opus 4.5 --- .../VoiceBroadcastDeduplicationService.java | 228 ------------------ .../service/voice/VoiceBroadcastService.java | 94 ++++++++ 2 files changed, 94 insertions(+), 228 deletions(-) delete mode 100644 viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/voice/VoiceBroadcastDeduplicationService.java create mode 100644 viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/voice/VoiceBroadcastService.java diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/voice/VoiceBroadcastDeduplicationService.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/voice/VoiceBroadcastDeduplicationService.java deleted file mode 100644 index 1636dff..0000000 --- a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/voice/VoiceBroadcastDeduplicationService.java +++ /dev/null @@ -1,228 +0,0 @@ -package com.viewsh.module.ops.environment.service.voice; - -import jakarta.annotation.Resource; -import lombok.extern.slf4j.Slf4j; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Service; - -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.TimeUnit; - -/** - * 语音播报去重服务 - * - * 功能: - * 1. 短时间内多个工单入队,合并播报 - * 2. 使用计数器累积播报请求 - * 3. 定时任务定期合并播报 - * - * @author lzh - */ -@Slf4j -@Service -public class VoiceBroadcastDeduplicationService { - - @Resource - private RedisTemplate redisTemplate; - - /** - * 本地缓存:存储待合并的播报请求(保洁员ID -> 新增工单数量) - * Key: cleanerId - * Value: 新增工单数量 - */ - private final Map pendingBroadcasts = new ConcurrentHashMap<>(); - - /** - * 本地缓存:存储最后播报时间(保洁员ID -> 最后播报时间) - */ - private final Map lastBroadcastTime = new ConcurrentHashMap<>(); - - /** - * 播报合并时间窗口(秒) - * 在此时间窗口内的多个播报请求将被合并 - */ - private static final int BROADCAST_WINDOW_SECONDS = 5; - - /** - * Redis Key 前缀 - */ - private static final String REDIS_KEY_PREFIX = "ops:voice:pending:"; - private static final String REDIS_LAST_BROADCAST_PREFIX = "ops:voice:last:"; - - /** - * 记录播报请求(不立即播报,而是累积) - * - * @param cleanerId 保洁员ID - * @param orderCount 新增工单数量(通常为1) - */ - public void recordBroadcastRequest(Long cleanerId, int orderCount) { - // 1. 累积到本地缓存 - pendingBroadcasts.merge(cleanerId, orderCount, Integer::sum); - - // 2. 同步到 Redis(支持分布式环境) - String redisKey = REDIS_KEY_PREFIX + cleanerId; - redisTemplate.opsForValue().increment(redisKey, orderCount); - redisTemplate.expire(redisKey, BROADCAST_WINDOW_SECONDS + 2, TimeUnit.SECONDS); - - log.debug("记录播报请求: cleanerId={}, orderCount={}, totalPending={}", - cleanerId, orderCount, pendingBroadcasts.getOrDefault(cleanerId, 0)); - } - - /** - * 记录播报请求并立即播报 - * 如果距离上次播报时间过短,则合并播报 - * - * @param cleanerId 保洁员ID - * @param orderCount 新增工单数量 - * @param immediate 是否立即播报 - */ - public void recordAndBroadcast(Long cleanerId, int orderCount, boolean immediate) { - recordBroadcastRequest(cleanerId, orderCount); - - if (immediate) { - // 检查是否应该立即播报(距离上次播报超过时间窗口) - LocalDateTime lastTime = lastBroadcastTime.get(cleanerId); - if (lastTime == null || - lastTime.plusSeconds(BROADCAST_WINDOW_SECONDS).isBefore(LocalDateTime.now())) { - // 立即播报 - flushBroadcast(cleanerId); - } - // 否则等待定时任务合并播报 - } - } - - /** - * 立即播报(合并累积的请求) - * - * @param cleanerId 保洁员ID - */ - public void flushBroadcast(Long cleanerId) { - // 1. 获取累积的工单数量 - Integer totalOrders = pendingBroadcasts.get(cleanerId); - if (totalOrders == null || totalOrders == 0) { - // 从 Redis 获取 - String redisKey = REDIS_KEY_PREFIX + cleanerId; - Object value = redisTemplate.opsForValue().get(redisKey); - if (value instanceof Integer) { - totalOrders = (Integer) value; - } else { - return; // 没有待播报的请求 - } - } - - if (totalOrders <= 0) { - return; - } - - // 2. 计算总待办数量(当前队列中的工单数) - // TODO: 调用 OrderQueueService 获取队列中的工单数量 - // int totalQueueCount = orderQueueService.countByUserId(cleanerId); - int totalQueueCount = totalOrders; // 临时使用 totalOrders - - // 3. 生成播报内容 - String message = String.format("新增%d项待办,您共有%d个待办工单", - totalOrders, totalQueueCount); - - // 4. 调用 IoT 设备服务播报 - // TODO: iotDeviceService.playVoice(cleanerId, message); - - // 5. 清除累积的请求 - pendingBroadcasts.remove(cleanerId); - String redisKey = REDIS_KEY_PREFIX + cleanerId; - redisTemplate.delete(redisKey); - - // 6. 更新最后播报时间 - lastBroadcastTime.put(cleanerId, LocalDateTime.now()); - String lastBroadcastKey = REDIS_LAST_BROADCAST_PREFIX + cleanerId; - redisTemplate.opsForValue().set(lastBroadcastKey, - LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME), - 1, TimeUnit.HOURS); - - log.info("语音播报已发送: cleanerId={}, newOrders={}, totalQueue={}, message={}", - cleanerId, totalOrders, totalQueueCount, message); - } - - /** - * 定时任务:定期合并播报(每5秒执行一次) - * 将累积的播报请求合并后发送 - */ - @Scheduled(fixedDelay = BROADCAST_WINDOW_SECONDS * 1000) - public void scheduledFlush() { - if (pendingBroadcasts.isEmpty()) { - return; - } - - log.debug("定时合并播报开始: pendingCount={}", pendingBroadcasts.size()); - - // 复制一份,避免并发修改 - Map toFlush = new HashMap<>(pendingBroadcasts); - - for (Map.Entry entry : toFlush.entrySet()) { - Long cleanerId = entry.getKey(); - try { - flushBroadcast(cleanerId); - } catch (Exception e) { - log.error("定时播报失败: cleanerId={}", cleanerId, e); - } - } - - log.debug("定时合并播报完成"); - } - - /** - * 清理过期的播报记录(每小时执行一次) - */ - @Scheduled(fixedDelay = 3600 * 1000) - public void cleanupExpiredRecords() { - LocalDateTime now = LocalDateTime.now(); - lastBroadcastTime.entrySet().removeIf(entry -> - entry.getValue().plusHours(2).isBefore(now)); - - log.debug("清理过期播报记录完成"); - } - - /** - * 获取待播报的工单数量 - * - * @param cleanerId 保洁员ID - * @return 待播报的工单数量 - */ - public int getPendingOrders(Long cleanerId) { - Integer count = pendingBroadcasts.getOrDefault(cleanerId, 0); - if (count == 0) { - String redisKey = REDIS_KEY_PREFIX + cleanerId; - Object value = redisTemplate.opsForValue().get(redisKey); - if (value instanceof Integer) { - count = (Integer) value; - } - } - return count; - } - - /** - * 检查是否可以立即播报 - * - * @param cleanerId 保洁员ID - * @return true 如果距离上次播报超过时间窗口 - */ - public boolean canBroadcastImmediately(Long cleanerId) { - LocalDateTime lastTime = lastBroadcastTime.get(cleanerId); - if (lastTime == null) { - // 从 Redis 获取 - String lastBroadcastKey = REDIS_LAST_BROADCAST_PREFIX + cleanerId; - Object value = redisTemplate.opsForValue().get(lastBroadcastKey); - if (value instanceof String) { - lastTime = LocalDateTime.parse((String) value, DateTimeFormatter.ISO_LOCAL_DATE_TIME); - lastBroadcastTime.put(cleanerId, lastTime); - } else { - return true; // 没有播报记录,可以立即播报 - } - } - - return lastTime.plusSeconds(BROADCAST_WINDOW_SECONDS).isBefore(LocalDateTime.now()); - } -} 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 new file mode 100644 index 0000000..b6fcc80 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/voice/VoiceBroadcastService.java @@ -0,0 +1,94 @@ +package com.viewsh.module.ops.environment.service.voice; + +import cn.hutool.core.map.MapUtil; +import com.viewsh.module.iot.api.device.IotDeviceControlApi; +import com.viewsh.module.iot.api.device.dto.IotDeviceServiceInvokeReqDTO; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; + +/** + * 语音播报服务(统一入口) + *

+ * 职责: + * 1. 统一所有 TTS 下发入口 + * 2. 提供同步/异步播报接口 + * 3. 管理播报音量参数 + *

+ * 设计原则: + * - 接受 deviceId 参数(而非 cleanerId) + * - 简单可靠,不实现复杂的去重逻辑,由调用方控制 + * - 直接调用 IoT 设备控制接口 + * + * @author lzh + */ +@Slf4j +@Service +public class VoiceBroadcastService { + + @Resource + private IotDeviceControlApi iotDeviceControlApi; + + /** + * 播报语音(同步) + * + * @param deviceId 设备ID + * @param text 播报文本 + */ + public void broadcast(Long deviceId, String text) { + broadcast(deviceId, text, null); + } + + /** + * 播报语音(带音量) + * + * @param deviceId 设备ID + * @param text 播报文本 + * @param volume 音量(0-100) + */ + public void broadcast(Long deviceId, String text, Integer volume) { + if (deviceId == null || text == null) { + return; + } + + try { + IotDeviceServiceInvokeReqDTO reqDTO = new IotDeviceServiceInvokeReqDTO(); + reqDTO.setDeviceId(deviceId); + reqDTO.setIdentifier("tts"); + reqDTO.setParams(MapUtil.builder() + .put("text", text) + .put("playMode", 1) // 立即播报 + .put("volume", volume != null ? volume : 50) + .build()); + + iotDeviceControlApi.invokeService(reqDTO); + log.debug("[VoiceBroadcast] 播报成功: deviceId={}, text={}", deviceId, text); + } catch (Exception e) { + log.error("[VoiceBroadcast] 播报失败: deviceId={}, text={}", deviceId, text, e); + } + } + + /** + * 播报语音(异步) + * + * @param deviceId 设备ID + * @param text 播报文本 + */ + @Async("ops-task-executor") + public void broadcastAsync(Long deviceId, String text) { + broadcast(deviceId, text); + } + + /** + * 播报语音(异步,带音量) + * + * @param deviceId 设备ID + * @param text 播报文本 + * @param volume 音量(0-100) + */ + @Async("ops-task-executor") + public void broadcastAsync(Long deviceId, String text, Integer volume) { + broadcast(deviceId, text, volume); + } +}