Ver Fonte

feat(cockpit): 接入总体概览真实数据

Kazerin há 3 semanas atrás
pai
commit
64adba6d4a

+ 97 - 0
pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/BusinessCockpitOverviewInfrastructureAdapterTest.java

@@ -0,0 +1,97 @@
+package com.zksy.web.controller.cockpit;
+
+import com.zksy.base.domain.EquipmentBase;
+import com.zksy.base.domain.EquipmentType;
+import com.zksy.base.domain.PipeNetworkBase;
+import com.zksy.base.service.EquipmentBaseService;
+import com.zksy.base.service.EquipmentTypeService;
+import com.zksy.base.service.PipeNetworkBaseService;
+import com.zksy.cockpit.overview.BusinessCockpitOverviewInfrastructureAdapter;
+import com.zksy.cockpit.overview.CockpitOverviewInfrastructureSnapshot;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class BusinessCockpitOverviewInfrastructureAdapterTest {
+
+    private EquipmentTypeService equipmentTypeService;
+    private EquipmentBaseService equipmentBaseService;
+    private PipeNetworkBaseService pipeNetworkBaseService;
+    private BusinessCockpitOverviewInfrastructureAdapter adapter;
+
+    @BeforeEach
+    void setUp() {
+        equipmentTypeService = mock(EquipmentTypeService.class);
+        equipmentBaseService = mock(EquipmentBaseService.class);
+        pipeNetworkBaseService = mock(PipeNetworkBaseService.class);
+        adapter = new BusinessCockpitOverviewInfrastructureAdapter(
+                equipmentTypeService, equipmentBaseService, pipeNetworkBaseService);
+    }
+
+    @Test
+    void countsDevicesByTypeAndFacilitiesByNetworkTypeWithLengthInKm() {
+        EquipmentType gasRoot = type(3, "燃气", 0);
+        EquipmentType gasSensor = type(11, "燃气浓度计", 3);
+        when(equipmentTypeService.list()).thenReturn(Arrays.asList(gasRoot, gasSensor));
+
+        EquipmentBase first = device("E-1", "GAS-001", 11);
+        EquipmentBase second = device("E-2", "GAS-002", 11);
+        second.setUpdateTime(LocalDateTime.parse("2026-09-02T09:59:50"));
+        when(equipmentBaseService.list()).thenReturn(Arrays.asList(first, second));
+
+        PipeNetworkBase facility = new PipeNetworkBase();
+        facility.setNetworkId("N-1");
+        facility.setNetworkCode("PIPE-001");
+        facility.setNetworkName("城北主管网");
+        facility.setNetworkType("主管");
+        facility.setLength(new BigDecimal("5000"));
+        when(pipeNetworkBaseService.list()).thenReturn(Collections.singletonList(facility));
+
+        CockpitOverviewInfrastructureSnapshot snapshot = adapter.load();
+
+        assertEquals(3, snapshot.getTypes().size());
+        assertEquals("2026-09-02T09:59:50+08:00", snapshot.getUpdatedAt());
+
+        long deviceCount = snapshot.getCounts().stream()
+                .filter(count -> "SMART_DEVICE".equals(count.getObjectKind())
+                        && "11".equals(count.getTypeCode()))
+                .mapToInt(CockpitOverviewInfrastructureSnapshot.SourceCount::getCount)
+                .sum();
+        assertEquals(2, deviceCount);
+
+        CockpitOverviewInfrastructureSnapshot.SourceCount facilityCount = snapshot.getCounts().stream()
+                .filter(count -> "FACILITY".equals(count.getObjectKind()))
+                .findFirst().orElseThrow(AssertionError::new);
+        assertEquals("主管", facilityCount.getTypeCode());
+        assertEquals(1, facilityCount.getCount());
+        assertEquals(0, new BigDecimal("5.000").compareTo(facilityCount.getValue().getLengthKm()));
+        assertEquals("km", facilityCount.getValue().getScale());
+        assertNull(facilityCount.getValue().getRange());
+    }
+
+    private static EquipmentType type(int id, String name, int parentId) {
+        EquipmentType type = new EquipmentType();
+        type.setId(id);
+        type.setTypeName(name);
+        type.setParentId(parentId);
+        return type;
+    }
+
+    private static EquipmentBase device(String id, String code, int typeId) {
+        EquipmentBase device = new EquipmentBase();
+        device.setEquipmentId(id);
+        device.setEquipmentCode(code);
+        device.setEquipmentName(code);
+        device.setEquipmentTypeId(typeId);
+        return device;
+    }
+}

+ 157 - 0
pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/BusinessCockpitOverviewMonitoringAdapterTest.java

@@ -0,0 +1,157 @@
+package com.zksy.web.controller.cockpit;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.zksy.base.domain.EquipmentBase;
+import com.zksy.base.domain.EquipmentStatus;
+import com.zksy.base.domain.EquipmentType;
+import com.zksy.base.domain.MonitorPoint;
+import com.zksy.base.domain.PipeNetworkBase;
+import com.zksy.base.gas.domain.GasMonitorData;
+import com.zksy.base.gas.service.GasMonitorDataService;
+import com.zksy.base.service.EquipmentBaseService;
+import com.zksy.base.service.EquipmentStatusService;
+import com.zksy.base.service.EquipmentTypeService;
+import com.zksy.base.service.MonitorPointService;
+import com.zksy.base.service.PipeNetworkBaseService;
+import com.zksy.base.water.service.WaterMonitorDataService;
+import com.zksy.cockpit.overview.BusinessCockpitOverviewMonitoringAdapter;
+import com.zksy.cockpit.overview.CockpitOverviewMonitoringSnapshot;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.time.LocalDateTime;
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class BusinessCockpitOverviewMonitoringAdapterTest {
+
+    private static final String GAS_CODE = "GAS-001";
+
+    private EquipmentTypeService equipmentTypeService;
+    private EquipmentBaseService equipmentBaseService;
+    private MonitorPointService monitorPointService;
+    private PipeNetworkBaseService pipeNetworkBaseService;
+    private EquipmentStatusService equipmentStatusService;
+    private GasMonitorDataService gasMonitorDataService;
+    private WaterMonitorDataService waterMonitorDataService;
+    private BusinessCockpitOverviewMonitoringAdapter adapter;
+
+    @BeforeEach
+    void setUp() {
+        equipmentTypeService = mock(EquipmentTypeService.class);
+        equipmentBaseService = mock(EquipmentBaseService.class);
+        monitorPointService = mock(MonitorPointService.class);
+        pipeNetworkBaseService = mock(PipeNetworkBaseService.class);
+        equipmentStatusService = mock(EquipmentStatusService.class);
+        gasMonitorDataService = mock(GasMonitorDataService.class);
+        waterMonitorDataService = mock(WaterMonitorDataService.class);
+        adapter = new BusinessCockpitOverviewMonitoringAdapter(
+                equipmentTypeService, equipmentBaseService, monitorPointService,
+                pipeNetworkBaseService, equipmentStatusService,
+                gasMonitorDataService, waterMonitorDataService);
+
+        EquipmentType gasRoot = type(3, "燃气", 0);
+        EquipmentType gasSensor = type(11, "燃气浓度计", 3);
+        when(equipmentTypeService.list()).thenReturn(Arrays.asList(gasRoot, gasSensor));
+    }
+
+    @Test
+    void mapsDeviceStatePointTypeAndMetricFreshness() {
+        EquipmentBase device = device("E-1", GAS_CODE, 11);
+        device.setUpdateTime(LocalDateTime.parse("2026-09-02T09:59:50"));
+        when(equipmentBaseService.list()).thenReturn(Collections.singletonList(device));
+
+        MonitorPoint point = new MonitorPoint();
+        point.setPointId("P-1");
+        point.setPointCode("MP-1");
+        when(monitorPointService.list()).thenReturn(Collections.singletonList(point));
+        when(equipmentBaseService.getEquipmentByPoint("P-1"))
+                .thenReturn(Collections.singletonList(device));
+
+        EquipmentStatus status = new EquipmentStatus();
+        status.setStatusId(1);
+        status.setEquipmentId("E-1");
+        status.setOnlineStatus(1);
+        when(equipmentStatusService.getByDeviceCode(GAS_CODE)).thenReturn(status);
+
+        when(pipeNetworkBaseService.list()).thenReturn(Collections.emptyList());
+
+        GasMonitorData frame = new GasMonitorData();
+        frame.setId("F-1");
+        frame.setCreateTime(LocalDateTime.parse("2026-09-02T09:00:00"));
+        when(gasMonitorDataService.list(any(LambdaQueryWrapper.class)))
+                .thenReturn(Collections.singletonList(frame));
+        when(waterMonitorDataService.list(any(LambdaQueryWrapper.class))).thenReturn(Collections.emptyList());
+
+        CockpitOverviewMonitoringSnapshot snapshot = adapter.load();
+
+        assertEquals("2026-09-02T09:59:50+08:00", snapshot.getUpdatedAt());
+        assertEquals("ONLINE", snapshot.getDevices().get(0).getMonitoringState());
+        assertEquals("11", snapshot.getMonitoringPoints().get(0).getTypeCode());
+        assertEquals("CURRENT", snapshot.getMetricFreshness().getStatus());
+        assertEquals("2026-09-02T09:00:00+08:00", snapshot.getMetricFreshness().getUpdatedAt());
+    }
+
+    @Test
+    void deviceWithoutStatusStaysUnknownAndPointWithoutUniqueTypeIsNull() {
+        EquipmentBase device = device("E-1", GAS_CODE, 11);
+        when(equipmentBaseService.list()).thenReturn(Collections.singletonList(device));
+        when(equipmentStatusService.getByDeviceCode(GAS_CODE)).thenReturn(null);
+
+        MonitorPoint point = new MonitorPoint();
+        point.setPointId("P-1");
+        point.setPointCode("PO-1");
+        when(monitorPointService.list()).thenReturn(Collections.singletonList(point));
+        when(equipmentBaseService.getEquipmentByPoint("P-1")).thenReturn(Collections.emptyList());
+        when(pipeNetworkBaseService.list()).thenReturn(Collections.emptyList());
+        when(gasMonitorDataService.list(any(LambdaQueryWrapper.class))).thenReturn(Collections.emptyList());
+        when(waterMonitorDataService.list(any(LambdaQueryWrapper.class))).thenReturn(Collections.emptyList());
+
+        CockpitOverviewMonitoringSnapshot snapshot = adapter.load();
+
+        assertNull(snapshot.getDevices().get(0).getMonitoringState());
+        assertNull(snapshot.getMonitoringPoints().get(0).getTypeCode());
+        assertNull(snapshot.getMetricFreshness());
+    }
+
+    @Test
+    void countsAllInfrastructureFacilities() {
+        when(equipmentBaseService.list()).thenReturn(Collections.emptyList());
+        when(monitorPointService.list()).thenReturn(Collections.emptyList());
+        PipeNetworkBase facility = new PipeNetworkBase();
+        facility.setNetworkId("N-1");
+        facility.setNetworkCode("PIPE-001");
+        facility.setNetworkType("主管");
+        when(pipeNetworkBaseService.list()).thenReturn(Collections.singletonList(facility));
+        when(gasMonitorDataService.list(any(LambdaQueryWrapper.class))).thenReturn(Collections.emptyList());
+        when(waterMonitorDataService.list(any(LambdaQueryWrapper.class))).thenReturn(Collections.emptyList());
+
+        CockpitOverviewMonitoringSnapshot snapshot = adapter.load();
+
+        assertEquals(1, snapshot.getInfrastructureFacilityCount());
+        assertEquals("主管", snapshot.getMonitoringFacilities().get(0).getTypeCode());
+    }
+
+    private static EquipmentType type(int id, String name, int parentId) {
+        EquipmentType type = new EquipmentType();
+        type.setId(id);
+        type.setTypeName(name);
+        type.setParentId(parentId);
+        return type;
+    }
+
+    private static EquipmentBase device(String id, String code, int typeId) {
+        EquipmentBase device = new EquipmentBase();
+        device.setEquipmentId(id);
+        device.setEquipmentCode(code);
+        device.setEquipmentName(code);
+        device.setEquipmentTypeId(typeId);
+        return device;
+    }
+}

+ 141 - 0
pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/BusinessCockpitOverviewWarningSettingsAdapterTest.java

@@ -0,0 +1,141 @@
+package com.zksy.web.controller.cockpit;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.zksy.base.alarm.domain.AlarmData;
+import com.zksy.base.alarm.service.AlarmDataService;
+import com.zksy.base.warning.domain.AlarmWarningRel;
+import com.zksy.base.warning.domain.EarlyWarning;
+import com.zksy.base.warning.mapper.AlarmWarningRelMapper;
+import com.zksy.base.warning.service.EarlyWarningService;
+import com.zksy.cockpit.overview.BusinessCockpitOverviewWarningSettingsAdapter;
+import com.zksy.cockpit.overview.CockpitOverviewWarningSettingsSnapshot;
+import com.zksy.ops.domain.WorkOrder;
+import com.zksy.ops.service.WorkOrderService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.sql.Timestamp;
+import java.time.LocalDateTime;
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class BusinessCockpitOverviewWarningSettingsAdapterTest {
+
+    private EarlyWarningService earlyWarningService;
+    private AlarmDataService alarmDataService;
+    private AlarmWarningRelMapper alarmWarningRelMapper;
+    private WorkOrderService workOrderService;
+    private BusinessCockpitOverviewWarningSettingsAdapter adapter;
+
+    @BeforeEach
+    void setUp() {
+        earlyWarningService = mock(EarlyWarningService.class);
+        alarmDataService = mock(AlarmDataService.class);
+        alarmWarningRelMapper = mock(AlarmWarningRelMapper.class);
+        workOrderService = mock(WorkOrderService.class);
+        adapter = new BusinessCockpitOverviewWarningSettingsAdapter(
+                earlyWarningService, alarmDataService, alarmWarningRelMapper, workOrderService);
+    }
+
+    @Test
+    void bridgesWarningWorkOrderThroughRelationAndAggregatesTrend() {
+        EarlyWarning warning = new EarlyWarning();
+        warning.setWarningId("WARN-ID-001");
+        warning.setWarningNo("W-001");
+        warning.setDeviceCode("GAS-001");
+        warning.setPublishTime(LocalDateTime.parse("2026-09-02T09:00:00"));
+        when(earlyWarningService.list()).thenReturn(Collections.singletonList(warning));
+
+        AlarmData alarm = new AlarmData();
+        alarm.setId("A-001");
+        alarm.setDeviceCode("GAS-001");
+        alarm.setWorkOrderId(1001L);
+        alarm.setCreateTime(LocalDateTime.parse("2026-09-02T10:00:00"));
+        when(alarmDataService.list()).thenReturn(Collections.singletonList(alarm));
+
+        AlarmWarningRel relation = new AlarmWarningRel();
+        relation.setRelId("R-1");
+        relation.setWarningId("WARN-ID-001");
+        relation.setAlarmId("A-001");
+        when(alarmWarningRelMapper.selectList(any(QueryWrapper.class)))
+                .thenReturn(Collections.singletonList(relation));
+
+        WorkOrder order = new WorkOrder();
+        order.setOrderId(1001L);
+        order.setOrderNo("WO-001");
+        order.setOrderStatus(5);
+        order.setCreateTime(Timestamp.valueOf("2026-09-02 10:10:00"));
+        when(workOrderService.list()).thenReturn(Collections.singletonList(order));
+
+        CockpitOverviewWarningSettingsSnapshot snapshot = adapter.load();
+
+        assertEquals(2, snapshot.getEvents().size());
+        CockpitOverviewWarningSettingsSnapshot.SourceEvent alarmEvent = snapshot.getEvents().get(0);
+        assertEquals("ALARM", alarmEvent.getEventFamily());
+        assertEquals("A-001", alarmEvent.getEventCode());
+        assertEquals("WO-001", alarmEvent.getWorkOrderCode());
+        assertEquals(Integer.valueOf(5), alarmEvent.getWorkOrderState());
+
+        CockpitOverviewWarningSettingsSnapshot.SourceEvent warningEvent = snapshot.getEvents().get(1);
+        assertEquals("WARNING", warningEvent.getEventFamily());
+        assertEquals("W-001", warningEvent.getEventCode());
+        assertEquals("WO-001", warningEvent.getWorkOrderCode());
+        assertEquals(Integer.valueOf(5), warningEvent.getWorkOrderState());
+
+        CockpitOverviewWarningSettingsSnapshot.SourceTrendPoint warningTrend = snapshot.getTrendPoints().stream()
+                .filter(point -> "WARNING".equals(point.getEventFamily()))
+                .findFirst().orElseThrow(AssertionError::new);
+        assertEquals("2026-09-02", warningTrend.getBucket());
+        assertEquals(1, warningTrend.getTotal());
+        assertEquals(1, warningTrend.getHandled());
+
+        CockpitOverviewWarningSettingsSnapshot.SourceTrendPoint alarmTrend = snapshot.getTrendPoints().stream()
+                .filter(point -> "ALARM".equals(point.getEventFamily()))
+                .findFirst().orElseThrow(AssertionError::new);
+        assertEquals(1, alarmTrend.getTotal());
+        assertEquals(1, alarmTrend.getHandled());
+    }
+
+    @Test
+    void alarmWithoutWorkOrderStaysUnlinkedAndUnhandled() {
+        when(earlyWarningService.list()).thenReturn(Collections.emptyList());
+        AlarmData alarm = new AlarmData();
+        alarm.setId("A-001");
+        alarm.setDeviceCode("GAS-001");
+        alarm.setCreateTime(LocalDateTime.parse("2026-09-02T10:00:00"));
+        when(alarmDataService.list()).thenReturn(Collections.singletonList(alarm));
+        when(alarmWarningRelMapper.selectList(any(QueryWrapper.class)))
+                .thenReturn(Collections.emptyList());
+        when(workOrderService.list()).thenReturn(Collections.emptyList());
+
+        CockpitOverviewWarningSettingsSnapshot snapshot = adapter.load();
+
+        CockpitOverviewWarningSettingsSnapshot.SourceEvent alarmEvent = snapshot.getEvents().get(0);
+        assertNull(alarmEvent.getWorkOrderCode());
+        assertNull(alarmEvent.getWorkOrderState());
+        CockpitOverviewWarningSettingsSnapshot.SourceTrendPoint alarmTrend = snapshot.getTrendPoints().get(0);
+        assertEquals(1, alarmTrend.getTotal());
+        assertEquals(0, alarmTrend.getHandled());
+    }
+
+    @Test
+    void emptySourcesStillProduceEmptySnapshotWithoutNpe() {
+        when(earlyWarningService.list()).thenReturn(Collections.emptyList());
+        when(alarmDataService.list()).thenReturn(Collections.emptyList());
+        when(alarmWarningRelMapper.selectList(any(QueryWrapper.class)))
+                .thenReturn(Collections.emptyList());
+        when(workOrderService.list()).thenReturn(Collections.emptyList());
+
+        CockpitOverviewWarningSettingsSnapshot snapshot = adapter.load();
+
+        assertEquals(0, snapshot.getEvents().size());
+        assertEquals(0, snapshot.getTrendPoints().size());
+        assertNull(snapshot.getUpdatedAt());
+    }
+}

+ 174 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/overview/BusinessCockpitOverviewInfrastructureAdapter.java

@@ -0,0 +1,174 @@
+package com.zksy.cockpit.overview;
+
+import com.zksy.base.domain.EquipmentBase;
+import com.zksy.base.domain.EquipmentType;
+import com.zksy.base.domain.PipeNetworkBase;
+import com.zksy.base.service.EquipmentBaseService;
+import com.zksy.base.service.EquipmentTypeService;
+import com.zksy.base.service.PipeNetworkBaseService;
+import com.zksy.cockpit.foundation.CockpitTypeIndustry;
+import org.springframework.context.annotation.Primary;
+import org.springframework.stereotype.Component;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * 总体概览-基础设施 真实数据适配器(BT-5)。
+ *
+ * <p>智能设备按 {@code equipment_type} 分组计数({@code objectKind=SMART_DEVICE});
+ * 管网设施按 {@code pipe_network_base.network_type} 分组计数({@code objectKind=FACILITY})。
+ * 设施类型无行业字典,{@code industry} 如实为 {@code null};设施长度由 {@code length}(米)换算公里。
+ * {@code updatedAt} 取台账/设施表最新更新时间。
+ */
+@Component
+@Primary
+public class BusinessCockpitOverviewInfrastructureAdapter
+        implements CockpitOverviewInfrastructureAdapter {
+
+    private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
+    private static final ZoneId BUSINESS_ZONE = ZoneId.of("Asia/Shanghai");
+
+    private final EquipmentTypeService equipmentTypeService;
+    private final EquipmentBaseService equipmentBaseService;
+    private final PipeNetworkBaseService pipeNetworkBaseService;
+
+    public BusinessCockpitOverviewInfrastructureAdapter(
+            EquipmentTypeService equipmentTypeService,
+            EquipmentBaseService equipmentBaseService,
+            PipeNetworkBaseService pipeNetworkBaseService) {
+        this.equipmentTypeService = equipmentTypeService;
+        this.equipmentBaseService = equipmentBaseService;
+        this.pipeNetworkBaseService = pipeNetworkBaseService;
+    }
+
+    @Override
+    public CockpitOverviewInfrastructureSnapshot load() {
+        Map<Integer, EquipmentType> types = equipmentTypes();
+
+        List<CockpitOverviewInfrastructureSnapshot.SourceType> sourceTypes = new ArrayList<>();
+        for (EquipmentType type : safeList(equipmentTypeService.list())) {
+            if (type == null || type.getId() == null) {
+                continue;
+            }
+            sourceTypes.add(new CockpitOverviewInfrastructureSnapshot.SourceType(
+                    String.valueOf(type.getId()),
+                    type.getTypeName(),
+                    CockpitTypeIndustry.industryOf(type, types),
+                    null,
+                    "SMART_DEVICE"));
+        }
+
+        List<CockpitOverviewInfrastructureSnapshot.SourceCount> counts = new ArrayList<>();
+        Map<Integer, Integer> deviceCountByType = new LinkedHashMap<>();
+        for (EquipmentBase device : safeList(equipmentBaseService.list())) {
+            if (device == null || device.getEquipmentTypeId() == null) {
+                continue;
+            }
+            deviceCountByType.merge(device.getEquipmentTypeId(), 1, Integer::sum);
+        }
+        for (Map.Entry<Integer, Integer> entry : deviceCountByType.entrySet()) {
+            counts.add(new CockpitOverviewInfrastructureSnapshot.SourceCount(
+                    "SMART_DEVICE", String.valueOf(entry.getKey()), entry.getValue()));
+        }
+
+        Map<String, FacilityAggregate> facilityByType = new LinkedHashMap<>();
+        for (PipeNetworkBase facility : safeList(pipeNetworkBaseService.list())) {
+            if (facility == null) {
+                continue;
+            }
+            String typeCode = trimToNull(facility.getNetworkType());
+            if (typeCode == null) {
+                continue;
+            }
+            FacilityAggregate aggregate = facilityByType.computeIfAbsent(typeCode, key -> new FacilityAggregate());
+            aggregate.count++;
+            if (facility.getLength() != null) {
+                aggregate.lengthMeters = aggregate.lengthMeters == null
+                        ? BigDecimal.ZERO : aggregate.lengthMeters;
+                aggregate.lengthMeters = aggregate.lengthMeters.add(facility.getLength());
+            }
+        }
+        for (Map.Entry<String, FacilityAggregate> entry : facilityByType.entrySet()) {
+            String typeCode = entry.getKey();
+            FacilityAggregate aggregate = entry.getValue();
+            sourceTypes.add(new CockpitOverviewInfrastructureSnapshot.SourceType(
+                    typeCode, typeCode, null, null, "FACILITY"));
+            BigDecimal lengthKm = aggregate.lengthMeters == null
+                    ? null : aggregate.lengthMeters.divide(BigDecimal.valueOf(1000), 3, RoundingMode.HALF_UP);
+            counts.add(new CockpitOverviewInfrastructureSnapshot.SourceCount(
+                    "FACILITY", typeCode, aggregate.count,
+                    new CockpitOverviewInfrastructureSnapshot.SourceValue(lengthKm, "km", null)));
+        }
+
+        LocalDateTime updatedAt = null;
+        for (EquipmentBase device : safeList(equipmentBaseService.list())) {
+            if (device == null) {
+                continue;
+            }
+            updatedAt = latest(updatedAt, device.getUpdateTime());
+            updatedAt = latest(updatedAt, device.getCreateTime());
+        }
+        for (PipeNetworkBase facility : safeList(pipeNetworkBaseService.list())) {
+            if (facility == null) {
+                continue;
+            }
+            updatedAt = latest(updatedAt, facility.getUpdateTime());
+            updatedAt = latest(updatedAt, facility.getCreateTime());
+        }
+
+        return new CockpitOverviewInfrastructureSnapshot(
+                offsetTime(updatedAt), sourceTypes, counts);
+    }
+
+    private Map<Integer, EquipmentType> equipmentTypes() {
+        Map<Integer, EquipmentType> result = new HashMap<>();
+        for (EquipmentType type : safeList(equipmentTypeService.list())) {
+            if (type != null && type.getId() != null) {
+                result.put(type.getId(), type);
+            }
+        }
+        return result;
+    }
+
+    private static LocalDateTime latest(LocalDateTime current, LocalDateTime candidate) {
+        if (candidate == null) {
+            return current;
+        }
+        if (current == null || candidate.isAfter(current)) {
+            return candidate;
+        }
+        return current;
+    }
+
+    private static String offsetTime(LocalDateTime value) {
+        return value == null ? null : TIME_FORMATTER.format(value.atZone(BUSINESS_ZONE).toOffsetDateTime());
+    }
+
+    private static String trimToNull(String value) {
+        if (value == null || value.trim().isEmpty()) {
+            return null;
+        }
+        return value.trim();
+    }
+
+    private static <T> List<T> safeList(List<T> value) {
+        return value == null ? Collections.emptyList() : value.stream().filter(Objects::nonNull).collect(Collectors.toList());
+    }
+
+    private static class FacilityAggregate {
+        private int count;
+        private BigDecimal lengthMeters;
+    }
+}

+ 258 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/overview/BusinessCockpitOverviewMonitoringAdapter.java

@@ -0,0 +1,258 @@
+package com.zksy.cockpit.overview;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.zksy.base.domain.EquipmentBase;
+import com.zksy.base.domain.EquipmentStatus;
+import com.zksy.base.domain.EquipmentType;
+import com.zksy.base.domain.MonitorPoint;
+import com.zksy.base.domain.PipeNetworkBase;
+import com.zksy.base.gas.domain.GasMonitorData;
+import com.zksy.base.gas.service.GasMonitorDataService;
+import com.zksy.base.service.EquipmentBaseService;
+import com.zksy.base.service.EquipmentStatusService;
+import com.zksy.base.service.EquipmentTypeService;
+import com.zksy.base.service.MonitorPointService;
+import com.zksy.base.service.PipeNetworkBaseService;
+import com.zksy.base.water.domain.WaterMonitorData;
+import com.zksy.base.water.service.WaterMonitorDataService;
+import com.zksy.cockpit.foundation.CockpitTypeIndustry;
+import org.springframework.context.annotation.Primary;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * 总体概览-运行监测 真实数据适配器(BT-5)。
+ *
+ * <p>设备来自 {@code equipment_base},监测状态取 {@code equipment_status}(在线→ONLINE、离线→OFFLINE,
+ * 缺失→未知,不产生 STALE——现网无样本过期依据);监测点经 {@code getEquipmentByPoint} 关联设备,
+ * 仅当点内设备类型唯一时推导类型编码;设施按 {@code pipe_network_base.network_type} 计为 FACILITY。
+ * 指标新鲜度取燃气/水位监测表最新入库时间(无帧 → 不提供)。
+ */
+@Component
+@Primary
+public class BusinessCockpitOverviewMonitoringAdapter
+        implements CockpitOverviewMonitoringAdapter {
+
+    private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
+    private static final ZoneId BUSINESS_ZONE = ZoneId.of("Asia/Shanghai");
+
+    private final EquipmentTypeService equipmentTypeService;
+    private final EquipmentBaseService equipmentBaseService;
+    private final MonitorPointService monitorPointService;
+    private final PipeNetworkBaseService pipeNetworkBaseService;
+    private final EquipmentStatusService equipmentStatusService;
+    private final GasMonitorDataService gasMonitorDataService;
+    private final WaterMonitorDataService waterMonitorDataService;
+
+    public BusinessCockpitOverviewMonitoringAdapter(
+            EquipmentTypeService equipmentTypeService,
+            EquipmentBaseService equipmentBaseService,
+            MonitorPointService monitorPointService,
+            PipeNetworkBaseService pipeNetworkBaseService,
+            EquipmentStatusService equipmentStatusService,
+            GasMonitorDataService gasMonitorDataService,
+            WaterMonitorDataService waterMonitorDataService) {
+        this.equipmentTypeService = equipmentTypeService;
+        this.equipmentBaseService = equipmentBaseService;
+        this.monitorPointService = monitorPointService;
+        this.pipeNetworkBaseService = pipeNetworkBaseService;
+        this.equipmentStatusService = equipmentStatusService;
+        this.gasMonitorDataService = gasMonitorDataService;
+        this.waterMonitorDataService = waterMonitorDataService;
+    }
+
+    @Override
+    public CockpitOverviewMonitoringSnapshot load() {
+        Map<Integer, EquipmentType> types = equipmentTypes();
+
+        List<CockpitOverviewMonitoringSnapshot.SourceType> sourceTypes = new ArrayList<>();
+        for (EquipmentType type : safeList(equipmentTypeService.list())) {
+            if (type == null || type.getId() == null) {
+                continue;
+            }
+            sourceTypes.add(new CockpitOverviewMonitoringSnapshot.SourceType(
+                    String.valueOf(type.getId()),
+                    type.getTypeName(),
+                    CockpitTypeIndustry.industryOf(type, types),
+                    null,
+                    "SMART_DEVICE"));
+        }
+        Set<String> facilityTypeCodes = new LinkedHashSet<>();
+        List<CockpitOverviewMonitoringSnapshot.SourceMonitoringFacility> monitoringFacilities = new ArrayList<>();
+        int infrastructureFacilityCount = 0;
+        for (PipeNetworkBase facility : safeList(pipeNetworkBaseService.list())) {
+            if (facility == null || isBlank(facility.getNetworkCode())) {
+                continue;
+            }
+            infrastructureFacilityCount++;
+            String typeCode = trimToNull(facility.getNetworkType());
+            if (typeCode != null) {
+                facilityTypeCodes.add(typeCode);
+            }
+            monitoringFacilities.add(new CockpitOverviewMonitoringSnapshot.SourceMonitoringFacility(
+                    facility.getNetworkCode().trim(), typeCode));
+        }
+        for (String typeCode : facilityTypeCodes) {
+            sourceTypes.add(new CockpitOverviewMonitoringSnapshot.SourceType(
+                    typeCode, typeCode, null, null, "FACILITY"));
+        }
+
+        Map<String, String> pointTypeByCode = pointTypes(safeList(monitorPointService.list()));
+        List<CockpitOverviewMonitoringSnapshot.SourceMonitoringPoint> monitoringPoints = new ArrayList<>();
+        for (MonitorPoint point : safeList(monitorPointService.list())) {
+            if (point == null || isBlank(point.getPointCode())) {
+                continue;
+            }
+            monitoringPoints.add(new CockpitOverviewMonitoringSnapshot.SourceMonitoringPoint(
+                    point.getPointCode().trim(), pointTypeByCode.get(point.getPointCode().trim())));
+        }
+
+        List<CockpitOverviewMonitoringSnapshot.SourceDevice> sourceDevices = new ArrayList<>();
+        LocalDateTime updatedAt = null;
+        for (EquipmentBase device : safeList(equipmentBaseService.list())) {
+            if (device == null || isBlank(device.getEquipmentCode())) {
+                continue;
+            }
+            updatedAt = latest(updatedAt, device.getUpdateTime());
+            updatedAt = latest(updatedAt, device.getCreateTime());
+            sourceDevices.add(new CockpitOverviewMonitoringSnapshot.SourceDevice(
+                    device.getEquipmentCode().trim(),
+                    device.getEquipmentTypeId() == null ? null : String.valueOf(device.getEquipmentTypeId()),
+                    monitoringState(device.getEquipmentCode().trim())));
+        }
+
+        LocalDateTime metricUpdatedAt = metricUpdatedAt();
+        CockpitOverviewMonitoringSnapshot.SourceMetricFreshness metricFreshness =
+                metricUpdatedAt == null ? null
+                        : new CockpitOverviewMonitoringSnapshot.SourceMetricFreshness(
+                                "CURRENT", offsetTime(metricUpdatedAt));
+
+        return new CockpitOverviewMonitoringSnapshot(
+                offsetTime(updatedAt),
+                metricFreshness,
+                sourceTypes,
+                sourceDevices,
+                monitoringPoints,
+                monitoringFacilities,
+                infrastructureFacilityCount);
+    }
+
+    private Map<String, String> pointTypes(List<MonitorPoint> points) {
+        Map<String, String> result = new HashMap<>();
+        for (MonitorPoint point : points) {
+            if (point == null || isBlank(point.getPointId())) {
+                continue;
+            }
+            Set<String> types = new LinkedHashSet<>();
+            try {
+                for (EquipmentBase device : safeList(equipmentBaseService.getEquipmentByPoint(point.getPointId()))) {
+                    if (device != null && device.getEquipmentTypeId() != null) {
+                        types.add(String.valueOf(device.getEquipmentTypeId()));
+                    }
+                }
+            } catch (RuntimeException ignored) {
+                continue;
+            }
+            if (types.size() == 1 && point.getPointCode() != null) {
+                result.put(point.getPointCode().trim(), types.iterator().next());
+            }
+        }
+        return result;
+    }
+
+    /**
+     * 监测状态:在线/离线取自 {@code equipment_status.online_status};缺失 → null(未知),
+     * 不产生 STALE(无样本过期依据)。
+     */
+    private String monitoringState(String deviceCode) {
+        EquipmentStatus status = null;
+        try {
+            status = equipmentStatusService.getByDeviceCode(deviceCode);
+        } catch (Exception ignored) {
+            return null;
+        }
+        if (status == null || status.getOnlineStatus() == null) {
+            return null;
+        }
+        return Integer.valueOf(1).equals(status.getOnlineStatus()) ? "ONLINE" : "OFFLINE";
+    }
+
+    private LocalDateTime metricUpdatedAt() {
+        LocalDateTime latestGas = latestCreateTime(gasMonitorDataService);
+        LocalDateTime latestWater = latestCreateTime(waterMonitorDataService);
+        return latest(latestGas, latestWater);
+    }
+
+    private LocalDateTime latestCreateTime(GasMonitorDataService service) {
+        List<GasMonitorData> rows = safeList(service.list(
+                new LambdaQueryWrapper<GasMonitorData>()
+                        .orderByDesc(GasMonitorData::getCreateTime)
+                        .last("limit 1")));
+        if (rows.isEmpty() || rows.get(0).getCreateTime() == null) {
+            return null;
+        }
+        return rows.get(0).getCreateTime();
+    }
+
+    private LocalDateTime latestCreateTime(WaterMonitorDataService service) {
+        List<WaterMonitorData> rows = safeList(service.list(
+                new LambdaQueryWrapper<WaterMonitorData>()
+                        .orderByDesc(WaterMonitorData::getCreateTime)
+                        .last("limit 1")));
+        if (rows.isEmpty() || rows.get(0).getCreateTime() == null) {
+            return null;
+        }
+        return rows.get(0).getCreateTime();
+    }
+
+    private Map<Integer, EquipmentType> equipmentTypes() {
+        Map<Integer, EquipmentType> result = new HashMap<>();
+        for (EquipmentType type : safeList(equipmentTypeService.list())) {
+            if (type != null && type.getId() != null) {
+                result.put(type.getId(), type);
+            }
+        }
+        return result;
+    }
+
+    private static LocalDateTime latest(LocalDateTime current, LocalDateTime candidate) {
+        if (candidate == null) {
+            return current;
+        }
+        if (current == null || candidate.isAfter(current)) {
+            return candidate;
+        }
+        return current;
+    }
+
+    private static String offsetTime(LocalDateTime value) {
+        return value == null ? null : TIME_FORMATTER.format(value.atZone(BUSINESS_ZONE).toOffsetDateTime());
+    }
+
+    private static String trimToNull(String value) {
+        if (value == null || value.trim().isEmpty()) {
+            return null;
+        }
+        return value.trim();
+    }
+
+    private static <T> List<T> safeList(List<T> value) {
+        return value == null ? Collections.emptyList() : value.stream().filter(Objects::nonNull).collect(Collectors.toList());
+    }
+
+    private static boolean isBlank(String value) {
+        return value == null || value.trim().isEmpty();
+    }
+}

+ 283 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/overview/BusinessCockpitOverviewWarningSettingsAdapter.java

@@ -0,0 +1,283 @@
+package com.zksy.cockpit.overview;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.zksy.base.alarm.domain.AlarmData;
+import com.zksy.base.alarm.service.AlarmDataService;
+import com.zksy.base.warning.domain.AlarmWarningRel;
+import com.zksy.base.warning.domain.EarlyWarning;
+import com.zksy.base.warning.mapper.AlarmWarningRelMapper;
+import com.zksy.base.warning.service.EarlyWarningService;
+import com.zksy.ops.domain.WorkOrder;
+import com.zksy.ops.service.WorkOrderService;
+import org.springframework.context.annotation.Primary;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * 总体概览-预警设置 真实数据适配器(BT-5)。
+ *
+ * <p>报警事件来自 {@code alarm_data}(经 {@code work_order_id → work_order.order_id} 关联工单);
+ * 预警事件来自 {@code early_warning},工单经
+ * {@code alarm_warning_rel.warning_id → alarm_data.work_order_id → work_order} 桥接(README 冻结关联)。
+ * 趋势桶按业务时区日期({@code yyyy-MM-dd})聚合,已处理 = 关联工单已结案(状态 5)。
+ * {@code updatedAt} 取事件与工单的最新时间。
+ */
+@Component
+@Primary
+public class BusinessCockpitOverviewWarningSettingsAdapter
+        implements CockpitOverviewWarningSettingsAdapter {
+
+    private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
+    private static final DateTimeFormatter BUCKET_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+    private static final ZoneId BUSINESS_ZONE = ZoneId.of("Asia/Shanghai");
+    private static final int WORK_ORDER_HANDLED = 5;
+
+    private final EarlyWarningService earlyWarningService;
+    private final AlarmDataService alarmDataService;
+    private final AlarmWarningRelMapper alarmWarningRelMapper;
+    private final WorkOrderService workOrderService;
+
+    public BusinessCockpitOverviewWarningSettingsAdapter(
+            EarlyWarningService earlyWarningService,
+            AlarmDataService alarmDataService,
+            AlarmWarningRelMapper alarmWarningRelMapper,
+            WorkOrderService workOrderService) {
+        this.earlyWarningService = earlyWarningService;
+        this.alarmDataService = alarmDataService;
+        this.alarmWarningRelMapper = alarmWarningRelMapper;
+        this.workOrderService = workOrderService;
+    }
+
+    @Override
+    public CockpitOverviewWarningSettingsSnapshot load() {
+        List<EarlyWarning> warnings = safeList(earlyWarningService.list());
+        List<AlarmData> alarms = safeList(alarmDataService.list());
+        List<AlarmWarningRel> relations = safeList(alarmWarningRelMapper.selectList(new QueryWrapper<AlarmWarningRel>()));
+        List<WorkOrder> orders = safeList(workOrderService.list());
+
+        Map<Long, WorkOrder> orderByOrderId = new HashMap<>();
+        for (WorkOrder order : orders) {
+            if (order != null && order.getOrderId() != null) {
+                orderByOrderId.put(order.getOrderId(), order);
+            }
+        }
+
+        Map<String, List<String>> warningNosByAlarmId = new HashMap<>();
+        Map<String, String> warningNoById = new HashMap<>();
+        for (EarlyWarning warning : warnings) {
+            if (warning == null || isBlank(warning.getWarningId()) || isBlank(warning.getWarningNo())) {
+                continue;
+            }
+            warningNoById.put(warning.getWarningId(), warning.getWarningNo().trim());
+        }
+        for (AlarmWarningRel relation : relations) {
+            if (relation == null || isBlank(relation.getAlarmId())) {
+                continue;
+            }
+            String warningNo = warningNoById.get(relation.getWarningId());
+            if (warningNo != null) {
+                warningNosByAlarmId.computeIfAbsent(relation.getAlarmId(), key -> new ArrayList<>()).add(warningNo);
+            }
+        }
+
+        List<CockpitOverviewWarningSettingsSnapshot.SourceEvent> events = new ArrayList<>();
+        LocalDateTime updatedAt = null;
+
+        for (AlarmData alarm : alarms) {
+            if (alarm == null || isBlank(alarm.getId())) {
+                continue;
+            }
+            updatedAt = latest(updatedAt, alarm.getUpdateTime());
+            updatedAt = latest(updatedAt, alarm.getCreateTime());
+            WorkOrder order = alarm.getWorkOrderId() == null ? null : orderByOrderId.get(alarm.getWorkOrderId());
+            if (order != null) {
+                updatedAt = latest(updatedAt, toLocal(order.getUpdateTime()));
+                updatedAt = latest(updatedAt, toLocal(order.getCreateTime()));
+            }
+            events.add(new CockpitOverviewWarningSettingsSnapshot.SourceEvent(
+                    "ALARM",
+                    alarm.getId().trim(),
+                    trimToNull(alarm.getDeviceCode()),
+                    order == null ? null : order.getOrderNo(),
+                    order == null || order.getOrderStatus() == null ? null : order.getOrderStatus()));
+        }
+
+        for (EarlyWarning warning : warnings) {
+            if (warning == null || isBlank(warning.getWarningNo())) {
+                continue;
+            }
+            updatedAt = latest(updatedAt, warning.getUpdateTime());
+            updatedAt = latest(updatedAt, warning.getPublishTime());
+            List<String> linkedAlarmIds = new ArrayList<>();
+            for (AlarmWarningRel relation : relations) {
+                if (relation != null && warning.getWarningId() != null
+                        && warning.getWarningId().equals(relation.getWarningId())) {
+                    linkedAlarmIds.add(relation.getAlarmId());
+                }
+            }
+            WorkOrder order = null;
+            for (String alarmId : linkedAlarmIds) {
+                AlarmData alarm = alarms.stream()
+                        .filter(candidate -> candidate != null && alarmId != null && alarmId.equals(candidate.getId()))
+                        .findFirst().orElse(null);
+                if (alarm != null && alarm.getWorkOrderId() != null) {
+                    order = orderByOrderId.get(alarm.getWorkOrderId());
+                    if (order != null) {
+                        break;
+                    }
+                }
+            }
+            if (order != null) {
+                updatedAt = latest(updatedAt, toLocal(order.getUpdateTime()));
+                updatedAt = latest(updatedAt, toLocal(order.getCreateTime()));
+            }
+            events.add(new CockpitOverviewWarningSettingsSnapshot.SourceEvent(
+                    "WARNING",
+                    warning.getWarningNo().trim(),
+                    trimToNull(warning.getDeviceCode()),
+                    order == null ? null : order.getOrderNo(),
+                    order == null || order.getOrderStatus() == null ? null : order.getOrderStatus()));
+        }
+
+        List<CockpitOverviewWarningSettingsSnapshot.SourceTrendPoint> trendPoints = new ArrayList<>();
+        addBucket(trendPoints, "WARNING", bucketFor(warnings, alarms, warningNosByAlarmId, orderByOrderId));
+        addBucket(trendPoints, "ALARM", bucketFor(alarms, orderByOrderId));
+
+        return new CockpitOverviewWarningSettingsSnapshot(offsetTime(updatedAt), events, trendPoints);
+    }
+
+    private Map<String, Counter> bucketFor(
+            List<EarlyWarning> warnings,
+            List<AlarmData> alarms,
+            Map<String, List<String>> warningNosByAlarmId,
+            Map<Long, WorkOrder> orderByOrderId) {
+        Map<String, Counter> buckets = new LinkedHashMap<>();
+        for (EarlyWarning warning : warnings) {
+            if (warning == null || isBlank(warning.getWarningNo())) {
+                continue;
+            }
+            LocalDateTime time = firstNonNull(warning.getPublishTime(), warning.getCreateTime());
+            if (time == null) {
+                continue;
+            }
+            Counter counter = buckets.computeIfAbsent(bucket(time), key -> new Counter());
+            counter.total++;
+            if (isHandledWarning(warning, warningNosByAlarmId, alarms, orderByOrderId)) {
+                counter.handled++;
+            }
+        }
+        return buckets;
+    }
+
+    private Map<String, Counter> bucketFor(List<AlarmData> alarms, Map<Long, WorkOrder> orderByOrderId) {
+        Map<String, Counter> buckets = new LinkedHashMap<>();
+        for (AlarmData alarm : alarms) {
+            if (alarm == null || isBlank(alarm.getId())) {
+                continue;
+            }
+            LocalDateTime time = firstNonNull(alarm.getUpdateTime(), alarm.getCreateTime());
+            if (time == null) {
+                continue;
+            }
+            Counter counter = buckets.computeIfAbsent(bucket(time), key -> new Counter());
+            counter.total++;
+            WorkOrder order = alarm.getWorkOrderId() == null ? null : orderByOrderId.get(alarm.getWorkOrderId());
+            if (order != null && WORK_ORDER_HANDLED == order.getOrderStatus()) {
+                counter.handled++;
+            }
+        }
+        return buckets;
+    }
+
+    private boolean isHandledWarning(
+            EarlyWarning warning,
+            Map<String, List<String>> warningNosByAlarmId,
+            List<AlarmData> alarms,
+            Map<Long, WorkOrder> orderByOrderId) {
+        if (warning.getWarningId() == null) {
+            return false;
+        }
+        for (AlarmData alarm : alarms) {
+            if (alarm == null || alarm.getWorkOrderId() == null) {
+                continue;
+            }
+            List<String> linked = warningNosByAlarmId.get(alarm.getId());
+            if (linked != null && linked.contains(warning.getWarningNo().trim())) {
+                WorkOrder order = orderByOrderId.get(alarm.getWorkOrderId());
+                if (order != null && WORK_ORDER_HANDLED == order.getOrderStatus()) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    private void addBucket(
+            List<CockpitOverviewWarningSettingsSnapshot.SourceTrendPoint> trendPoints,
+            String family,
+            Map<String, Counter> buckets) {
+        for (Map.Entry<String, Counter> entry : buckets.entrySet()) {
+            trendPoints.add(new CockpitOverviewWarningSettingsSnapshot.SourceTrendPoint(
+                    family, entry.getKey(), entry.getValue().total, entry.getValue().handled));
+        }
+    }
+
+    private static String bucket(LocalDateTime time) {
+        return BUCKET_FORMATTER.format(time.atZone(BUSINESS_ZONE).toLocalDate());
+    }
+
+    private static LocalDateTime firstNonNull(LocalDateTime primary, LocalDateTime fallback) {
+        return primary != null ? primary : fallback;
+    }
+
+    private static LocalDateTime toLocal(java.util.Date value) {
+        return value == null ? null
+                : LocalDateTime.ofInstant(value.toInstant(), BUSINESS_ZONE);
+    }
+
+    private static LocalDateTime latest(LocalDateTime current, LocalDateTime candidate) {
+        if (candidate == null) {
+            return current;
+        }
+        if (current == null || candidate.isAfter(current)) {
+            return candidate;
+        }
+        return current;
+    }
+
+    private static String offsetTime(LocalDateTime value) {
+        return value == null ? null : TIME_FORMATTER.format(value.atZone(BUSINESS_ZONE).toOffsetDateTime());
+    }
+
+    private static String trimToNull(String value) {
+        if (value == null || value.trim().isEmpty()) {
+            return null;
+        }
+        return value.trim();
+    }
+
+    private static <T> List<T> safeList(List<T> value) {
+        return value == null ? Collections.emptyList() : value.stream().filter(Objects::nonNull).collect(Collectors.toList());
+    }
+
+    private static boolean isBlank(String value) {
+        return value == null || value.trim().isEmpty();
+    }
+
+    private static class Counter {
+        private int total;
+        private int handled;
+    }
+}