瀏覽代碼

fix(cockpit): unify facility count sources

Kazerin 1 周之前
父節點
當前提交
9683479429
共有 11 個文件被更改,包括 403 次插入 和 513 次删除
  1. 6 10
      pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/BusinessCockpitInfrastructureBaseArchivesAdapterTest.java
  2. 7 4
      pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/BusinessCockpitInfrastructureManagementInformationAdapterTest.java
  3. 14 11
      pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/BusinessCockpitOverviewInfrastructureAdapterTest.java
  4. 144 0
      pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/CockpitFacilityCountCrossEndpointContractTest.java
  5. 8 12
      pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/CockpitPipeNetworkCrossEndpointContractTest.java
  6. 100 138
      pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/infrastructure/BusinessCockpitInfrastructureBaseArchivesAdapter.java
  7. 45 136
      pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/infrastructure/BusinessCockpitInfrastructureManagementInformationAdapter.java
  8. 8 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/infrastructure/CockpitFacilityCatalog.java
  9. 1 1
      pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/infrastructure/CockpitInfrastructureBaseArchivesService.java
  10. 8 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/infrastructure/CockpitInfrastructureBaseArchivesSnapshot.java
  11. 62 201
      pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/overview/BusinessCockpitOverviewInfrastructureAdapter.java

+ 6 - 10
pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/BusinessCockpitInfrastructureBaseArchivesAdapterTest.java

@@ -1,6 +1,5 @@
 package com.zksy.web.controller.cockpit;
 
-import com.baomidou.mybatisplus.core.conditions.Wrapper;
 import com.zksy.WaterSupply.WaterPlantInfo.domain.WaterPlantInfo;
 import com.zksy.WaterSupply.WaterPlantInfo.service.IWaterPlantInfoService;
 import com.zksy.WaterSupply.WaterPumpStation.domain.PumpStationType;
@@ -17,10 +16,7 @@ import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
 import java.math.BigDecimal;
-import java.time.Clock;
-import java.time.Instant;
 import java.time.LocalDate;
-import java.time.ZoneOffset;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
@@ -30,7 +26,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
@@ -50,7 +45,8 @@ class BusinessCockpitInfrastructureBaseArchivesAdapterTest {
         engineeringFacilityService = mock(EngineeringFacilityService.class);
         adapter = new BusinessCockpitInfrastructureBaseArchivesAdapter(
                 plantService, pumpStationService, pipeNetworkBaseService, engineeringFacilityService,
-                Clock.fixed(Instant.parse("2026-09-02T02:00:00Z"), ZoneOffset.UTC));
+                mock(com.zksy.WaterSupply.WaterSourceInfo.service.IWaterSourceInfoService.class),
+                mock(com.zksy.base.service.ManholeBaseService.class));
     }
 
     @Test
@@ -80,14 +76,14 @@ class BusinessCockpitInfrastructureBaseArchivesAdapterTest {
         drainage.setFacilityType("调蓄设施");
 
         when(plantService.list()).thenReturn(Collections.singletonList(plant));
-        when(pumpStationService.list(any(Wrapper.class))).thenReturn(Collections.singletonList(pump));
+        when(pumpStationService.list()).thenReturn(Collections.singletonList(pump));
         when(pipeNetworkBaseService.list()).thenReturn(Collections.singletonList(pipe));
         when(engineeringFacilityService.list()).thenReturn(Collections.singletonList(drainage));
 
         CockpitInfrastructureBaseArchivesSnapshot snapshot = adapter.load();
 
         assertEquals(4, snapshot.getFacilities().size());
-        assertEquals(8, snapshot.getTypes().size());
+        assertEquals(10, snapshot.getTypes().size());
         assertEquals("DRAINAGE_STORAGE", snapshot.getTypes().get(6).getCode());
         // 设施表都没有 equipment_base.district 那种权威片区来源,全部如实报未分区。
         assertTrue(snapshot.getFacilities().stream().allMatch(item -> item.getDistrict() == null),
@@ -124,7 +120,7 @@ class BusinessCockpitInfrastructureBaseArchivesAdapterTest {
         PipeNetworkBase gas = pipe("GAS-PIPE", "燃气管线", "GAS", "500");
         PipeNetworkBase unknown = pipe("UNKNOWN-PIPE", "未知管线", "99", "500");
         when(plantService.list()).thenReturn(Collections.emptyList());
-        when(pumpStationService.list(any(Wrapper.class))).thenReturn(Collections.emptyList());
+        when(pumpStationService.list()).thenReturn(Collections.emptyList());
         when(pipeNetworkBaseService.list()).thenReturn(Arrays.asList(drainage, gas, unknown));
         when(engineeringFacilityService.list()).thenReturn(Collections.emptyList());
 
@@ -148,7 +144,7 @@ class BusinessCockpitInfrastructureBaseArchivesAdapterTest {
         PipeNetworkBase drainage = pipe("P-DRAIN", "排水管线", "1", "1000");
         PipeNetworkBase water = pipe("P-WATER", "供水管线", "2", "1500");
         when(plantService.list()).thenReturn(Collections.emptyList());
-        when(pumpStationService.list(any(Wrapper.class))).thenReturn(Collections.emptyList());
+        when(pumpStationService.list()).thenReturn(Collections.emptyList());
         when(pipeNetworkBaseService.list()).thenReturn(Arrays.asList(gas, drainage, water));
         when(engineeringFacilityService.list()).thenReturn(Collections.emptyList());
 

+ 7 - 4
pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/BusinessCockpitInfrastructureManagementInformationAdapterTest.java

@@ -11,6 +11,7 @@ import com.zksy.base.service.EquipmentTypeService;
 import com.zksy.base.service.ManholeBaseService;
 import com.zksy.base.service.PipeNetworkBaseService;
 import com.zksy.cockpit.infrastructure.BusinessCockpitInfrastructureManagementInformationAdapter;
+import com.zksy.cockpit.infrastructure.BusinessCockpitInfrastructureBaseArchivesAdapter;
 import com.zksy.cockpit.infrastructure.CockpitInfrastructureManagementInformationSnapshot;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -44,9 +45,11 @@ class BusinessCockpitInfrastructureManagementInformationAdapterTest {
         adapter = new BusinessCockpitInfrastructureManagementInformationAdapter(
                 equipmentBaseService,
                 equipmentTypeService,
-                manholeBaseService,
-                pipeNetworkBaseService,
-                waterSourceInfoService);
+                new BusinessCockpitInfrastructureBaseArchivesAdapter(
+                        mock(com.zksy.WaterSupply.WaterPlantInfo.service.IWaterPlantInfoService.class),
+                        mock(com.zksy.WaterSupply.WaterPumpStation.service.IWaterPumpStationService.class),
+                        pipeNetworkBaseService, mock(com.zksy.drainage.service.EngineeringFacilityService.class),
+                        waterSourceInfoService, manholeBaseService));
     }
 
     @Test
@@ -163,7 +166,7 @@ class BusinessCockpitInfrastructureManagementInformationAdapterTest {
             List<CockpitInfrastructureManagementInformationSnapshot.SourceRelation> relations,
             String code) {
         return relations.stream()
-                .filter(item -> code.equals(item.getObjectCode()))
+                .filter(item -> code.equals(item.getObjectCode()) || item.getObjectCode().endsWith("|" + code))
                 .findFirst()
                 .orElseThrow(AssertionError::new);
     }

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

@@ -1,6 +1,5 @@
 package com.zksy.web.controller.cockpit;
 
-import com.baomidou.mybatisplus.core.conditions.Wrapper;
 import com.zksy.WaterSupply.WaterPlantInfo.domain.WaterPlantInfo;
 import com.zksy.WaterSupply.WaterPlantInfo.service.IWaterPlantInfoService;
 import com.zksy.WaterSupply.WaterPumpStation.domain.PumpStationType;
@@ -12,7 +11,10 @@ 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.infrastructure.BusinessCockpitInfrastructureBaseArchivesAdapter;
+import com.zksy.cockpit.infrastructure.CockpitFacilityCatalog;
 import com.zksy.cockpit.overview.BusinessCockpitOverviewInfrastructureAdapter;
+import com.zksy.drainage.service.EngineeringFacilityService;
 import com.zksy.cockpit.overview.CockpitOverviewInfrastructureSnapshot;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -25,7 +27,6 @@ import java.util.Collections;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 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.times;
 import static org.mockito.Mockito.verify;
@@ -47,9 +48,11 @@ class BusinessCockpitOverviewInfrastructureAdapterTest {
         pipeNetworkBaseService = mock(PipeNetworkBaseService.class);
         plantService = mock(IWaterPlantInfoService.class);
         pumpStationService = mock(IWaterPumpStationService.class);
-        adapter = new BusinessCockpitOverviewInfrastructureAdapter(
-                equipmentTypeService, equipmentBaseService, pipeNetworkBaseService,
-                plantService, pumpStationService);
+        CockpitFacilityCatalog catalog = new BusinessCockpitInfrastructureBaseArchivesAdapter(
+                plantService, pumpStationService, pipeNetworkBaseService, mock(EngineeringFacilityService.class),
+                mock(com.zksy.WaterSupply.WaterSourceInfo.service.IWaterSourceInfoService.class),
+                mock(com.zksy.base.service.ManholeBaseService.class));
+        adapter = new BusinessCockpitOverviewInfrastructureAdapter(equipmentTypeService, equipmentBaseService, catalog);
     }
 
     @Test
@@ -73,7 +76,7 @@ class BusinessCockpitOverviewInfrastructureAdapterTest {
 
         CockpitOverviewInfrastructureSnapshot snapshot = adapter.load();
 
-        assertEquals(3, snapshot.getTypes().size());
+        assertEquals(12, snapshot.getTypes().size());
         assertEquals("2026-09-02T09:59:50+08:00", snapshot.getUpdatedAt());
 
         long deviceCount = snapshot.getCounts().stream()
@@ -103,7 +106,7 @@ class BusinessCockpitOverviewInfrastructureAdapterTest {
         when(equipmentTypeService.list()).thenReturn(Collections.emptyList());
         when(equipmentBaseService.list()).thenReturn(Collections.emptyList());
         when(plantService.list()).thenReturn(Collections.emptyList());
-        when(pumpStationService.list(any(Wrapper.class))).thenReturn(Collections.emptyList());
+        when(pumpStationService.list()).thenReturn(Collections.emptyList());
 
         java.util.List<PipeNetworkBase> pipes = new java.util.ArrayList<>();
         pipes.add(pipe("P-GAS", "燃气管线", "0", "500"));
@@ -152,20 +155,20 @@ class BusinessCockpitOverviewInfrastructureAdapterTest {
         WaterPumpStation unclassified = new WaterPumpStation();
         unclassified.setStationCode("PS-3");
         unclassified.setStationType("unknown");
-        when(pumpStationService.list(any(Wrapper.class))).thenReturn(Arrays.asList(water, drainage, unclassified));
+        when(pumpStationService.list()).thenReturn(Arrays.asList(water, drainage, unclassified));
 
         CockpitOverviewInfrastructureSnapshot snapshot = adapter.load();
         assertEquals("2026-09-03T08:00:00+08:00", snapshot.getUpdatedAt());
-        assertEquals(3, snapshot.getCounts().size());
+        assertEquals(4, snapshot.getCounts().size());
         assertEquals("WATER_PLANT", snapshot.getCounts().get(0).getTypeCode());
         assertEquals("WATER_PUMP_STATION", snapshot.getCounts().get(1).getTypeCode());
         assertEquals("DRAINAGE_PUMP_STATION", snapshot.getCounts().get(2).getTypeCode());
-        assertEquals("DRAINAGE", snapshot.getTypes().get(2).getIndustry());
+        assertEquals("DRAINAGE", snapshot.getTypes().get(7).getIndustry());
         verify(equipmentTypeService, times(1)).list();
         verify(equipmentBaseService, times(1)).list();
         verify(pipeNetworkBaseService, times(1)).list();
         verify(plantService, times(1)).list();
-        verify(pumpStationService, times(1)).list(any(Wrapper.class));
+        verify(pumpStationService, times(1)).list();
     }
 
     private static EquipmentType type(int id, String name, int parentId) {

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

@@ -0,0 +1,144 @@
+package com.zksy.web.controller.cockpit;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.zksy.WaterSupply.WaterPlantInfo.domain.WaterPlantInfo;
+import com.zksy.WaterSupply.WaterPlantInfo.service.IWaterPlantInfoService;
+import com.zksy.WaterSupply.WaterPumpStation.domain.PumpStationType;
+import com.zksy.WaterSupply.WaterPumpStation.domain.WaterPumpStation;
+import com.zksy.WaterSupply.WaterPumpStation.service.IWaterPumpStationService;
+import com.zksy.WaterSupply.WaterSourceInfo.domain.WaterSourceInfo;
+import com.zksy.WaterSupply.WaterSourceInfo.service.IWaterSourceInfoService;
+import com.zksy.base.domain.ManholeBase;
+import com.zksy.base.domain.PipeNetworkBase;
+import com.zksy.base.service.*;
+import com.zksy.cockpit.infrastructure.*;
+import com.zksy.cockpit.overview.*;
+import com.zksy.drainage.domain.EngineeringFacility;
+import com.zksy.drainage.service.EngineeringFacilityService;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+import java.time.Clock;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.ZoneOffset;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/** 只替换表读取,真实设施来源与三套服务贯通;固定数据与时钟。 */
+class CockpitFacilityCountCrossEndpointContractTest {
+    private static final Clock CLOCK = Clock.fixed(Instant.parse("2026-10-12T00:00:00Z"), ZoneOffset.UTC);
+    private static final ObjectMapper JSON = new ObjectMapper();
+
+    @Test
+    void allLegalFacilityTablesShareTotalsIndustriesAndDistrictSums() {
+        IWaterPlantInfoService plants = mock(IWaterPlantInfoService.class);
+        IWaterPumpStationService stations = mock(IWaterPumpStationService.class);
+        PipeNetworkBaseService pipes = mock(PipeNetworkBaseService.class);
+        EngineeringFacilityService engineering = mock(EngineeringFacilityService.class);
+        IWaterSourceInfoService sources = mock(IWaterSourceInfoService.class);
+        ManholeBaseService manholes = mock(ManholeBaseService.class);
+        WaterPlantInfo plant = new WaterPlantInfo();
+        plant.setPlantCode("SHARED-CODE");
+        plant.setCommissionDate(LocalDate.of(2030, 1, 1));
+        WaterPumpStation water = station("WATER-PUMP", PumpStationType.WATER_SUPPLY);
+        WaterPumpStation drainage = station("DRAIN-PUMP", PumpStationType.DRAINAGE);
+        WaterPumpStation unknownStation = station("UNKNOWN-PUMP", "unknown");
+        WaterSourceInfo source = new WaterSourceInfo();
+        source.setSourceId("SHARED-CODE"); // 跨类型同码是不同设施,不能由管理信息误去重。
+        ManholeBase manhole = new ManholeBase();
+        manhole.setManholeCode("MH-1");
+        EngineeringFacility gate = new EngineeringFacility();
+        gate.setId("GATE-1");
+        gate.setFacilityType("闸门");
+        EngineeringFacility pump = new EngineeringFacility();
+        pump.setId("ENGINEERING-PUMP");
+        pump.setFacilityType("泵站");
+        EngineeringFacility unknown = new EngineeringFacility();
+        unknown.setId("UNKNOWN-1");
+        unknown.setFacilityType("unclassified");
+        WaterPlantInfo missingCode = new WaterPlantInfo();
+        when(plants.list()).thenReturn(Arrays.asList(plant, missingCode));
+        when(stations.list()).thenReturn(Arrays.asList(water, drainage, unknownStation));
+        when(pipes.list()).thenReturn(Arrays.asList(pipe("GAS", "0"), pipe("DRAIN", "1"), pipe("WATER", "2"), pipe("UNKNOWN", "99")));
+        when(engineering.list()).thenReturn(Arrays.asList(gate, pump, unknown));
+        when(sources.list()).thenReturn(Collections.singletonList(source));
+        when(manholes.list()).thenReturn(Collections.singletonList(manhole));
+        CockpitFacilityCatalog catalog = new BusinessCockpitInfrastructureBaseArchivesAdapter(
+                plants, stations, pipes, engineering, sources, manholes);
+        assertThreeEndpoints(catalog, 10, 4);
+    }
+
+    @Test
+    void emptyCatalogProducesZeroCountsAcrossThreeEndpoints() {
+        assertThreeEndpoints(() -> new CockpitInfrastructureBaseArchivesSnapshot(
+                "2026-10-12T08:00:00+08:00", Collections.emptyList(), Collections.emptyList()), 0, 0);
+    }
+
+    private void assertThreeEndpoints(CockpitFacilityCatalog catalog, int total, int excluded) {
+        EquipmentBaseService devices = mock(EquipmentBaseService.class);
+        EquipmentTypeService types = mock(EquipmentTypeService.class);
+        JsonNode archives = JSON.valueToTree(new CockpitInfrastructureBaseArchivesService(catalog::load, CLOCK)
+                .load("archives", null, null, null, null));
+        JsonNode overview = JSON.valueToTree(new CockpitOverviewInfrastructureService(
+                new BusinessCockpitOverviewInfrastructureAdapter(types, devices, catalog), CLOCK).load("overview"));
+        CockpitInfrastructureManagementInformationService managementService = new CockpitInfrastructureManagementInformationService(
+                new BusinessCockpitInfrastructureManagementInformationAdapter(devices, types, catalog), CLOCK);
+        JsonNode management = JSON.valueToTree(managementService.load("management", null, null));
+        for (JsonNode response : Arrays.asList(archives, overview, management)) {
+            assertEquals(total, response.path("totals").path("facilityCount").asInt());
+        }
+        Map<String, Integer> expected = new HashMap<>();
+        expected.put("GAS", total == 0 ? 0 : 1);
+        expected.put("WATER_SUPPLY", total == 0 ? 0 : 4);
+        expected.put("DRAINAGE", total == 0 ? 0 : 4);
+        expected.put("MANHOLE", total == 0 ? 0 : 1);
+        assertEquals(expected, industryCounts(archives, "industryDistribution", "code", "count"));
+        assertEquals(expected, industryCounts(overview, "industries", "code", "facilityCount"));
+        assertEquals(expected, industryCounts(management, "districtIndustry", "industry", "facilityCount"));
+        assertEquals(total, sum(archives, "districtDistribution", "count"), "未来日期不能让片区分布漏设施");
+        assertEquals(total, sum(management, "districtIndustry", "facilityCount"));
+        assertEquals(excluded, archives.path("excluded").path("unknownTypeCount").asInt());
+        assertEquals(excluded, overview.path("excluded").path("unknownTypeCount").asInt());
+        assertEquals(excluded, management.path("excluded").path("unknownIndustryCount").asInt());
+        for (String industry : expected.keySet()) {
+            JsonNode filtered = JSON.valueToTree(managementService.load("filtered", industry, "UNASSIGNED"));
+            assertEquals(expected.get(industry).intValue(), filtered.path("totals").path("facilityCount").asInt());
+        }
+    }
+
+    private static Map<String, Integer> industryCounts(JsonNode response, String field, String key, String value) {
+        Map<String, Integer> result = new HashMap<>();
+        for (String industry : Arrays.asList("GAS", "WATER_SUPPLY", "DRAINAGE", "MANHOLE")) result.put(industry, 0);
+        for (JsonNode row : response.path(field)) result.merge(row.path(key).asText(), row.path(value).asInt(), Integer::sum);
+        return result;
+    }
+
+    private static int sum(JsonNode response, String field, String value) {
+        int result = 0;
+        for (JsonNode row : response.path(field)) result += row.path(value).asInt();
+        return result;
+    }
+
+    private static WaterPumpStation station(String code, String type) {
+        WaterPumpStation station = new WaterPumpStation();
+        station.setStationCode(code);
+        station.setStationType(type);
+        return station;
+    }
+
+    private static PipeNetworkBase pipe(String code, String type) {
+        PipeNetworkBase pipe = new PipeNetworkBase();
+        pipe.setNetworkCode(code);
+        pipe.setNetworkType(type);
+        pipe.setLength(BigDecimal.valueOf(1000));
+        return pipe;
+    }
+}

+ 8 - 12
pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/CockpitPipeNetworkCrossEndpointContractTest.java

@@ -1,7 +1,5 @@
 package com.zksy.web.controller.cockpit;
 
-import com.baomidou.mybatisplus.core.MybatisConfiguration;
-import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.zksy.WaterSupply.WaterPlantInfo.service.IWaterPlantInfoService;
@@ -24,7 +22,8 @@ import com.zksy.cockpit.infrastructure.CockpitInfrastructureOperatingInformation
 import com.zksy.cockpit.overview.BusinessCockpitOverviewInfrastructureAdapter;
 import com.zksy.cockpit.overview.CockpitOverviewInfrastructureService;
 import com.zksy.drainage.service.EngineeringFacilityService;
-import org.apache.ibatis.builder.MapperBuilderAssistant;
+
+
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
 
@@ -47,11 +46,6 @@ class CockpitPipeNetworkCrossEndpointContractTest {
     private static final Clock CLOCK = Clock.fixed(Instant.parse("2026-10-12T00:00:00Z"), ZoneOffset.UTC);
     private static final ObjectMapper JSON = new ObjectMapper();
 
-    @BeforeAll
-    static void initializeLambdaCache() {
-        TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""),
-                WaterPumpStation.class);
-    }
 
     @Test
     void numericAndHistoricalTypesHaveEqualIndustryCountsAcrossFiveEndpoints() {
@@ -81,19 +75,21 @@ class CockpitPipeNetworkCrossEndpointContractTest {
         EquipmentTypeService equipmentTypes = mock(EquipmentTypeService.class);
         EquipmentBaseService equipment = mock(EquipmentBaseService.class);
 
+        com.zksy.cockpit.infrastructure.CockpitFacilityCatalog catalog =
+                new BusinessCockpitInfrastructureBaseArchivesAdapter(plants, stations, pipes, engineering,
+                        mock(IWaterSourceInfoService.class), mock(ManholeBaseService.class));
         // 只替换表读取;类型解析、未知计数、行业归属与响应构造全部走真实代码。
         JsonNode archives = JSON.valueToTree(new CockpitInfrastructureBaseArchivesService(
-                new BusinessCockpitInfrastructureBaseArchivesAdapter(plants, stations, pipes, engineering, CLOCK),
+                catalog::load,
                 CLOCK).load("archives", null, null, null, null));
         JsonNode management = JSON.valueToTree(new CockpitInfrastructureManagementInformationService(
-                new BusinessCockpitInfrastructureManagementInformationAdapter(equipment, equipmentTypes,
-                        mock(ManholeBaseService.class), pipes, mock(IWaterSourceInfoService.class)),
+                new BusinessCockpitInfrastructureManagementInformationAdapter(equipment, equipmentTypes, catalog),
                 CLOCK).load("management", null, null));
         JsonNode operating = JSON.valueToTree(new CockpitInfrastructureOperatingInformationService(
                 new BusinessCockpitInfrastructureOperatingInformationAdapter(plants, stations, pipes, engineering),
                 CLOCK, 60).load("operating", null, null, null, null));
         JsonNode overview = JSON.valueToTree(new CockpitOverviewInfrastructureService(
-                new BusinessCockpitOverviewInfrastructureAdapter(equipmentTypes, equipment, pipes, plants, stations),
+                new BusinessCockpitOverviewInfrastructureAdapter(equipmentTypes, equipment, catalog),
                 CLOCK).load("overview"));
         JsonNode boundary = JSON.valueToTree(new CockpitDeviceBoundaryService(
                 new BusinessCockpitDeviceBoundaryAdapter(equipmentTypes, equipment, pipes), CLOCK).load("boundary"));

+ 100 - 138
pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/infrastructure/BusinessCockpitInfrastructureBaseArchivesAdapter.java

@@ -1,12 +1,15 @@
 package com.zksy.cockpit.infrastructure;
 
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.zksy.WaterSupply.WaterPlantInfo.domain.WaterPlantInfo;
 import com.zksy.WaterSupply.WaterPlantInfo.service.IWaterPlantInfoService;
 import com.zksy.WaterSupply.WaterPumpStation.domain.PumpStationType;
 import com.zksy.WaterSupply.WaterPumpStation.domain.WaterPumpStation;
 import com.zksy.WaterSupply.WaterPumpStation.service.IWaterPumpStationService;
+import com.zksy.WaterSupply.WaterSourceInfo.domain.WaterSourceInfo;
+import com.zksy.WaterSupply.WaterSourceInfo.service.IWaterSourceInfoService;
+import com.zksy.base.domain.ManholeBase;
 import com.zksy.base.domain.PipeNetworkBase;
+import com.zksy.base.service.ManholeBaseService;
 import com.zksy.base.service.PipeNetworkBaseService;
 import com.zksy.cockpit.foundation.CockpitPipeNetworkType;
 import com.zksy.drainage.domain.EngineeringFacility;
@@ -15,8 +18,8 @@ import org.springframework.context.annotation.Primary;
 import org.springframework.stereotype.Component;
 
 import java.math.BigDecimal;
-import java.time.Clock;
 import java.time.LocalDate;
+import java.time.LocalDateTime;
 import java.time.ZoneId;
 import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
@@ -27,180 +30,139 @@ import java.util.Objects;
 import java.util.stream.Collectors;
 
 /**
- * 基础档案真实数据适配器(BT-5 第 6 片)。
- *
- * <p>组合现有设施表,不新增投影:水厂、共用泵站、统一管网基础表和排水工程设施。
- * 只有实体明确提供的字段才映射;缺少统一来源的规模、片区或长度保持 null,避免把
- * 容量、坐标、自由文本位置或展示文本误当作基础档案事实。
- *
- * <p>**片区(district)一律传 null**:水厂/泵站/管网/排水设施都没有
- * {@code equipment_base.district} 那种权威片区列,也不关联设备,故如实归入「未分区」,
- * 不新增数据库列、不用行业或管理单位冒充(决策票 06 / 执行票 12)。
+ * 三个驾驶舱设施统计端点的权威设施目录(执行票 31)。
+ * 包含管网、水厂、供排水泵站、排水工程设施、水源地与窨井的合法记录并集。
+ * 设施无权威片区来源,一律保持 null;未知类型或缺编码行以哨兵保留到 excluded。
  */
 @Component
 @Primary
 public class BusinessCockpitInfrastructureBaseArchivesAdapter
-        implements CockpitInfrastructureBaseArchivesAdapter {
-
+        implements CockpitInfrastructureBaseArchivesAdapter, CockpitFacilityCatalog {
     private static final ZoneId BUSINESS_ZONE = ZoneId.of("Asia/Shanghai");
-    private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
+    private static final String UNCLASSIFIED = "FACILITY_UNCLASSIFIED";
     private static final List<CockpitInfrastructureBaseArchivesSnapshot.SourceType> TYPES = Arrays.asList(
             type("WATER_PLANT", "水厂", "WATER_SUPPLY"),
             type("WATER_PUMP_STATION", "供水泵站", "WATER_SUPPLY"),
-            type(CockpitPipeNetworkType.WATER_PIPE,
-                    CockpitPipeNetworkType.nameOf(CockpitPipeNetworkType.WATER_PIPE),
-                    CockpitPipeNetworkType.industryOfTypeCode(CockpitPipeNetworkType.WATER_PIPE)),
-            type(CockpitPipeNetworkType.GAS_PIPE,
-                    CockpitPipeNetworkType.nameOf(CockpitPipeNetworkType.GAS_PIPE),
-                    CockpitPipeNetworkType.industryOfTypeCode(CockpitPipeNetworkType.GAS_PIPE)),
-            type(CockpitPipeNetworkType.DRAINAGE_PIPE,
-                    CockpitPipeNetworkType.nameOf(CockpitPipeNetworkType.DRAINAGE_PIPE),
-                    CockpitPipeNetworkType.industryOfTypeCode(CockpitPipeNetworkType.DRAINAGE_PIPE)),
+            type(CockpitPipeNetworkType.WATER_PIPE, CockpitPipeNetworkType.nameOf(CockpitPipeNetworkType.WATER_PIPE), "WATER_SUPPLY"),
+            type(CockpitPipeNetworkType.GAS_PIPE, CockpitPipeNetworkType.nameOf(CockpitPipeNetworkType.GAS_PIPE), "GAS"),
+            type(CockpitPipeNetworkType.DRAINAGE_PIPE, CockpitPipeNetworkType.nameOf(CockpitPipeNetworkType.DRAINAGE_PIPE), "DRAINAGE"),
             type("DRAINAGE_GATE", "排水闸门", "DRAINAGE"),
             type("DRAINAGE_STORAGE", "排水调蓄设施", "DRAINAGE"),
-            type("DRAINAGE_PUMP_STATION", "排水泵站", "DRAINAGE"));
+            type("DRAINAGE_PUMP_STATION", "排水泵站", "DRAINAGE"),
+            type("WATER_SOURCE", "水源地", "WATER_SUPPLY"),
+            type("MANHOLE", "窨井", "MANHOLE"));
 
-    private final IWaterPlantInfoService plantService;
-    private final IWaterPumpStationService pumpStationService;
-    private final PipeNetworkBaseService pipeNetworkBaseService;
-    private final EngineeringFacilityService engineeringFacilityService;
-    private final Clock clock;
+    private final IWaterPlantInfoService plants;
+    private final IWaterPumpStationService stations;
+    private final PipeNetworkBaseService pipes;
+    private final EngineeringFacilityService engineering;
+    private final IWaterSourceInfoService sources;
+    private final ManholeBaseService manholes;
 
     public BusinessCockpitInfrastructureBaseArchivesAdapter(
-            IWaterPlantInfoService plantService,
-            IWaterPumpStationService pumpStationService,
-            PipeNetworkBaseService pipeNetworkBaseService,
-            EngineeringFacilityService engineeringFacilityService,
-            Clock clock) {
-        this.plantService = plantService;
-        this.pumpStationService = pumpStationService;
-        this.pipeNetworkBaseService = pipeNetworkBaseService;
-        this.engineeringFacilityService = engineeringFacilityService;
-        this.clock = clock;
+            IWaterPlantInfoService plants, IWaterPumpStationService stations,
+            PipeNetworkBaseService pipes, EngineeringFacilityService engineering,
+            IWaterSourceInfoService sources, ManholeBaseService manholes) {
+        this.plants = plants;
+        this.stations = stations;
+        this.pipes = pipes;
+        this.engineering = engineering;
+        this.sources = sources;
+        this.manholes = manholes;
     }
 
     @Override
     public CockpitInfrastructureBaseArchivesSnapshot load() {
         List<CockpitInfrastructureBaseArchivesSnapshot.SourceFacility> facilities = new ArrayList<>();
-        addPlants(facilities);
-        addPumpStations(facilities);
-        addPipes(facilities);
-        addDrainageFacilities(facilities);
-        return new CockpitInfrastructureBaseArchivesSnapshot(
-                TIME_FORMATTER.format(clock.instant().atZone(BUSINESS_ZONE)), TYPES, facilities);
-    }
-
-    private void addPlants(List<CockpitInfrastructureBaseArchivesSnapshot.SourceFacility> facilities) {
-        for (WaterPlantInfo plant : safeList(plantService.list())) {
-            String code = trimToNull(plant.getPlantCode());
-            if (code == null) {
-                continue;
-            }
-            facilities.add(facility(code, "WATER_PLANT", null, null,
-                    plant.getCommissionDate(), null));
+        LocalDateTime updatedAt = null;
+        for (WaterPlantInfo plant : safeList(plants.list())) {
+            updatedAt = latest(updatedAt, plant.getUpdateTime());
+            updatedAt = latest(updatedAt, plant.getCreateTime());
+            add(facilities, key("water_plant_info", plant.getId(), plant.getPlantCode()), plant.getPlantCode(), "WATER_PLANT", null, plant.getCommissionDate(), null);
         }
-    }
-
-    private void addPumpStations(List<CockpitInfrastructureBaseArchivesSnapshot.SourceFacility> facilities) {
-        for (WaterPumpStation station : safeList(pumpStationService.list(
-                new LambdaQueryWrapper<WaterPumpStation>()
-                        .in(WaterPumpStation::getStationType,
-                                PumpStationType.WATER_SUPPLY, PumpStationType.DRAINAGE)))) {
-            String code = trimToNull(station.getStationCode());
-            if (code == null) {
-                continue;
-            }
+        // 驾驶舱跨行业统计读取共用表全集,按 station_type 分类;未知归属显式 excluded。
+        // 不用于供水/排水管理入口(其强制归属过滤规则仍保持 ADR-0007)。
+        for (WaterPumpStation station : safeList(stations.list())) {
+            updatedAt = latest(updatedAt, station.getUpdateTime());
+            updatedAt = latest(updatedAt, station.getCreateTime());
             String stationType = trimToNull(station.getStationType());
-            String typeCode = PumpStationType.DRAINAGE.equalsIgnoreCase(stationType)
-                    ? "DRAINAGE_PUMP_STATION"
-                    : PumpStationType.WATER_SUPPLY.equalsIgnoreCase(stationType)
-                            ? "WATER_PUMP_STATION" : null;
-            if (typeCode == null) {
-                continue;
-            }
-            facilities.add(facility(code, typeCode, null, null,
-                    station.getCommissionDate(), null));
+            String type = PumpStationType.DRAINAGE.equalsIgnoreCase(stationType) ? "DRAINAGE_PUMP_STATION"
+                    : PumpStationType.WATER_SUPPLY.equalsIgnoreCase(stationType) ? "WATER_PUMP_STATION" : UNCLASSIFIED;
+            add(facilities, key("pump_station", station.getId(), station.getStationCode()), station.getStationCode(), type, null, station.getCommissionDate(), null);
         }
-    }
-
-    private void addPipes(List<CockpitInfrastructureBaseArchivesSnapshot.SourceFacility> facilities) {
-        for (PipeNetworkBase pipe : safeList(pipeNetworkBaseService.list())) {
-            String code = trimToNull(pipe.getNetworkCode());
-            if (code == null) {
-                continue;
-            }
-            // 权威映射在 CockpitPipeNetworkType(0-燃气/1-排水/2-供水)。无法映射的取值
-            // 归入未分类哨兵码:设施仍留在响应里,并由服务层计入 excluded.unknownTypeCount,
-            // 不再被静默 continue 掉(执行票 20:4272 条管网曾因漏认 0/1 而整体丢失)。
-            String typeCode = CockpitPipeNetworkType.typeCodeOf(pipe.getNetworkType());
-            BigDecimal lengthKm = pipe.getLength() == null
-                    ? null : pipe.getLength().divide(BigDecimal.valueOf(1000));
-            LocalDate builtAt = pipe.getBuildDate() == null
-                    ? null : new java.sql.Date(pipe.getBuildDate().getTime()).toLocalDate();
-            facilities.add(facility(code, typeCode, null, lengthKm, null, builtAt));
+        for (PipeNetworkBase pipe : safeList(pipes.list())) {
+            updatedAt = latest(updatedAt, pipe.getUpdateTime());
+            updatedAt = latest(updatedAt, pipe.getCreateTime());
+            BigDecimal lengthKm = pipe.getLength() == null ? null : pipe.getLength().divide(BigDecimal.valueOf(1000));
+            LocalDate builtAt = pipe.getBuildDate() == null ? null
+                    : new java.sql.Date(pipe.getBuildDate().getTime()).toLocalDate();
+            add(facilities, key("pipe_network_base", pipe.getNetworkId(), pipe.getNetworkCode()), pipe.getNetworkCode(), CockpitPipeNetworkType.typeCodeOf(pipe.getNetworkType()),
+                    lengthKm, null, builtAt);
+        }
+        for (EngineeringFacility facility : safeList(engineering.list())) {
+            updatedAt = latest(updatedAt, facility.getUpdateTime());
+            updatedAt = latest(updatedAt, facility.getCreateTime());
+            // 保留现有工程设施年限映射;本票只统一设施范围,不裁定建设日期来源。
+            add(facilities, key("psgw_engineering_facility", facility.getId(), facility.getId()),
+                    facility.getId(), drainageType(facility.getFacilityType()), null, null,
+                    facility.getCreateTime() == null ? null : facility.getCreateTime().toLocalDate());
         }
+        for (WaterSourceInfo source : safeList(sources.list())) {
+            updatedAt = latest(updatedAt, source.getUpdateTime());
+            updatedAt = latest(updatedAt, source.getCreateTime());
+            add(facilities, key("water_source_info", source.getId(), source.getSourceId()), source.getSourceId(), "WATER_SOURCE", null, null, null);
+        }
+        for (ManholeBase manhole : safeList(manholes.list())) {
+            updatedAt = latest(updatedAt, manhole.getUpdateTime());
+            updatedAt = latest(updatedAt, manhole.getCreateTime());
+            add(facilities, key("manhole_base", manhole.getManholeId(), manhole.getManholeCode()), manhole.getManholeCode(), "MANHOLE", null, null, null);
+        }
+        return new CockpitInfrastructureBaseArchivesSnapshot(
+                updatedAt == null ? null : DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(updatedAt.atZone(BUSINESS_ZONE)),
+                TYPES, facilities);
     }
 
-    private void addDrainageFacilities(
-            List<CockpitInfrastructureBaseArchivesSnapshot.SourceFacility> facilities) {
-        for (EngineeringFacility source : safeList(engineeringFacilityService.list())) {
-            String code = trimToNull(source.getId());
-            String typeCode = drainageFacilityType(source.getFacilityType());
-            if (code == null || typeCode == null) {
-                continue;
-            }
-            // 片区一律 null:排水工程设施只有自由文本 area,不是 equipment_base.district,
-            // 用它冒充片区会凭空造出片区归属(决策票 06),故如实交未分区。
-            facilities.add(facility(code, typeCode, null, null, null,
-                    source.getCreateTime() == null ? null : source.getCreateTime().toLocalDate()));
+    private static void add(List<CockpitInfrastructureBaseArchivesSnapshot.SourceFacility> facilities,
+            String sourceKey, String code, String type, BigDecimal lengthKm, LocalDate commissionedAt, LocalDate builtAt) {
+        String resolvedCode = trimToNull(code);
+        if (resolvedCode == null) {
+            // 缺少标识不能进入正式设施集合,但必须让三个服务都能复算排除数。
+            resolvedCode = "MISSING_CODE:" + facilities.size();
+            type = UNCLASSIFIED;
         }
+        facilities.add(new CockpitInfrastructureBaseArchivesSnapshot.SourceFacility(
+                sourceKey == null ? resolvedCode : sourceKey, resolvedCode, type, null, lengthKm, null, commissionedAt, builtAt));
     }
 
-    private static String drainageFacilityType(String facilityType) {
-        String normalized = trimToNull(facilityType);
-        if (normalized == null) {
-            return null;
-        }
-        if (normalized.contains("闸")) {
-            return "DRAINAGE_GATE";
-        }
-        if (normalized.contains("调蓄") || normalized.contains("蓄水")) {
-            return "DRAINAGE_STORAGE";
-        }
-        if (normalized.contains("泵站")) {
-            return "DRAINAGE_PUMP_STATION";
-        }
-        return null;
+    private static String key(String table, Object id, String code) {
+        String identity = id == null ? trimToNull(code) : id.toString();
+        return identity == null ? null : table + "|" + identity;
     }
 
-    private static CockpitInfrastructureBaseArchivesSnapshot.SourceFacility facility(
-            String code,
-            String typeCode,
-            Integer district,
-            BigDecimal lengthKm,
-            LocalDate commissionedAt,
-            LocalDate builtAt) {
-        return new CockpitInfrastructureBaseArchivesSnapshot.SourceFacility(
-                code, typeCode, district, lengthKm, null, commissionedAt, builtAt);
+    private static String drainageType(String value) {
+        String type = trimToNull(value);
+        if (type == null) return UNCLASSIFIED;
+        if (type.contains("闸")) return "DRAINAGE_GATE";
+        if (type.contains("调蓄") || type.contains("蓄水")) return "DRAINAGE_STORAGE";
+        if (type.contains("泵站")) return "DRAINAGE_PUMP_STATION";
+        return UNCLASSIFIED;
     }
 
-    private static CockpitInfrastructureBaseArchivesSnapshot.SourceType type(
-            String code, String name, String industry) {
-        return new CockpitInfrastructureBaseArchivesSnapshot.SourceType(
-                code, name, industry, "FACILITY");
+    private static CockpitInfrastructureBaseArchivesSnapshot.SourceType type(String code, String name, String industry) {
+        return new CockpitInfrastructureBaseArchivesSnapshot.SourceType(code, name, industry, "FACILITY");
     }
 
     private static <T> List<T> safeList(List<T> value) {
-        return value == null
-                ? Collections.emptyList()
+        return value == null ? Collections.emptyList()
                 : value.stream().filter(Objects::nonNull).collect(Collectors.toList());
     }
 
-    private static String trimToNull(String value) {
-        return isBlank(value) ? null : value.trim();
+    private static LocalDateTime latest(LocalDateTime current, LocalDateTime candidate) {
+        if (candidate == null) return current;
+        return current == null || candidate.isAfter(current) ? candidate : current;
     }
 
-    private static boolean isBlank(String value) {
-        return value == null || value.trim().isEmpty();
+    private static String trimToNull(String value) {
+        return value == null || value.trim().isEmpty() ? null : value.trim();
     }
 }

+ 45 - 136
pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/infrastructure/BusinessCockpitInfrastructureManagementInformationAdapter.java

@@ -1,16 +1,9 @@
 package com.zksy.cockpit.infrastructure;
 
-import com.zksy.WaterSupply.WaterSourceInfo.domain.WaterSourceInfo;
-import com.zksy.WaterSupply.WaterSourceInfo.service.IWaterSourceInfoService;
 import com.zksy.base.domain.EquipmentBase;
 import com.zksy.base.domain.EquipmentType;
-import com.zksy.base.domain.ManholeBase;
-import com.zksy.base.domain.PipeNetworkBase;
 import com.zksy.base.service.EquipmentBaseService;
 import com.zksy.base.service.EquipmentTypeService;
-import com.zksy.base.service.ManholeBaseService;
-import com.zksy.base.service.PipeNetworkBaseService;
-import com.zksy.cockpit.foundation.CockpitPipeNetworkType;
 import com.zksy.cockpit.foundation.CockpitDistrict;
 import com.zksy.cockpit.foundation.CockpitTypeIndustry;
 import org.springframework.context.annotation.Primary;
@@ -27,156 +20,72 @@ import java.util.Map;
 import java.util.Objects;
 import java.util.stream.Collectors;
 
-/**
- * 管理信息真实数据适配器(决策票 06 / 执行票 12)。
- *
- * <p>按「对象 → 行业 + 片区」上报关系,供服务层做片区×行业计数。片区**只**取
- * {@code equipment_base.district}(设备的权威片区来源):
- * <ul>
- *   <li>设备({@link EquipmentBase}):用自身 {@code district},归一交给服务层。</li>
- *   <li>窨井 / 管网设施 / 水源地:这些表没有片区列、也不关联设备,故如实传
- *       {@code null}=未分区,**不用管养单位或权属单位冒充片区**。</li>
- * </ul>
- * 组织与权属字段已随决策票 06 删除,不再上报。
- */
+/** 设备保留权威片区;设施关系只投影统一目录,不再独立选择设施表(执行票 31)。 */
 @Component
 @Primary
 public class BusinessCockpitInfrastructureManagementInformationAdapter
         implements CockpitInfrastructureManagementInformationAdapter {
-
-    private static final ZoneId BUSINESS_ZONE = ZoneId.of("Asia/Shanghai");
-    private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
-
-    private final EquipmentBaseService equipmentBaseService;
-    private final EquipmentTypeService equipmentTypeService;
-    private final ManholeBaseService manholeBaseService;
-    private final PipeNetworkBaseService pipeNetworkBaseService;
-    private final IWaterSourceInfoService waterSourceInfoService;
-
-    public BusinessCockpitInfrastructureManagementInformationAdapter(
-            EquipmentBaseService equipmentBaseService,
-            EquipmentTypeService equipmentTypeService,
-            ManholeBaseService manholeBaseService,
-            PipeNetworkBaseService pipeNetworkBaseService,
-            IWaterSourceInfoService waterSourceInfoService) {
-        this.equipmentBaseService = equipmentBaseService;
-        this.equipmentTypeService = equipmentTypeService;
-        this.manholeBaseService = manholeBaseService;
-        this.pipeNetworkBaseService = pipeNetworkBaseService;
-        this.waterSourceInfoService = waterSourceInfoService;
+    private final EquipmentBaseService equipment;
+    private final EquipmentTypeService equipmentTypes;
+    private final CockpitFacilityCatalog facilities;
+
+    public BusinessCockpitInfrastructureManagementInformationAdapter(EquipmentBaseService equipment,
+            EquipmentTypeService equipmentTypes, CockpitFacilityCatalog facilities) {
+        this.equipment = equipment;
+        this.equipmentTypes = equipmentTypes;
+        this.facilities = facilities;
     }
 
     @Override
     public CockpitInfrastructureManagementInformationSnapshot load() {
-        List<CockpitInfrastructureManagementInformationSnapshot.SourceRelation> relations =
-                new ArrayList<>();
-        String[] latestUpdatedAt = new String[1];
-        Map<Integer, EquipmentType> types = typeIndex(equipmentTypeService.list());
-
-        for (EquipmentBase device : safeList(equipmentBaseService.list())) {
-            if (device == null) {
-                continue;
-            }
-            String objectCode = trimToNull(device.getEquipmentCode());
-            String industry = device.getEquipmentTypeId() == null
-                    ? null
+        List<CockpitInfrastructureManagementInformationSnapshot.SourceRelation> relations = new ArrayList<>();
+        Map<Integer, EquipmentType> types = new HashMap<>();
+        for (EquipmentType type : safeList(equipmentTypes.list())) if (type.getId() != null) types.put(type.getId(), type);
+        String updatedAt = null;
+        for (EquipmentBase device : safeList(equipment.list())) {
+            String code = trimToNull(device.getEquipmentCode());
+            String industry = device.getEquipmentTypeId() == null ? null
                     : CockpitTypeIndustry.industryOf(types.get(device.getEquipmentTypeId()), types);
-            if (objectCode == null || industry == null) {
-                continue;
-            }
-            // 设备自身片区即权威来源(equipment_base.district)。
-            relations.add(relation(objectCode, "SMART_DEVICE", industry,
-                    CockpitDistrict.codeOf(device.getDistrict())));
-            latestUpdatedAt[0] = latest(latestUpdatedAt[0], format(device.getUpdateTime()));
-        }
-
-        for (ManholeBase manhole : safeList(manholeBaseService.list())) {
-            if (manhole == null) {
-                continue;
-            }
-            String objectCode = trimToNull(manhole.getManholeCode());
-            if (objectCode == null) {
-                continue;
-            }
-            // 窨井无片区来源 → 未分区。
-            relations.add(relation(objectCode, "FACILITY", "MANHOLE", null));
-            latestUpdatedAt[0] = latest(latestUpdatedAt[0], format(manhole.getUpdateTime()));
-        }
-
-        for (PipeNetworkBase pipe : safeList(pipeNetworkBaseService.list())) {
-            if (pipe == null) {
-                continue;
-            }
-            String objectCode = trimToNull(pipe.getNetworkCode());
-            if (objectCode == null) {
-                continue;
-            }
-            // 权威映射在 CockpitPipeNetworkType。无法映射的 network_type 交由 null 行业上行,
-            // 由服务层计入 excluded.unknownIndustryCount —— 不在适配器里静默丢弃(执行票 20)。
-            String industry = CockpitPipeNetworkType.industryOf(pipe.getNetworkType());
-            // 管网设施无片区来源 → 未分区,不用 managementUnit 冒充。
-            relations.add(relation(objectCode, "FACILITY", industry, null));
-            latestUpdatedAt[0] = latest(latestUpdatedAt[0], format(pipe.getUpdateTime()));
+            if (code == null || industry == null) continue;
+            relations.add(new CockpitInfrastructureManagementInformationSnapshot.SourceRelation(
+                    code, "SMART_DEVICE", industry, CockpitDistrict.codeOf(device.getDistrict())));
+            updatedAt = latest(updatedAt, format(device.getUpdateTime()));
         }
-
-        for (WaterSourceInfo source : safeList(waterSourceInfoService.list())) {
-            if (source == null) {
-                continue;
+        CockpitInfrastructureBaseArchivesSnapshot catalog = facilities.load();
+        if (catalog != null) {
+            updatedAt = latest(updatedAt, catalog.getUpdatedAt());
+            Map<String, CockpitInfrastructureBaseArchivesSnapshot.SourceType> facilityTypes = new HashMap<>();
+            for (CockpitInfrastructureBaseArchivesSnapshot.SourceType type : safeList(catalog.getTypes())) {
+                facilityTypes.put(type.getCode(), type);
             }
-            String objectCode = trimToNull(source.getSourceId());
-            if (objectCode == null) {
-                continue;
+            for (CockpitInfrastructureBaseArchivesSnapshot.SourceFacility facility : safeList(catalog.getFacilities())) {
+                CockpitInfrastructureBaseArchivesSnapshot.SourceType type = facilityTypes.get(facility.getTypeCode());
+                // 来源表+主键的稳定身份:不同表同码不合并,同源重复关系仍可识别。
+                String rowKey = facility.getSourceKey();
+                relations.add(new CockpitInfrastructureManagementInformationSnapshot.SourceRelation(
+                        rowKey, type == null ? "FACILITY" : type.getObjectKind(),
+                        type == null ? null : type.getIndustry(), facility.getDistrict()));
             }
-            // 水源地无片区来源 → 未分区。
-            relations.add(relation(objectCode, "FACILITY", "WATER_SUPPLY", null));
-            latestUpdatedAt[0] = latest(latestUpdatedAt[0], format(source.getUpdateTime()));
         }
-
-        return new CockpitInfrastructureManagementInformationSnapshot(latestUpdatedAt[0], relations);
+        return new CockpitInfrastructureManagementInformationSnapshot(updatedAt, relations);
     }
 
-    private static CockpitInfrastructureManagementInformationSnapshot.SourceRelation relation(
-            String objectCode,
-            String objectKind,
-            String industry,
-            Integer district) {
-        return new CockpitInfrastructureManagementInformationSnapshot.SourceRelation(
-                objectCode, objectKind, industry, district);
-    }
-
-    private static Map<Integer, EquipmentType> typeIndex(List<EquipmentType> values) {
-        Map<Integer, EquipmentType> result = new HashMap<>();
-        for (EquipmentType value : safeList(values)) {
-            if (value != null && value.getId() != null) {
-                result.put(value.getId(), value);
-            }
-        }
-        return result;
+    private static String latest(String current, String candidate) {
+        if (candidate == null) return current;
+        return current == null || candidate.compareTo(current) > 0 ? candidate : current;
     }
 
     private static String format(LocalDateTime value) {
-        return value == null
-                ? null
-                : TIME_FORMATTER.format(value.atZone(BUSINESS_ZONE).toOffsetDateTime());
-    }
-
-    private static String latest(String left, String right) {
-        if (left == null) {
-            return right;
-        }
-        if (right == null) {
-            return left;
-        }
-        return left.compareTo(right) >= 0 ? left : right;
-    }
-
-    private static <T> List<T> safeList(List<T> value) {
-        return value == null
-                ? Collections.emptyList()
-                : value.stream().filter(Objects::nonNull).collect(Collectors.toList());
+        return value == null ? null : DateTimeFormatter.ISO_OFFSET_DATE_TIME
+                .format(value.atZone(ZoneId.of("Asia/Shanghai")));
     }
 
     private static String trimToNull(String value) {
         return value == null || value.trim().isEmpty() ? null : value.trim();
     }
+
+    private static <T> List<T> safeList(List<T> values) {
+        return values == null ? Collections.emptyList()
+                : values.stream().filter(Objects::nonNull).collect(Collectors.toList());
+    }
 }

+ 8 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/infrastructure/CockpitFacilityCatalog.java

@@ -0,0 +1,8 @@
+package com.zksy.cockpit.infrastructure;
+
+/**
+ * 设施统计的唯一来源。各驾驶舱端点只能从同一份设施快照投影,避免各自选择表集合。
+ */
+public interface CockpitFacilityCatalog {
+    CockpitInfrastructureBaseArchivesSnapshot load();
+}

+ 1 - 1
pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/infrastructure/CockpitInfrastructureBaseArchivesService.java

@@ -163,6 +163,7 @@ public class CockpitInfrastructureBaseArchivesService {
             typeSummaries.compute(type.getCode(), (key, existing) -> existing == null
                     ? new TypeSummary(type.getCode(), type.getName(), 1, facility.getLengthKm(), facility.getScale(), null)
                     : mergeSummary(existing, facility));
+            districtCounts.merge(normalizedDistrict, 1, Integer::sum);
             if ("FUTURE".equals(bucket)) {
                 continue;
             }
@@ -174,7 +175,6 @@ public class CockpitInfrastructureBaseArchivesService {
             } else {
                 unknownDateCount++;
             }
-            districtCounts.merge(normalizedDistrict, 1, Integer::sum);
         }
 
         List<AgeDistribution> ageDistribution = new ArrayList<>();

+ 8 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/infrastructure/CockpitInfrastructureBaseArchivesSnapshot.java

@@ -49,6 +49,7 @@ public class CockpitInfrastructureBaseArchivesSnapshot {
      * {@code null},由服务层如实归一为「未分区」,不用行业或管理单位冒充。
      */
     public static class SourceFacility {
+        private final String sourceKey;
         private final String code;
         private final String typeCode;
         private final Integer district;
@@ -65,6 +66,12 @@ public class CockpitInfrastructureBaseArchivesSnapshot {
                 String scale,
                 LocalDate commissionedAt,
                 LocalDate builtAt) {
+            this(typeCode + "|" + code, code, typeCode, district, lengthKm, scale, commissionedAt, builtAt);
+        }
+
+        public SourceFacility(String sourceKey, String code, String typeCode, Integer district,
+                BigDecimal lengthKm, String scale, LocalDate commissionedAt, LocalDate builtAt) {
+            this.sourceKey = sourceKey;
             this.code = code;
             this.typeCode = typeCode;
             this.district = district;
@@ -74,6 +81,7 @@ public class CockpitInfrastructureBaseArchivesSnapshot {
             this.builtAt = builtAt;
         }
 
+        public String getSourceKey() { return sourceKey; }
         public String getCode() { return code; }
         public String getTypeCode() { return typeCode; }
         public Integer getDistrict() { return district; }

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

@@ -1,19 +1,12 @@
 package com.zksy.cockpit.overview;
 
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.zksy.WaterSupply.WaterPlantInfo.domain.WaterPlantInfo;
-import com.zksy.WaterSupply.WaterPlantInfo.service.IWaterPlantInfoService;
-import com.zksy.WaterSupply.WaterPumpStation.domain.PumpStationType;
-import com.zksy.WaterSupply.WaterPumpStation.domain.WaterPumpStation;
-import com.zksy.WaterSupply.WaterPumpStation.service.IWaterPumpStationService;
 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.CockpitPipeNetworkType;
 import com.zksy.cockpit.foundation.CockpitTypeIndustry;
+import com.zksy.cockpit.infrastructure.CockpitFacilityCatalog;
+import com.zksy.cockpit.infrastructure.CockpitInfrastructureBaseArchivesSnapshot;
 import org.springframework.context.annotation.Primary;
 import org.springframework.stereotype.Component;
 
@@ -31,217 +24,85 @@ import java.util.Map;
 import java.util.Objects;
 import java.util.stream.Collectors;
 
-/**
- * 总体概览-基础设施 真实数据适配器(BT-5)。
- *
- * <p>智能设备按 {@code equipment_type} 分组计数({@code objectKind=SMART_DEVICE});
- * 水厂和泵站分别取自其设施台账,共用泵站按 {@code station_type} 区分供水/排水。
- * 管网设施按 {@code pipe_network_base.network_type} 分组计数({@code objectKind=FACILITY})。
- * 管网类型无可信行业字典,{@code industry} 如实为 {@code null};设施长度由 {@code length}(米)换算公里。
- * {@code updatedAt} 取各台账最新更新时间。
- */
+/** 设备按类型计数;设施只投影权威目录,禁止另选设施表集合(执行票 31)。 */
 @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;
-    private final IWaterPlantInfoService plantService;
-    private final IWaterPumpStationService pumpStationService;
-
-    public BusinessCockpitOverviewInfrastructureAdapter(
-            EquipmentTypeService equipmentTypeService,
-            EquipmentBaseService equipmentBaseService,
-            PipeNetworkBaseService pipeNetworkBaseService,
-            IWaterPlantInfoService plantService,
-            IWaterPumpStationService pumpStationService) {
-        this.equipmentTypeService = equipmentTypeService;
-        this.equipmentBaseService = equipmentBaseService;
-        this.pipeNetworkBaseService = pipeNetworkBaseService;
-        this.plantService = plantService;
-        this.pumpStationService = pumpStationService;
+public class BusinessCockpitOverviewInfrastructureAdapter implements CockpitOverviewInfrastructureAdapter {
+    private final EquipmentTypeService equipmentTypes;
+    private final EquipmentBaseService equipment;
+    private final CockpitFacilityCatalog facilities;
+
+    public BusinessCockpitOverviewInfrastructureAdapter(EquipmentTypeService equipmentTypes,
+            EquipmentBaseService equipment, CockpitFacilityCatalog facilities) {
+        this.equipmentTypes = equipmentTypes;
+        this.equipment = equipment;
+        this.facilities = facilities;
     }
 
     @Override
     public CockpitOverviewInfrastructureSnapshot load() {
-        List<EquipmentType> equipmentTypes = safeList(equipmentTypeService.list());
-        Map<Integer, EquipmentType> types = equipmentTypes(equipmentTypes);
-
-        List<CockpitOverviewInfrastructureSnapshot.SourceType> sourceTypes = new ArrayList<>();
-        for (EquipmentType type : equipmentTypes) {
-            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<EquipmentType> deviceTypes = safeList(equipmentTypes.list());
+        Map<Integer, EquipmentType> index = new HashMap<>();
+        for (EquipmentType type : deviceTypes) if (type.getId() != null) index.put(type.getId(), type);
+        List<CockpitOverviewInfrastructureSnapshot.SourceType> types = new ArrayList<>();
+        for (EquipmentType type : deviceTypes) {
+            if (type.getId() != null) types.add(new CockpitOverviewInfrastructureSnapshot.SourceType(
+                    String.valueOf(type.getId()), type.getTypeName(), CockpitTypeIndustry.industryOf(type, index), null, "SMART_DEVICE"));
         }
-
-        List<EquipmentBase> devices = safeList(equipmentBaseService.list());
         List<CockpitOverviewInfrastructureSnapshot.SourceCount> counts = new ArrayList<>();
-        Map<Integer, Integer> deviceCountByType = new LinkedHashMap<>();
-        for (EquipmentBase device : devices) {
-            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<>();
-        List<PipeNetworkBase> facilities = safeList(pipeNetworkBaseService.list());
-        for (PipeNetworkBase facility : facilities) {
-            if (facility == null) {
-                continue;
-            }
-            // 权威映射在 CockpitPipeNetworkType(0-燃气/1-排水/2-供水)。此前这里直接把
-            // 原始 network_type 当类型码、且 industry 传 null,导致 5268 条管网整体被
-            // 服务层计入 excluded.unknownTypeCount(执行票 20)。
-            String typeCode = CockpitPipeNetworkType.typeCodeOf(facility.getNetworkType());
-            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());
+        Map<Integer, Integer> deviceCounts = new LinkedHashMap<>();
+        String updatedAt = null;
+        for (EquipmentBase device : safeList(equipment.list())) {
+            if (device.getEquipmentTypeId() != null) deviceCounts.merge(device.getEquipmentTypeId(), 1, Integer::sum);
+            updatedAt = latest(updatedAt, format(device.getUpdateTime()));
+            updatedAt = latest(updatedAt, format(device.getCreateTime()));
+        }
+        for (Map.Entry<Integer, Integer> entry : deviceCounts.entrySet()) counts.add(
+                new CockpitOverviewInfrastructureSnapshot.SourceCount("SMART_DEVICE", String.valueOf(entry.getKey()), entry.getValue()));
+
+        CockpitInfrastructureBaseArchivesSnapshot catalog = facilities.load();
+        if (catalog != null) {
+            updatedAt = latest(updatedAt, catalog.getUpdatedAt());
+            for (CockpitInfrastructureBaseArchivesSnapshot.SourceType type : safeList(catalog.getTypes())) {
+                types.add(new CockpitOverviewInfrastructureSnapshot.SourceType(
+                        type.getCode(), type.getName(), type.getIndustry(), null, type.getObjectKind()));
             }
-        }
-        for (Map.Entry<String, FacilityAggregate> entry : facilityByType.entrySet()) {
-            String typeCode = entry.getKey();
-            FacilityAggregate aggregate = entry.getValue();
-            // 行业由权威映射给出;未分类哨兵无行业,服务层据实计入 unknownTypeCount。
-            sourceTypes.add(new CockpitOverviewInfrastructureSnapshot.SourceType(
-                    typeCode,
-                    CockpitPipeNetworkType.nameOf(typeCode),
-                    CockpitPipeNetworkType.industryOfTypeCode(typeCode),
-                    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)));
-        }
-
-        List<WaterPlantInfo> plants = safeList(plantService.list());
-        int plantCount = 0;
-        for (WaterPlantInfo plant : plants) {
-            if (!isBlank(plant.getPlantCode())) {
-                plantCount++;
-            }
-        }
-        addFacilityCount(sourceTypes, counts, "WATER_PLANT", "水厂", "WATER_SUPPLY", plantCount);
-
-        List<WaterPumpStation> stations = safeList(pumpStationService.list(
-                new LambdaQueryWrapper<WaterPumpStation>()
-                        .in(WaterPumpStation::getStationType,
-                                PumpStationType.WATER_SUPPLY, PumpStationType.DRAINAGE)));
-        int waterStationCount = 0;
-        int drainageStationCount = 0;
-        for (WaterPumpStation station : stations) {
-            if (isBlank(station.getStationCode())) {
-                continue;
+            // 未分类哨兵不在正式字典中,由概览服务累计 excluded.unknownTypeCount。
+            Map<String, FacilityAggregate> aggregates = new LinkedHashMap<>();
+            for (CockpitInfrastructureBaseArchivesSnapshot.SourceFacility facility : safeList(catalog.getFacilities())) {
+                FacilityAggregate aggregate = aggregates.computeIfAbsent(facility.getTypeCode(), key -> new FacilityAggregate());
+                aggregate.count++;
+                if (facility.getLengthKm() != null) aggregate.lengthKm =
+                        (aggregate.lengthKm == null ? BigDecimal.ZERO : aggregate.lengthKm).add(facility.getLengthKm());
             }
-            String stationType = trimToNull(station.getStationType());
-            if (PumpStationType.WATER_SUPPLY.equalsIgnoreCase(stationType)) {
-                waterStationCount++;
-            } else if (PumpStationType.DRAINAGE.equalsIgnoreCase(stationType)) {
-                drainageStationCount++;
-            }
-        }
-        addFacilityCount(sourceTypes, counts, "WATER_PUMP_STATION", "供水泵站", "WATER_SUPPLY", waterStationCount);
-        addFacilityCount(sourceTypes, counts, "DRAINAGE_PUMP_STATION", "排水泵站", "DRAINAGE", drainageStationCount);
-
-        LocalDateTime updatedAt = null;
-        for (EquipmentBase device : devices) {
-            if (device == null) {
-                continue;
-            }
-            updatedAt = latest(updatedAt, device.getUpdateTime());
-            updatedAt = latest(updatedAt, device.getCreateTime());
-        }
-        for (PipeNetworkBase facility : facilities) {
-            if (facility == null) {
-                continue;
+            for (Map.Entry<String, FacilityAggregate> entry : aggregates.entrySet()) {
+                FacilityAggregate aggregate = entry.getValue();
+                counts.add(new CockpitOverviewInfrastructureSnapshot.SourceCount("FACILITY", entry.getKey(), aggregate.count,
+                        new CockpitOverviewInfrastructureSnapshot.SourceValue(
+                                aggregate.lengthKm == null ? null : aggregate.lengthKm.setScale(3, RoundingMode.HALF_UP),
+                                aggregate.lengthKm == null ? null : "km", null)));
             }
-            updatedAt = latest(updatedAt, facility.getUpdateTime());
-            updatedAt = latest(updatedAt, facility.getCreateTime());
         }
-        for (WaterPlantInfo plant : plants) {
-            updatedAt = latest(updatedAt, plant.getUpdateTime());
-            updatedAt = latest(updatedAt, plant.getCreateTime());
-        }
-        for (WaterPumpStation station : stations) {
-            updatedAt = latest(updatedAt, station.getUpdateTime());
-            updatedAt = latest(updatedAt, station.getCreateTime());
-        }
-
-        return new CockpitOverviewInfrastructureSnapshot(
-                offsetTime(updatedAt), sourceTypes, counts);
-    }
-
-    private static void addFacilityCount(
-            List<CockpitOverviewInfrastructureSnapshot.SourceType> types,
-            List<CockpitOverviewInfrastructureSnapshot.SourceCount> counts,
-            String code, String name, String industry, int count) {
-        if (count == 0) {
-            return;
-        }
-        types.add(new CockpitOverviewInfrastructureSnapshot.SourceType(code, name, industry, null, "FACILITY"));
-        counts.add(new CockpitOverviewInfrastructureSnapshot.SourceCount("FACILITY", code, count));
+        return new CockpitOverviewInfrastructureSnapshot(updatedAt, types, counts);
     }
 
-    private static Map<Integer, EquipmentType> equipmentTypes(List<EquipmentType> values) {
-        Map<Integer, EquipmentType> result = new HashMap<>();
-        for (EquipmentType type : values) {
-            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 class FacilityAggregate {
+        private int count;
+        private BigDecimal lengthKm;
     }
 
-    private static boolean isBlank(String value) {
-        return value == null || value.trim().isEmpty();
+    private static String latest(String current, String candidate) {
+        if (candidate == null) return current;
+        return current == null || candidate.compareTo(current) > 0 ? candidate : current;
     }
 
-    private static <T> List<T> safeList(List<T> value) {
-        return value == null ? Collections.emptyList() : value.stream().filter(Objects::nonNull).collect(Collectors.toList());
+    private static String format(LocalDateTime value) {
+        return value == null ? null : DateTimeFormatter.ISO_OFFSET_DATE_TIME
+                .format(value.atZone(ZoneId.of("Asia/Shanghai")));
     }
 
-    private static class FacilityAggregate {
-        private int count;
-        private BigDecimal lengthMeters;
+    private static <T> List<T> safeList(List<T> values) {
+        return values == null ? Collections.emptyList()
+                : values.stream().filter(Objects::nonNull).collect(Collectors.toList());
     }
 }