From dc5c2e1396513b2f800fe2afa778de6672a7e0e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AD=99=E5=B0=8F=E4=BA=91?= Date: Fri, 30 Jan 2026 17:13:12 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E7=BC=93=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 6 + .../device/config/DeviceCacheConfig.java | 8 + .../ruoyi/device/config/WebSocketConfig.java | 14 + .../device/controller/StaticsController.java | 178 +++--------- .../device/domain/api/IThingsBoardDomain.java | 16 ++ .../domain/impl/ThingsBoardDomainImpl.java | 158 ++++------- .../device/websocket/StatisticsWebSocket.java | 256 ++++++++++++++++++ 7 files changed, 402 insertions(+), 234 deletions(-) create mode 100644 src/main/java/com/ruoyi/device/config/WebSocketConfig.java create mode 100644 src/main/java/com/ruoyi/device/websocket/StatisticsWebSocket.java diff --git a/pom.xml b/pom.xml index 8f498a1..205bf5b 100644 --- a/pom.xml +++ b/pom.xml @@ -117,6 +117,12 @@ caffeine + + + org.springframework.boot + spring-boot-starter-websocket + + org.springframework.integration diff --git a/src/main/java/com/ruoyi/device/config/DeviceCacheConfig.java b/src/main/java/com/ruoyi/device/config/DeviceCacheConfig.java index 972cbbb..bd235b5 100644 --- a/src/main/java/com/ruoyi/device/config/DeviceCacheConfig.java +++ b/src/main/java/com/ruoyi/device/config/DeviceCacheConfig.java @@ -35,6 +35,8 @@ public class DeviceCacheConfig { public static final String PAYLOAD_CACHE = "payload"; public static final String DOCK_AIRCRAFT_CACHE = "dockAircraft"; public static final String AIRCRAFT_PAYLOAD_CACHE = "aircraftPayload"; + public static final String THINGSBOARD_ATTRIBUTES_CACHE = "thingsboardAttributes"; + public static final String THINGSBOARD_TELEMETRY_CACHE = "thingsboardTelemetry"; /** * 配置缓存管理器 @@ -68,6 +70,12 @@ public class DeviceCacheConfig { cacheConfigurations.put(DOCK_AIRCRAFT_CACHE, defaultConfig.entryTtl(Duration.ofMinutes(15))); cacheConfigurations.put(AIRCRAFT_PAYLOAD_CACHE, defaultConfig.entryTtl(Duration.ofMinutes(15))); + // ThingsBoard 设备属性:90秒(属性数据,变化较少) + cacheConfigurations.put(THINGSBOARD_ATTRIBUTES_CACHE, defaultConfig.entryTtl(Duration.ofSeconds(90))); + + // ThingsBoard 设备遥测:15秒(遥测数据,实时性要求高) + cacheConfigurations.put(THINGSBOARD_TELEMETRY_CACHE, defaultConfig.entryTtl(Duration.ofSeconds(15))); + return RedisCacheManager.builder(connectionFactory) .cacheDefaults(defaultConfig) .withInitialCacheConfigurations(cacheConfigurations) diff --git a/src/main/java/com/ruoyi/device/config/WebSocketConfig.java b/src/main/java/com/ruoyi/device/config/WebSocketConfig.java new file mode 100644 index 0000000..55f30f9 --- /dev/null +++ b/src/main/java/com/ruoyi/device/config/WebSocketConfig.java @@ -0,0 +1,14 @@ +package com.ruoyi.device.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.socket.server.standard.ServerEndpointExporter; + +@Configuration +public class WebSocketConfig { + + @Bean + public ServerEndpointExporter serverEndpointExporter() { + return new ServerEndpointExporter(); + } +} diff --git a/src/main/java/com/ruoyi/device/controller/StaticsController.java b/src/main/java/com/ruoyi/device/controller/StaticsController.java index 5e3d6f1..c39cfae 100644 --- a/src/main/java/com/ruoyi/device/controller/StaticsController.java +++ b/src/main/java/com/ruoyi/device/controller/StaticsController.java @@ -53,9 +53,51 @@ public class StaticsController extends BaseController */ @GetMapping public R getStatistics() + { + return R.ok(buildDjiStatisticsVO()); + } + + + + + + @GetMapping("/dji") + public R getDjiStatistics() + { + return R.ok(buildDjiStatisticsVO()); + } + + + @GetMapping("/th") + public R getThStatistics() { StatisticsVO vo = new StatisticsVO(); + // 机场统计 + vo.setDockCount(0); + vo.setIdleDockCount(0); + vo.setWorkingDockCount(0); + vo.setDebuggingDockCount(0); + vo.setOfflineDockCount(0); + + // 无人机统计 + vo.setAircraftCount(0); + vo.setPowerOnInCabinCount(0); + vo.setPowerOffInCabinCount(0); + vo.setInMissionCount(0); + vo.setDebuggingAircraftCount(0); + vo.setOfflineAircraftCount(0); + + // 挂载统计 + vo.setPayloadCount(0); + vo.setOfflinePayloadCount(0); + + return R.ok(vo); + } + + private StatisticsVO buildDjiStatisticsVO (){ + StatisticsVO vo = new StatisticsVO(); + // 获取所有机场 List docks = dockService.selectDockList(new DockDTO()); vo.setDockCount(docks != null ? docks.size() : 0); @@ -153,140 +195,6 @@ public class StaticsController extends BaseController // 统计离线挂载数量(暂时设置为0,因为挂载状态需要从实时数据获取) vo.setOfflinePayloadCount(0); - return R.ok(vo); - } - - - @GetMapping("/dji") - public R getDjiStatistics() - { - StatisticsVO vo = new StatisticsVO(); - - // 获取所有机场 - List docks = dockService.selectDockList(new DockDTO()); - vo.setDockCount(docks != null ? docks.size() : 0); - - // 批量获取机场详情 - 优化:从N次查询减少到1次批量查询 - Map dockDetailsMap = null; - if (docks != null && !docks.isEmpty()) { - List dockIds = docks.stream() - .map(DockDTO::getDockId) - .collect(Collectors.toList()); - dockDetailsMap = bufferDeviceService.getDockDetailsByIds(dockIds); - } - - // 统计各状态机场数量 - int idleCount = 0; - int workingCount = 0; - int debuggingCount = 0; - int offlineCount = 0; - - if (docks != null && dockDetailsMap != null) { - for (DockDTO dock : docks) { - DockDetailDTO dockDetail = dockDetailsMap.get(dock.getDockId()); - if (dockDetail != null && dockDetail.getDockStatus() != null) { - String status = dockDetail.getDockStatus(); - if (DockStatusEnum.IDLE.getCode().equals(status)) { - idleCount++; - } else if (DockStatusEnum.WORKING.getCode().equals(status)) { - workingCount++; - } else if (DockStatusEnum.Debugging.getCode().equals(status)) { - debuggingCount++; - } else { - offlineCount++; - } - } - } - } - - vo.setIdleDockCount(idleCount); - vo.setWorkingDockCount(workingCount); - vo.setDebuggingDockCount(debuggingCount); - vo.setOfflineDockCount(offlineCount); - - // 获取所有无人机 - List aircrafts = aircraftService.selectAircraftList(new AircraftDTO()); - vo.setAircraftCount(aircrafts != null ? aircrafts.size() : 0); - - // 批量获取无人机详情 - 优化:从N次查询减少到1次批量查询 - Map aircraftDetailsMap = null; - if (aircrafts != null && !aircrafts.isEmpty()) { - List aircraftIds = aircrafts.stream() - .map(AircraftDTO::getAircraftId) - .collect(Collectors.toList()); - aircraftDetailsMap = bufferDeviceService.getAircraftDetailsByIds(aircraftIds); - } - - // 统计各状态无人机数量 - int powerOnInCabinCount = 0; - int powerOffInCabinCount = 0; - int inMissionCount = 0; - int debuggingAircraftCount = 0; - int offlineAircraftCount = 0; - - if (aircrafts != null && aircraftDetailsMap != null) { - for (AircraftDTO aircraft : aircrafts) { - AircraftDetailDTO aircraftDetail = aircraftDetailsMap.get(aircraft.getAircraftId()); - if (aircraftDetail != null && aircraftDetail.getAircraftStatus() != null) { - String status = aircraftDetail.getAircraftStatus(); - if (AircraftStatusEnum.POWER_ON_IN_CABIN.getCode().equals(status)) { - powerOnInCabinCount++; - } else if (AircraftStatusEnum.POWER_OFF_IN_CABIN.getCode().equals(status)) { - powerOffInCabinCount++; - } else if (AircraftStatusEnum.IN_MISSION.getCode().equals(status)) { - inMissionCount++; - } else if (AircraftStatusEnum.DEBUGGING.getCode().equals(status)) { - debuggingAircraftCount++; - } else if (AircraftStatusEnum.OFFLINE.getCode().equals(status)) { - offlineAircraftCount++; - } else { - offlineAircraftCount++; - } - } - } - } - - vo.setPowerOnInCabinCount(powerOnInCabinCount); - vo.setPowerOffInCabinCount(powerOffInCabinCount); - vo.setInMissionCount(inMissionCount); - vo.setDebuggingAircraftCount(debuggingAircraftCount); - vo.setOfflineAircraftCount(offlineAircraftCount); - - // 获取所有挂载 - List payloads = payloadService.selectPayloadList(new PayloadDTO()); - vo.setPayloadCount(payloads != null ? payloads.size() : 0); - - // 统计离线挂载数量(暂时设置为0,因为挂载状态需要从实时数据获取) - vo.setOfflinePayloadCount(0); - - return R.ok(vo); - } - - - @GetMapping("/th") - public R getThStatistics() - { - StatisticsVO vo = new StatisticsVO(); - - // 机场统计 - vo.setDockCount(0); - vo.setIdleDockCount(0); - vo.setWorkingDockCount(0); - vo.setDebuggingDockCount(0); - vo.setOfflineDockCount(0); - - // 无人机统计 - vo.setAircraftCount(0); - vo.setPowerOnInCabinCount(0); - vo.setPowerOffInCabinCount(0); - vo.setInMissionCount(0); - vo.setDebuggingAircraftCount(0); - vo.setOfflineAircraftCount(0); - - // 挂载统计 - vo.setPayloadCount(0); - vo.setOfflinePayloadCount(0); - - return R.ok(vo); + return vo; } } \ No newline at end of file diff --git a/src/main/java/com/ruoyi/device/domain/api/IThingsBoardDomain.java b/src/main/java/com/ruoyi/device/domain/api/IThingsBoardDomain.java index 2c5903d..1a8074c 100644 --- a/src/main/java/com/ruoyi/device/domain/api/IThingsBoardDomain.java +++ b/src/main/java/com/ruoyi/device/domain/api/IThingsBoardDomain.java @@ -91,4 +91,20 @@ public interface IThingsBoardDomain { * @return 子设备ID列表,如果网关没有子设备则返回空列表 */ List getGatewayChildDevices(String gatewayDeviceId); + + /** + * 清除设备属性缓存 + * 使 getDeviceAttributes 方法的缓存失效 + * + * @param deviceId 设备ID + */ + void evictDeviceAttributesCache(String deviceId); + + /** + * 清除设备遥测数据缓存 + * 使 getDeviceTelemetry 方法的缓存失效 + * + * @param deviceId 设备ID + */ + void evictDeviceTelemetryCache(String deviceId); } \ No newline at end of file diff --git a/src/main/java/com/ruoyi/device/domain/impl/ThingsBoardDomainImpl.java b/src/main/java/com/ruoyi/device/domain/impl/ThingsBoardDomainImpl.java index d464842..c293af7 100644 --- a/src/main/java/com/ruoyi/device/domain/impl/ThingsBoardDomainImpl.java +++ b/src/main/java/com/ruoyi/device/domain/impl/ThingsBoardDomainImpl.java @@ -1,9 +1,7 @@ package com.ruoyi.device.domain.impl; -import com.github.benmanes.caffeine.cache.Cache; -import com.github.benmanes.caffeine.cache.Caffeine; -import com.github.benmanes.caffeine.cache.Expiry; +import com.ruoyi.device.config.DeviceCacheConfig; import com.ruoyi.device.domain.api.IThingsBoardDomain; import com.ruoyi.device.domain.model.thingsboard.*; import com.ruoyi.device.domain.model.thingsboard.constants.DeviceAttributes; @@ -11,6 +9,8 @@ import com.ruoyi.device.domain.model.thingsboard.constants.DeviceTelemetry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import org.thingsboard.rest.client.RestClient; @@ -30,9 +30,7 @@ import org.thingsboard.server.common.data.relation.RelationTypeGroup; import java.util.ArrayList; import java.util.List; import java.util.Optional; -import java.util.Random; import java.util.UUID; -import java.util.concurrent.TimeUnit; /** * ThingsBoard设备服务实现类 @@ -46,10 +44,6 @@ public class ThingsBoardDomainImpl implements IThingsBoardDomain { private final RestClient client; private final int pageSize; - private final Random random = new Random(); - - private final Cache attributesCache; - private final Cache telemetryCache; /** * 构造函数 - Spring 会自动装配 @@ -60,48 +54,6 @@ public class ThingsBoardDomainImpl implements IThingsBoardDomain { @Value("${thingsboard.page-size:10}") int pageSize) { this.client = clientManager.getClient(); this.pageSize = pageSize; - - this.attributesCache = Caffeine.newBuilder() - .expireAfter(new Expiry() { - @Override - public long expireAfterCreate(String key, AttributeMap value, long currentTime) { - long randomSeconds = 60 + random.nextInt(61); - return TimeUnit.SECONDS.toNanos(randomSeconds); - } - - @Override - public long expireAfterUpdate(String key, AttributeMap value, long currentTime, long currentDuration) { - long randomSeconds = 60 + random.nextInt(61); - return TimeUnit.SECONDS.toNanos(randomSeconds); - } - - @Override - public long expireAfterRead(String key, AttributeMap value, long currentTime, long currentDuration) { - return currentDuration; - } - }) - .build(); - - this.telemetryCache = Caffeine.newBuilder() - .expireAfter(new Expiry() { - @Override - public long expireAfterCreate(String key, TelemetryMap value, long currentTime) { - long randomSeconds = 12 + random.nextInt(7); - return TimeUnit.SECONDS.toNanos(randomSeconds); - } - - @Override - public long expireAfterUpdate(String key, TelemetryMap value, long currentTime, long currentDuration) { - long randomSeconds = 12 + random.nextInt(7); - return TimeUnit.SECONDS.toNanos(randomSeconds); - } - - @Override - public long expireAfterRead(String key, TelemetryMap value, long currentTime, long currentDuration) { - return currentDuration; - } - }) - .build(); } @Override @@ -138,67 +90,65 @@ public class ThingsBoardDomainImpl implements IThingsBoardDomain { } @Override + @Cacheable(value = DeviceCacheConfig.THINGSBOARD_ATTRIBUTES_CACHE, key = "#deviceId", unless = "#result == null || #result.isEmpty()") public AttributeMap getDeviceAttributes(String deviceId) { - return attributesCache.get(deviceId, id -> { - AttributeMap attributeMap = new AttributeMap(); + AttributeMap attributeMap = new AttributeMap(); - try { - DeviceId deviceIdObj = new DeviceId(UUID.fromString(id)); + try { + DeviceId deviceIdObj = new DeviceId(UUID.fromString(deviceId)); - List attributeKeys = client.getAttributeKeys(deviceIdObj); - if (attributeKeys == null || attributeKeys.isEmpty()) { - log.debug("设备 {} 没有属性", id); - return attributeMap; - } - - List attributeKvEntries = client.getAttributeKvEntries(deviceIdObj, attributeKeys); - if (attributeKvEntries == null || attributeKvEntries.isEmpty()) { - log.debug("设备 {} 的属性值为空", id); - return attributeMap; - } - - for (AttributeKvEntry entry : attributeKvEntries) { - parseAndPutAttribute(attributeMap, entry); - } - - } catch (Exception e) { - log.error("获取设备属性失败: deviceId={}, error={}", id, e.getMessage(), e); + List attributeKeys = client.getAttributeKeys(deviceIdObj); + if (attributeKeys == null || attributeKeys.isEmpty()) { + log.debug("设备 {} 没有属性", deviceId); + return attributeMap; } - return attributeMap; - }); + List attributeKvEntries = client.getAttributeKvEntries(deviceIdObj, attributeKeys); + if (attributeKvEntries == null || attributeKvEntries.isEmpty()) { + log.debug("设备 {} 的属性值为空", deviceId); + return attributeMap; + } + + for (AttributeKvEntry entry : attributeKvEntries) { + parseAndPutAttribute(attributeMap, entry); + } + + } catch (Exception e) { + log.error("获取设备属性失败: deviceId={}, error={}", deviceId, e.getMessage(), e); + } + + return attributeMap; } @Override + @Cacheable(value = DeviceCacheConfig.THINGSBOARD_TELEMETRY_CACHE, key = "#deviceId", unless = "#result == null || #result.isEmpty()") public TelemetryMap getDeviceTelemetry(String deviceId) { - return telemetryCache.get(deviceId, id -> { - TelemetryMap telemetryMap = new TelemetryMap(); + TelemetryMap telemetryMap = new TelemetryMap(); - try { - DeviceId deviceIdObj = new DeviceId(UUID.fromString(id)); + try { + DeviceId deviceIdObj = new DeviceId(UUID.fromString(deviceId)); - List timeseriesKeys = client.getTimeseriesKeys(deviceIdObj); - if (timeseriesKeys == null || timeseriesKeys.isEmpty()) { - log.debug("设备 {} 没有遥测数据", id); - return telemetryMap; - } - - List latestTimeseries = client.getLatestTimeseries(deviceIdObj, timeseriesKeys); - if (latestTimeseries == null || latestTimeseries.isEmpty()) { - log.debug("设备 {} 的遥测数据为空", id); - return telemetryMap; - } - - for (TsKvEntry entry : latestTimeseries) { - parseAndPutTelemetry(telemetryMap, entry); - } - - } catch (Exception e) { - log.error("获取设备遥测数据失败: deviceId={}, error={}", id, e.getMessage(), e); + List timeseriesKeys = client.getTimeseriesKeys(deviceIdObj); + if (timeseriesKeys == null || timeseriesKeys.isEmpty()) { + log.debug("设备 {} 没有遥测数据", deviceId); + return telemetryMap; } - return telemetryMap; - }); + List latestTimeseries = client.getLatestTimeseries(deviceIdObj, timeseriesKeys); + if (latestTimeseries == null || latestTimeseries.isEmpty()) { + log.debug("设备 {} 的遥测数据为空", deviceId); + return telemetryMap; + } + + for (TsKvEntry entry : latestTimeseries) { + parseAndPutTelemetry(telemetryMap, entry); + } + + } catch (Exception e) { + log.error("获取设备遥测数据失败: deviceId={}, error={}", deviceId, e.getMessage(), e); + } + + return telemetryMap; } @@ -379,5 +329,15 @@ public class ThingsBoardDomainImpl implements IThingsBoardDomain { } } + @Override + @CacheEvict(value = DeviceCacheConfig.THINGSBOARD_ATTRIBUTES_CACHE, key = "#deviceId") + public void evictDeviceAttributesCache(String deviceId) { + // 空实现,仅用于清除缓存 + } + @Override + @CacheEvict(value = DeviceCacheConfig.THINGSBOARD_TELEMETRY_CACHE, key = "#deviceId") + public void evictDeviceTelemetryCache(String deviceId) { + // 空实现,仅用于清除缓存 + } } \ No newline at end of file diff --git a/src/main/java/com/ruoyi/device/websocket/StatisticsWebSocket.java b/src/main/java/com/ruoyi/device/websocket/StatisticsWebSocket.java new file mode 100644 index 0000000..1f4c495 --- /dev/null +++ b/src/main/java/com/ruoyi/device/websocket/StatisticsWebSocket.java @@ -0,0 +1,256 @@ +package com.ruoyi.device.websocket; + +import com.alibaba.fastjson2.JSON; +import com.ruoyi.device.api.domain.StatisticsVO; +import com.ruoyi.device.service.api.IBufferDeviceService; +import com.ruoyi.device.service.api.IAircraftService; +import com.ruoyi.device.service.api.IDockService; +import com.ruoyi.device.service.api.IPayloadService; +import com.ruoyi.device.service.dto.AircraftDTO; +import com.ruoyi.device.service.dto.AircraftDetailDTO; +import com.ruoyi.device.service.dto.DockDTO; +import com.ruoyi.device.service.dto.DockDetailDTO; +import com.ruoyi.device.service.dto.PayloadDTO; +import com.ruoyi.device.api.enums.AircraftStatusEnum; +import com.ruoyi.device.api.enums.DockStatusEnum; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import jakarta.websocket.*; +import jakarta.websocket.server.PathParam; +import jakarta.websocket.server.ServerEndpoint; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.stream.Collectors; + +@Component +@ServerEndpoint("/websocket/statistics/{type}") +public class StatisticsWebSocket { + + private static final Logger log = LoggerFactory.getLogger(StatisticsWebSocket.class); + + private static IBufferDeviceService bufferDeviceService; + private static IAircraftService aircraftService; + private static IDockService dockService; + private static IPayloadService payloadService; + + private Session session; + + private static final Map> sessions = new ConcurrentHashMap<>(); + + @Autowired + public void setBufferDeviceService(IBufferDeviceService bufferDeviceService) { + StatisticsWebSocket.bufferDeviceService = bufferDeviceService; + } + + @Autowired + public void setAircraftService(IAircraftService aircraftService) { + StatisticsWebSocket.aircraftService = aircraftService; + } + + @Autowired + public void setDockService(IDockService dockService) { + StatisticsWebSocket.dockService = dockService; + } + + @Autowired + public void setPayloadService(IPayloadService payloadService) { + StatisticsWebSocket.payloadService = payloadService; + } + + @OnOpen + public void onOpen(Session session, @PathParam("type") String type) { + this.session = session; + sessions.computeIfAbsent(type, k -> new CopyOnWriteArraySet<>()).add(this); + log.info("WebSocket连接建立: sessionId={}, type={}", session.getId(), type); + + sendCurrentStatistics(type); + } + + @OnClose + public void onClose(@PathParam("type") String type) { + CopyOnWriteArraySet typeSessions = sessions.get(type); + if (typeSessions != null) { + typeSessions.remove(this); + if (typeSessions.isEmpty()) { + sessions.remove(type); + } + } + log.info("WebSocket连接关闭: sessionId={}, type={}", session.getId(), type); + } + + @OnMessage + public void onMessage(String message, @PathParam("type") String type) { + log.info("收到WebSocket消息: sessionId={}, type={}, message={}", session.getId(), type, message); + if ("refresh".equals(message)) { + sendCurrentStatistics(type); + } + } + + @OnError + public void onError(Session session, Throwable error, @PathParam("type") String type) { + log.error("WebSocket错误: sessionId={}, type={}, error={}", session.getId(), type, error.getMessage(), error); + } + + private void sendCurrentStatistics(String type) { + try { + StatisticsVO statistics = buildStatistics(type); + sendMessage(JSON.toJSONString(statistics)); + } catch (Exception e) { + log.error("发送统计数据失败: type={}, error={}", type, e.getMessage(), e); + } + } + + private StatisticsVO buildStatistics(String type) { + if ("th".equals(type)) { + return buildThStatistics(); + } else { + return buildDjiStatistics(); + } + } + + private StatisticsVO buildDjiStatistics() { + StatisticsVO vo = new StatisticsVO(); + + List docks = dockService.selectDockList(new DockDTO()); + vo.setDockCount(docks != null ? docks.size() : 0); + + Map dockDetailsMap = null; + if (docks != null && !docks.isEmpty()) { + List dockIds = docks.stream() + .map(DockDTO::getDockId) + .collect(Collectors.toList()); + dockDetailsMap = bufferDeviceService.getDockDetailsByIds(dockIds); + } + + int idleCount = 0; + int workingCount = 0; + int debuggingCount = 0; + int offlineCount = 0; + + if (docks != null && dockDetailsMap != null) { + for (DockDTO dock : docks) { + DockDetailDTO dockDetail = dockDetailsMap.get(dock.getDockId()); + if (dockDetail != null && dockDetail.getDockStatus() != null) { + String status = dockDetail.getDockStatus(); + if (DockStatusEnum.IDLE.getCode().equals(status)) { + idleCount++; + } else if (DockStatusEnum.WORKING.getCode().equals(status)) { + workingCount++; + } else if (DockStatusEnum.Debugging.getCode().equals(status)) { + debuggingCount++; + } else { + offlineCount++; + } + } + } + } + + vo.setIdleDockCount(idleCount); + vo.setWorkingDockCount(workingCount); + vo.setDebuggingDockCount(debuggingCount); + vo.setOfflineDockCount(offlineCount); + + List aircrafts = aircraftService.selectAircraftList(new AircraftDTO()); + vo.setAircraftCount(aircrafts != null ? aircrafts.size() : 0); + + Map aircraftDetailsMap = null; + if (aircrafts != null && !aircrafts.isEmpty()) { + List aircraftIds = aircrafts.stream() + .map(AircraftDTO::getAircraftId) + .collect(Collectors.toList()); + aircraftDetailsMap = bufferDeviceService.getAircraftDetailsByIds(aircraftIds); + } + + int powerOnInCabinCount = 0; + int powerOffInCabinCount = 0; + int inMissionCount = 0; + int debuggingAircraftCount = 0; + int offlineAircraftCount = 0; + + if (aircrafts != null && aircraftDetailsMap != null) { + for (AircraftDTO aircraft : aircrafts) { + AircraftDetailDTO aircraftDetail = aircraftDetailsMap.get(aircraft.getAircraftId()); + if (aircraftDetail != null && aircraftDetail.getAircraftStatus() != null) { + String status = aircraftDetail.getAircraftStatus(); + if (AircraftStatusEnum.POWER_ON_IN_CABIN.getCode().equals(status)) { + powerOnInCabinCount++; + } else if (AircraftStatusEnum.POWER_OFF_IN_CABIN.getCode().equals(status)) { + powerOffInCabinCount++; + } else if (AircraftStatusEnum.IN_MISSION.getCode().equals(status)) { + inMissionCount++; + } else if (AircraftStatusEnum.DEBUGGING.getCode().equals(status)) { + debuggingAircraftCount++; + } else if (AircraftStatusEnum.OFFLINE.getCode().equals(status)) { + offlineAircraftCount++; + } else { + offlineAircraftCount++; + } + } + } + } + + vo.setPowerOnInCabinCount(powerOnInCabinCount); + vo.setPowerOffInCabinCount(powerOffInCabinCount); + vo.setInMissionCount(inMissionCount); + vo.setDebuggingAircraftCount(debuggingAircraftCount); + vo.setOfflineAircraftCount(offlineAircraftCount); + + List payloads = payloadService.selectPayloadList(new PayloadDTO()); + vo.setPayloadCount(payloads != null ? payloads.size() : 0); + vo.setOfflinePayloadCount(0); + + return vo; + } + + private StatisticsVO buildThStatistics() { + StatisticsVO vo = new StatisticsVO(); + vo.setDockCount(0); + vo.setIdleDockCount(0); + vo.setWorkingDockCount(0); + vo.setDebuggingDockCount(0); + vo.setOfflineDockCount(0); + vo.setAircraftCount(0); + vo.setPowerOnInCabinCount(0); + vo.setPowerOffInCabinCount(0); + vo.setInMissionCount(0); + vo.setDebuggingAircraftCount(0); + vo.setOfflineAircraftCount(0); + vo.setPayloadCount(0); + vo.setOfflinePayloadCount(0); + return vo; + } + + private void sendMessage(String message) { + try { + if (session != null && session.isOpen()) { + session.getBasicRemote().sendText(message); + } + } catch (Exception e) { + log.error("发送WebSocket消息失败: error={}", e.getMessage(), e); + } + } + + public static void broadcastToType(String type, String message) { + CopyOnWriteArraySet typeSessions = sessions.get(type); + if (typeSessions != null) { + for (StatisticsWebSocket ws : typeSessions) { + ws.sendMessage(message); + } + } + } + + public static void broadcastDjiStatistics() { + StatisticsVO statistics = new StatisticsWebSocket().buildDjiStatistics(); + broadcastToType("dji", JSON.toJSONString(statistics)); + } + + public static void broadcastThStatistics() { + StatisticsVO statistics = new StatisticsWebSocket().buildThStatistics(); + broadcastToType("th", JSON.toJSONString(statistics)); + } +}