| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614 |
- --- a/pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/BusinessCockpitMonitoringOverviewAdapterTest.java
- +++ b/pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/BusinessCockpitMonitoringOverviewAdapterTest.java
- @@ -140,6 +140,26 @@
- .findFirst().orElseThrow().getIndustry());
- }
-
- + @Test
- + void realEmptyPointsAndMissingDeviceTypesPublishDistinctAssociationFacts() {
- + stubPoints(Arrays.asList(point("P0", "EMPTY"), point("P1", "UNKNOWN")));
- + stubTypes(Collections.emptyList());
- + EquipmentBase bound = device("D", "BOUND", "99");
- + when(equipmentBaseService.list()).thenReturn(Collections.singletonList(bound));
- + when(equipmentBaseService.getEquipmentByPoint("P0")).thenReturn(Collections.emptyList());
- + when(equipmentBaseService.getEquipmentByPoint("P1")).thenReturn(Arrays.asList(bound, bound));
- + CockpitMonitoringOverviewSnapshot snapshot = adapter.load();
- + assertNull(snapshot.getMonitoringPoints().get(0).getTypeCode());
- + assertEquals(0, snapshot.getMonitoringPoints().get(0).getAssociatedDeviceCount());
- + assertNull(snapshot.getMonitoringPoints().get(1).getTypeCode());
- + assertEquals(1, snapshot.getMonitoringPoints().get(1).getAssociatedDeviceCount());
- + CockpitMonitoringOverviewService service = new CockpitMonitoringOverviewService(
- + () -> snapshot, Clock.fixed(Instant.parse("2026-09-02T02:00:00Z"), ZoneOffset.UTC));
- + var response = service.load("empty-real", null, null);
- + assertEquals(1, response.getCounts().getMonitoringPointCount());
- + assertEquals(Collections.singletonList("EMPTY"), response.getMapFilter().getPointCodes());
- + }
- +
- // ---- helpers ----
-
- private static String stateOf(CockpitMonitoringOverviewSnapshot snapshot, String deviceCode) {
- --- a/pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/CockpitMonitoringDistributionControllerTest.java
- +++ b/pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/CockpitMonitoringDistributionControllerTest.java
- @@ -76,7 +76,7 @@
- .andExpect(jsonPath("$.data.timezone").value("Asia/Shanghai"))
- .andExpect(jsonPath("$.data.pagination.page").value(1))
- .andExpect(jsonPath("$.data.pagination.pageSize").value(500))
- - .andExpect(jsonPath("$.data.pagination.total").value(8))
- + .andExpect(jsonPath("$.data.pagination.total").value(9))
- .andExpect(jsonPath("$.data.pagination.totalPages").value(1))
- .andExpect(jsonPath("$.data.pagination.hasMore").value(false))
- .andExpect(jsonPath("$.data.monitoringPoints", hasSize(5)))
- @@ -91,7 +91,7 @@
- .andExpect(jsonPath("$.data.monitoringPoints[3].pointStatus").value("WORK_ORDER"))
- .andExpect(jsonPath("$.data.monitoringPoints[4].pointCode").value("P-WS-2"))
- .andExpect(jsonPath("$.data.monitoringPoints[4].pointStatus").value("EMPTY"))
- - .andExpect(jsonPath("$.data.unassociatedEquipment", hasSize(3)))
- + .andExpect(jsonPath("$.data.unassociatedEquipment", hasSize(4)))
- .andExpect(jsonPath("$.data.unassociatedEquipment[0].deviceCode").value("EQ-FREE-001"))
- .andExpect(jsonPath("$.data.unassociatedEquipment[0].pointStatus").value("NORMAL"))
- .andExpect(jsonPath("$.data.unassociatedEquipment[1].deviceCode").value("EQ-FREE-002"))
- @@ -106,7 +106,7 @@
- .andExpect(jsonPath("$.data.pointStatusDistribution[2].status").value("WORK_ORDER"))
- .andExpect(jsonPath("$.data.pointStatusDistribution[2].count").value(1))
- .andExpect(jsonPath("$.data.pointStatusDistribution[3].status").value("NORMAL"))
- - .andExpect(jsonPath("$.data.pointStatusDistribution[3].count").value(1))
- + .andExpect(jsonPath("$.data.pointStatusDistribution[3].count").value(2))
- .andExpect(jsonPath("$.data.pointStatusDistribution[4].status").value("OFFLINE"))
- .andExpect(jsonPath("$.data.pointStatusDistribution[4].count").value(2))
- .andExpect(jsonPath("$.data.pointStatusDistribution[5].status").value("EMPTY"))
- @@ -115,7 +115,7 @@
- .andExpect(jsonPath("$.data.districtDistribution[0].district").value(0))
- .andExpect(jsonPath("$.data.districtDistribution[0].districtName").value("城南"))
- .andExpect(jsonPath("$.data.districtDistribution[0].pointCount").value(2))
- - .andExpect(jsonPath("$.data.districtDistribution[0].deviceCount").value(4))
- + .andExpect(jsonPath("$.data.districtDistribution[0].deviceCount").value(5))
- .andExpect(jsonPath("$.data.districtDistribution[1].district").value(1))
- .andExpect(jsonPath("$.data.districtDistribution[1].districtName").value("城北"))
- .andExpect(jsonPath("$.data.districtDistribution[1].pointCount").value(2))
- @@ -127,14 +127,14 @@
- .andExpect(jsonPath("$.data.industryDistribution", hasSize(3)))
- .andExpect(jsonPath("$.data.industryDistribution[0].industry").value("GAS"))
- .andExpect(jsonPath("$.data.industryDistribution[0].pointCount").value(2))
- - .andExpect(jsonPath("$.data.industryDistribution[0].deviceCount").value(4))
- + .andExpect(jsonPath("$.data.industryDistribution[0].deviceCount").value(5))
- .andExpect(jsonPath("$.data.industryDistribution[1].industry").value("WATER_SUPPLY"))
- .andExpect(jsonPath("$.data.industryDistribution[1].deviceCount").value(2))
- .andExpect(jsonPath("$.data.industryDistribution[2].industry").value("DRAINAGE"))
- .andExpect(jsonPath("$.data.industryDistribution[2].deviceCount").value(2))
- .andExpect(jsonPath("$.data.deviceTypeDistribution", hasSize(3)))
- .andExpect(jsonPath("$.data.deviceTypeDistribution[0].deviceType.code").value("GAS_SENSOR"))
- - .andExpect(jsonPath("$.data.deviceTypeDistribution[0].deviceCount").value(4))
- + .andExpect(jsonPath("$.data.deviceTypeDistribution[0].deviceCount").value(5))
- .andExpect(jsonPath("$.data.deviceTypeDistribution[1].deviceType.code").value("SEWAGE_ENV"))
- .andExpect(jsonPath("$.data.deviceTypeDistribution[2].deviceType.code").value("WATER_SUPPLY_SENSOR"))
- .andExpect(jsonPath("$.data.mapFilter.industry").value(nullValue()))
- @@ -144,11 +144,11 @@
- .andExpect(jsonPath("$.data.mapFilter.districtName").value(nullValue()))
- .andExpect(jsonPath("$.data.mapFilter.districtFilter").value("NONE"))
- .andExpect(jsonPath("$.data.mapFilter.pointCodes", hasSize(5)))
- - .andExpect(jsonPath("$.data.mapFilter.deviceCodes", hasSize(8)))
- + .andExpect(jsonPath("$.data.mapFilter.deviceCodes", hasSize(9)))
- .andExpect(jsonPath("$.data.excluded.unknownTypeCount").value(1))
- .andExpect(jsonPath("$.data.excluded.typeKindMismatchCount").value(1))
- .andExpect(jsonPath("$.data.excluded.nonFormalIndustryCount").value(1))
- - .andExpect(jsonPath("$.data.excluded.invalidCoordinateCount").value(1))
- + .andExpect(jsonPath("$.data.excluded.invalidCoordinateCount").value(0))
- .andExpect(jsonPath("$.data.freshness.status").value("CURRENT"))
- .andExpect(jsonPath("$.data.freshness.reason").value("READY"));
- }
- @@ -157,24 +157,24 @@
- void associatedDevicesRenderAsOnePointAndPaginationMatchesStatusTotals() throws Exception {
- mockMvc.perform(get(ENDPOINT))
- .andExpect(status().isOk())
- - .andExpect(jsonPath("$.data.pagination.total").value(8))
- + .andExpect(jsonPath("$.data.pagination.total").value(9))
- .andExpect(jsonPath("$.data.pointStatusDistribution[0].count").value(1))
- .andExpect(jsonPath("$.data.monitoringPoints", hasSize(5)))
- - .andExpect(jsonPath("$.data.unassociatedEquipment", hasSize(3)));
- + .andExpect(jsonPath("$.data.unassociatedEquipment", hasSize(4)));
- }
-
- @Test
- void industryFilterNarrowsDistributionAndMapFilter() throws Exception {
- mockMvc.perform(get(ENDPOINT).param("industry", "GAS"))
- .andExpect(status().isOk())
- - .andExpect(jsonPath("$.data.pagination.total").value(3))
- + .andExpect(jsonPath("$.data.pagination.total").value(4))
- .andExpect(jsonPath("$.data.monitoringPoints", hasSize(2)))
- - .andExpect(jsonPath("$.data.unassociatedEquipment", hasSize(1)))
- + .andExpect(jsonPath("$.data.unassociatedEquipment", hasSize(2)))
- .andExpect(jsonPath("$.data.mapFilter.industry").value("GAS"))
- .andExpect(jsonPath("$.data.industryDistribution", hasSize(1)))
- .andExpect(jsonPath("$.data.industryDistribution[0].industry").value("GAS"))
- .andExpect(jsonPath("$.data.pointStatusDistribution[0].count").value(1))
- - .andExpect(jsonPath("$.data.pointStatusDistribution[3].count").value(1))
- + .andExpect(jsonPath("$.data.pointStatusDistribution[3].count").value(2))
- .andExpect(jsonPath("$.data.pointStatusDistribution[4].count").value(1));
- }
-
- @@ -182,7 +182,7 @@
- void deviceTypeFilterNarrowsDistribution() throws Exception {
- mockMvc.perform(get(ENDPOINT).param("deviceType", "GAS_SENSOR"))
- .andExpect(status().isOk())
- - .andExpect(jsonPath("$.data.pagination.total").value(3))
- + .andExpect(jsonPath("$.data.pagination.total").value(4))
- .andExpect(jsonPath("$.data.mapFilter.deviceType").value("GAS_SENSOR"))
- .andExpect(jsonPath("$.data.deviceTypeDistribution", hasSize(1)))
- .andExpect(jsonPath("$.data.deviceTypeDistribution[0].deviceType.code").value("GAS_SENSOR"));
- @@ -200,7 +200,7 @@
-
- mockMvc.perform(get(ENDPOINT))
- .andExpect(status().isOk())
- - .andExpect(jsonPath("$.data.pagination.total").value(8))
- + .andExpect(jsonPath("$.data.pagination.total").value(9))
- .andExpect(jsonPath("$.data.mapFilter.pointStatus").value(nullValue()));
- }
-
- @@ -352,7 +352,7 @@
- CockpitMonitoringDistributionResponse initial = service.load(
- "delta-baseline", null, null, null, null, 1, 100, SNAPSHOT_AT);
- assertEquals(2, statusCount(initial, CockpitMonitoringPointStatus.OFFLINE));
- - assertEquals(1, statusCount(initial, CockpitMonitoringPointStatus.NORMAL));
- + assertEquals(2, statusCount(initial, CockpitMonitoringPointStatus.NORMAL));
-
- CockpitMonitoringDistributionResponse applied = service.applyDeviceStatusDelta(
- "delta-1", null, null, null, null, 1, 100, SNAPSHOT_AT,
- @@ -361,7 +361,7 @@
- "2026-09-02T10:05:00+08:00"));
-
- assertEquals(1, statusCount(applied, CockpitMonitoringPointStatus.OFFLINE));
- - assertEquals(2, statusCount(applied, CockpitMonitoringPointStatus.NORMAL));
- + assertEquals(3, statusCount(applied, CockpitMonitoringPointStatus.NORMAL));
-
- CockpitMonitoringDistributionResponse ignoredOld = service.applyDeviceStatusDelta(
- "delta-2", null, null, null, null, 1, 100, SNAPSHOT_AT,
- @@ -370,7 +370,7 @@
- "2026-09-02T10:04:00+08:00"));
-
- assertEquals(1, statusCount(ignoredOld, CockpitMonitoringPointStatus.OFFLINE));
- - assertEquals(2, statusCount(ignoredOld, CockpitMonitoringPointStatus.NORMAL));
- + assertEquals(3, statusCount(ignoredOld, CockpitMonitoringPointStatus.NORMAL));
-
- CockpitMonitoringDistributionResponse ignoredUnknown = service.applyDeviceStatusDelta(
- "delta-3", null, null, null, null, 1, 100, SNAPSHOT_AT,
- @@ -379,7 +379,114 @@
- "2026-09-02T10:06:00+08:00"));
-
- assertEquals(1, statusCount(ignoredUnknown, CockpitMonitoringPointStatus.OFFLINE));
- - assertEquals(2, statusCount(ignoredUnknown, CockpitMonitoringPointStatus.NORMAL));
- + assertEquals(3, statusCount(ignoredUnknown, CockpitMonitoringPointStatus.NORMAL));
- + }
- +
- + @Test
- + void sixEmptyUntypedPointsRemainInLedgerAndStatusTotalsWithoutVerifiedCoordinates() throws Exception {
- + List<CockpitMonitoringDistributionSnapshot.SourceMonitoringPoint> points = new java.util.ArrayList<>();
- + for (String code : Arrays.asList("GS001", "GS002", "PS001", "PS002", "RQ001", "RQ002")) {
- + points.add(new CockpitMonitoringDistributionSnapshot.SourceMonitoringPoint(
- + code, code, null, null, new BigDecimal("110.399876"), new BigDecimal("28.458287"),
- + false, Collections.emptyList()));
- + }
- + CockpitMonitoringDistributionSnapshot snapshot = new CockpitMonitoringDistributionSnapshot(
- + SNAPSHOT_AT, SNAPSHOT_AT, Collections.emptyList(), points, Collections.emptyList());
- + when(adapter.load(null)).thenReturn(snapshot);
- + when(adapter.load(SNAPSHOT_AT)).thenReturn(snapshot);
- +
- + mockMvc.perform(get(ENDPOINT).param("pointStatus", "EMPTY").param("pageSize", "2"))
- + .andExpect(status().isOk())
- + .andExpect(jsonPath("$.data.pagination.total").value(6))
- + .andExpect(jsonPath("$.data.pagination.totalPages").value(3))
- + .andExpect(jsonPath("$.data.pagination.hasMore").value(true))
- + .andExpect(jsonPath("$.data.monitoringPoints", hasSize(2)))
- + .andExpect(jsonPath("$.data.monitoringPoints[0].pointCode").value("GS001"))
- + .andExpect(jsonPath("$.data.monitoringPoints[0].pointStatus").value("EMPTY"))
- + .andExpect(jsonPath("$.data.monitoringPoints[0].industry").value(nullValue()))
- + .andExpect(jsonPath("$.data.monitoringPoints[0].deviceType.code").value(nullValue()))
- + .andExpect(jsonPath("$.data.monitoringPoints[0].coordinateValidation.renderable").value(false))
- + .andExpect(jsonPath("$.data.pointStatusDistribution[5].count").value(6))
- + .andExpect(jsonPath("$.data.districtDistribution[0].districtName").value("未分区"))
- + .andExpect(jsonPath("$.data.districtDistribution[0].pointCount").value(6))
- + .andExpect(jsonPath("$.data.industryDistribution[0].industry").value(nullValue()))
- + .andExpect(jsonPath("$.data.industryDistribution[0].pointCount").value(6))
- + .andExpect(jsonPath("$.data.deviceTypeDistribution[0].deviceType.code").value(nullValue()))
- + .andExpect(jsonPath("$.data.mapFilter.pointCodes", hasSize(6)))
- + .andExpect(jsonPath("$.data.mapFilter.deviceCodes", hasSize(0)))
- + .andExpect(jsonPath("$.data.excluded.unknownTypeCount").value(0))
- + .andExpect(jsonPath("$.data.excluded.invalidCoordinateCount").value(0))
- + .andExpect(jsonPath("$.data.freshness.status").value("CURRENT"))
- + .andExpect(jsonPath("$.data.freshness.reason").value("READY"));
- + mockMvc.perform(get(ENDPOINT).param("page", "3").param("pageSize", "2").param("snapshotAt", SNAPSHOT_AT))
- + .andExpect(status().isOk())
- + .andExpect(jsonPath("$.data.pagination.total").value(6))
- + .andExpect(jsonPath("$.data.pagination.hasMore").value(false))
- + .andExpect(jsonPath("$.data.monitoringPoints[0].pointCode").value("RQ001"));
- + mockMvc.perform(get(ENDPOINT).param("district", "UNASSIGNED"))
- + .andExpect(status().isOk()).andExpect(jsonPath("$.data.pagination.total").value(6));
- + for (String[] filter : Arrays.asList(new String[]{"industry", "GAS"},
- + new String[]{"deviceType", "GAS_SENSOR"}, new String[]{"district", "0"},
- + new String[]{"pointStatus", "NORMAL"})) {
- + mockMvc.perform(get(ENDPOINT).param(filter[0], filter[1]))
- + .andExpect(status().isOk()).andExpect(jsonPath("$.data.pagination.total").value(0));
- + }
- + }
- +
- + @Test
- + void coordinatesDoNotAffectStatisticsAndAssociatedDevicesNeverBecomeStandaloneMarkers() {
- + CockpitMonitoringPointDeviceSnapshot bound = device(
- + "BOUND", "关联设备", true, true, false, false, Collections.emptyList());
- + CockpitMonitoringDistributionSnapshot snapshot = new CockpitMonitoringDistributionSnapshot(
- + SNAPSHOT_AT, SNAPSHOT_AT, fixture().getTypes(),
- + Collections.singletonList(new CockpitMonitoringDistributionSnapshot.SourceMonitoringPoint(
- + "P", "点位", 0, "GAS_SENSOR", null, null, false, Collections.singletonList(bound))),
- + Arrays.asList(smartDevice("BOUND", "关联设备", 1, "GAS_SENSOR", "110.4", "28.4", false,
- + true, true, false, false, Collections.emptyList()),
- + smartDevice("FREE", "独立设备", 1, "GAS_SENSOR", "181", "28.4", true,
- + true, false, false, false, Collections.emptyList())));
- + when(adapter.load(null)).thenReturn(snapshot);
- + CockpitMonitoringDistributionResponse all = service.load("all", null, null, null, null, 1, 100, null);
- + assertEquals(2, all.getPagination().getTotal());
- + assertEquals(1, all.getUnassociatedEquipment().size());
- + assertEquals("FREE", all.getUnassociatedEquipment().get(0).getDeviceCode());
- + assertTrue(!all.getMonitoringPoints().get(0).getCoordinateValidation().isRenderable());
- + assertTrue(!all.getUnassociatedEquipment().get(0).getCoordinateValidation().isRenderable());
- + assertEquals(0, all.getExcluded().getInvalidCoordinateCount());
- + assertEquals("READY", all.getFreshness().getReason());
- + assertEquals(1, statusCount(all, CockpitMonitoringPointStatus.ALARM));
- + assertEquals(1, statusCount(all, CockpitMonitoringPointStatus.NORMAL));
- +
- + CockpitMonitoringDistributionResponse normal = service.load("normal", null, null, "NORMAL", null, 1, 100, null);
- + assertEquals(Collections.singletonList("FREE"), normal.getMapFilter().getDeviceCodes());
- + assertTrue(normal.getMapFilter().getPointCodes().isEmpty());
- + CockpitMonitoringDistributionResponse district = service.load("district", null, null, null, "1", 1, 100, null);
- + assertEquals(1, district.getPagination().getTotal(), "被筛掉的点位不能让关联设备变成独立设备");
- + assertEquals(Collections.singletonList("FREE"), district.getMapFilter().getDeviceCodes());
- + }
- +
- + @Test
- + void exclusionsCountEachRejectedObjectOnceRegardlessOfAssociatedDeviceCount() {
- + CockpitMonitoringPointDeviceSnapshot d = device("D", "设备", true, false, false, false, Collections.emptyList());
- + CockpitMonitoringDistributionSnapshot snapshot = new CockpitMonitoringDistributionSnapshot(
- + SNAPSHOT_AT, SNAPSHOT_AT, fixture().getTypes(), Arrays.asList(
- + point("EMPTY", "合法空点", null, null, "110.4", "28.4"),
- + point("UNKNOWN", "未知类型", 0, "LEGACY", "110.4", "28.4", d, d, d),
- + point("MISMATCH", "错种类", 0, "WATER_PLANT", "110.4", "28.4", d, d),
- + point("NONFORMAL", "无行业", 0, "SMART_MANHOLE", "110.4", "28.4", d)), Arrays.asList(
- + smartDevice("U", "未知", 0, "LEGACY", "110.4", "28.4", true, true, false, false, false, Collections.emptyList()),
- + smartDevice("M", "错种类", 0, "WATER_PLANT", "110.4", "28.4", true, true, false, false, false, Collections.emptyList()),
- + smartDevice("N", "无行业", 0, "SMART_MANHOLE", "110.4", "28.4", true, true, false, false, false, Collections.emptyList())));
- + when(adapter.load(null)).thenReturn(snapshot);
- + CockpitMonitoringDistributionResponse response = service.load("reconcile", null, null, null, null, 1, 100, null);
- + assertEquals(1, response.getPagination().getTotal());
- + assertEquals(1, statusCount(response, CockpitMonitoringPointStatus.EMPTY));
- + assertEquals(2, response.getExcluded().getUnknownTypeCount());
- + assertEquals(2, response.getExcluded().getTypeKindMismatchCount());
- + assertEquals(2, response.getExcluded().getNonFormalIndustryCount());
- + int excluded = response.getExcluded().getUnknownTypeCount() + response.getExcluded().getTypeKindMismatchCount()
- + + response.getExcluded().getNonFormalIndustryCount() + response.getExcluded().getInvalidCoordinateCount();
- + assertEquals(7, response.getPagination().getTotal() + excluded);
- }
-
- private int statusCount(CockpitMonitoringDistributionResponse response,
- --- a/pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/CockpitMonitoringOverviewControllerTest.java
- +++ b/pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/CockpitMonitoringOverviewControllerTest.java
- @@ -239,6 +239,45 @@
- }
- }
-
- + @Test
- + void sixEmptyUntypedPointsAreCountedAndEchoedWithoutFabricatingDevicesOrIndustry() throws Exception {
- + List<CockpitMonitoringOverviewSnapshot.SourceMonitoringPoint> points = new ArrayList<>();
- + for (String code : Arrays.asList("GS001", "GS002", "PS001", "PS002", "RQ001", "RQ002")) {
- + points.add(new CockpitMonitoringOverviewSnapshot.SourceMonitoringPoint(code, null, 0));
- + }
- + when(adapter.load()).thenReturn(new CockpitMonitoringOverviewSnapshot(
- + "2026-09-02T09:59:50+08:00", Collections.emptyList(), Collections.emptyList(), points));
- + mockMvc.perform(get(ENDPOINT))
- + .andExpect(status().isOk())
- + .andExpect(jsonPath("$.data.counts.monitoringPointCount").value(6))
- + .andExpect(jsonPath("$.data.counts.equipmentCount").value(0))
- + .andExpect(jsonPath("$.data.mapFilter.pointCodes", hasSize(6)))
- + .andExpect(jsonPath("$.data.mapFilter.deviceCodes", hasSize(0)))
- + .andExpect(jsonPath("$.data.status.denominator").value(0))
- + .andExpect(jsonPath("$.data.onlineRate.state").value("EMPTY"))
- + .andExpect(jsonPath("$.data.freshness.status").value("CURRENT"))
- + .andExpect(jsonPath("$.data.freshness.reason").value("READY"));
- + for (String[] filter : Arrays.asList(new String[]{"industry", "GAS"}, new String[]{"deviceType", "GAS_SENSOR"})) {
- + mockMvc.perform(get(ENDPOINT).param(filter[0], filter[1]))
- + .andExpect(status().isOk())
- + .andExpect(jsonPath("$.data.counts.monitoringPointCount").value(0))
- + .andExpect(jsonPath("$.data.mapFilter.pointCodes", hasSize(0)));
- + }
- + }
- +
- + @Test
- + void unknownOrMixedTypeWithDevicesIsNotReinterpretedAsAnEmptyPoint() {
- + when(adapter.load()).thenReturn(new CockpitMonitoringOverviewSnapshot(
- + "2026-09-02T09:59:50+08:00", fixture().getTypes(), Collections.emptyList(), Arrays.asList(
- + new CockpitMonitoringOverviewSnapshot.SourceMonitoringPoint("EMPTY", null, 0),
- + new CockpitMonitoringOverviewSnapshot.SourceMonitoringPoint("MIXED", null, 2),
- + new CockpitMonitoringOverviewSnapshot.SourceMonitoringPoint("LEGACY", null),
- + new CockpitMonitoringOverviewSnapshot.SourceMonitoringPoint("FACILITY", "WATER_PLANT", 0))));
- + CockpitMonitoringOverviewResponse response = service.load("points", null, null);
- + assertEquals(1, response.getCounts().getMonitoringPointCount());
- + assertEquals(Collections.singletonList("EMPTY"), response.getMapFilter().getPointCodes());
- + }
- +
- private static CockpitMonitoringOverviewSnapshot fixture() {
- return new CockpitMonitoringOverviewSnapshot(
- "2026-09-02T09:59:50+08:00",
- --- a/pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/monitoring/BusinessCockpitMonitoringOverviewAdapter.java
- +++ b/pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/monitoring/BusinessCockpitMonitoringOverviewAdapter.java
- @@ -82,10 +82,12 @@
- }
- String pointCode = point.getPointCode().trim();
- Set<String> deviceTypes = new LinkedHashSet<>();
- + Set<String> boundDeviceCodes = new LinkedHashSet<>();
- for (EquipmentBase bound : safeList(equipmentBaseService.getEquipmentByPoint(point.getPointId()))) {
- if (bound == null || isBlank(bound.getEquipmentCode())) {
- continue;
- }
- + boundDeviceCodes.add(bound.getEquipmentCode().trim());
- pointCodeByDevice.putIfAbsent(bound.getEquipmentCode().trim(), pointCode);
- if (bound.getEquipmentTypeId() == null) {
- continue;
- @@ -97,7 +99,8 @@
- }
- sourcePoints.add(new CockpitMonitoringOverviewSnapshot.SourceMonitoringPoint(
- pointCode,
- - deviceTypes.size() == 1 ? deviceTypes.iterator().next() : null));
- + deviceTypes.size() == 1 ? deviceTypes.iterator().next() : null,
- + boundDeviceCodes.size()));
- }
-
- List<CockpitMonitoringOverviewSnapshot.SourceDevice> devices = new ArrayList<>();
- --- a/pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/monitoring/CockpitMonitoringDistributionService.java
- +++ b/pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/monitoring/CockpitMonitoringDistributionService.java
- @@ -32,10 +32,9 @@
- /**
- * Read-only aggregation for 运行监测 / 监测分布.
- *
- - * <p>All distribution statistics use the map rendering scope: monitoring points with
- - * valid verified coordinates render once regardless of how many devices they carry;
- - * unassociated smart devices render as their own marker only when coordinates are
- - * valid and verified. Non-renderable objects are counted in excluded.invalidCoordinateCount.
- + * <p>Statistics and pagination use selected business objects, including empty points
- + * and objects without renderable coordinates. coordinateValidation controls map
- + * presentation only; excluded counts rejected objects, not their associated devices.
- */
- @Service
- public class CockpitMonitoringDistributionService {
- @@ -242,12 +241,22 @@
- int unknownTypeCount = 0;
- int typeKindMismatchCount = 0;
- int nonFormalIndustryCount = 0;
- + // Compatibility field: coordinates no longer exclude business objects.
- int invalidCoordinateCount = 0;
-
- List<Marker> pointMarkers = new ArrayList<>();
- List<Marker> deviceMarkers = new ArrayList<>();
- Set<String> pointCodes = new LinkedHashSet<>();
- Set<String> deviceCodes = new LinkedHashSet<>();
- + // Association is a source fact, independent of all display filters.
- + Set<String> associatedDeviceCodes = new LinkedHashSet<>();
- + if (snapshot.getMonitoringPoints() != null) {
- + for (CockpitMonitoringDistributionSnapshot.SourceMonitoringPoint point : snapshot.getMonitoringPoints()) {
- + if (point != null && !isBlank(point.getPointCode())) {
- + associatedDeviceCodes.addAll(codes(safeDevices(point.getDevices())));
- + }
- + }
- + }
-
- if (snapshot.getMonitoringPoints() != null) {
- for (CockpitMonitoringDistributionSnapshot.SourceMonitoringPoint point : snapshot.getMonitoringPoints()) {
- @@ -257,29 +266,27 @@
- CockpitMonitoringDistributionSnapshot.SourceType type = resolveType(types, point.getTypeCode());
- String mismatch = classify(type);
- List<CockpitMonitoringPointDeviceSnapshot> devices = safeDevices(point.getDevices());
- - if (mismatch != null) {
- - int amount = devices.size();
- + boolean emptyPointWithUnknownType = UNKNOWN_TYPE.equals(mismatch) && devices.isEmpty();
- + if (mismatch != null && !emptyPointWithUnknownType) {
- if (UNKNOWN_TYPE.equals(mismatch)) {
- - unknownTypeCount += amount;
- + unknownTypeCount++;
- } else if (TYPE_KIND_MISMATCH.equals(mismatch)) {
- - typeKindMismatchCount += amount;
- + typeKindMismatchCount++;
- } else {
- - nonFormalIndustryCount += amount;
- + nonFormalIndustryCount++;
- }
- continue;
- }
- - if (selectedIndustry != null && !selectedIndustry.equals(type.getIndustry())) {
- - continue;
- - }
- - if (!isBlank(deviceType) && !deviceType.trim().equals(type.getCode())) {
- + String markerIndustry = type == null ? null : type.getIndustry();
- + String markerTypeCode = type == null ? null : type.getCode();
- + if (selectedIndustry != null && !selectedIndustry.equals(markerIndustry)) {
- + continue;
- + }
- + if (!isBlank(deviceType) && !deviceType.trim().equals(markerTypeCode)) {
- continue;
- }
- CockpitCoordinateValidation validation = CockpitCoordinateValidator.validate(
- point.getLongitude(), point.getLatitude(), point.isCoordinateVerified());
- - if (!validation.isRenderable()) {
- - invalidCoordinateCount++;
- - continue;
- - }
- // 点位片区按「经其上挂设备归纳」取得(适配器已归纳,这里只做归一与筛选)
- Integer markerDistrict = CockpitDistrict.codeOf(point.getDistrict());
- if (!CockpitDistrict.matches(selectedDistrict, markerDistrict)) {
- @@ -290,22 +297,21 @@
- if (device == null || isBlank(device.getDeviceCode())) {
- continue;
- }
- - CockpitMonitoringPointDeviceSnapshot applied = effectiveDevice(device);
- - effective.add(applied);
- - deviceCodes.add(applied.getDeviceCode().trim());
- + effective.add(effectiveDevice(device));
- }
- CockpitMonitoringPointStatus status = CockpitMonitoringPointStatusResolver.resolve(effective);
- if (selectedStatus != null && selectedStatus != status) {
- continue;
- }
- pointCodes.add(point.getPointCode().trim());
- + deviceCodes.addAll(codes(effective));
- pointMarkers.add(new Marker(
- point.getPointCode().trim(),
- point.getPointName(),
- markerDistrict,
- - type.getIndustry(),
- - type.getCode(),
- - type.getName(),
- + markerIndustry,
- + markerTypeCode,
- + type == null ? "未知" : type.getName(),
- status,
- CockpitMonitoringPointStatusResolver.summaryReasons(status, effective),
- point.getLongitude(),
- @@ -318,7 +324,8 @@
-
- if (snapshot.getSmartDevices() != null) {
- for (CockpitMonitoringDistributionSnapshot.SourceSmartDevice device : snapshot.getSmartDevices()) {
- - if (device == null || isBlank(device.getDeviceCode())) {
- + if (device == null || isBlank(device.getDeviceCode())
- + || associatedDeviceCodes.contains(device.getDeviceCode().trim())) {
- continue;
- }
- CockpitMonitoringDistributionSnapshot.SourceType type = resolveType(types, device.getTypeCode());
- @@ -341,10 +348,6 @@
- }
- CockpitCoordinateValidation validation = CockpitCoordinateValidator.validate(
- device.getLongitude(), device.getLatitude(), device.isCoordinateVerified());
- - if (!validation.isRenderable()) {
- - invalidCoordinateCount++;
- - continue;
- - }
- // 设备自身片区(equipment_base.district)即权威来源
- Integer markerDistrict = CockpitDistrict.codeOf(device.getDistrict());
- if (!CockpitDistrict.matches(selectedDistrict, markerDistrict)) {
- @@ -441,7 +444,7 @@
- pageDevices.add(marker.toUnassociatedEquipmentMarker());
- }
-
- - boolean hasResolved = !deviceCodes.isEmpty();
- + boolean hasResolved = total > 0;
- String updatedAt = isBlank(snapshot.getUpdatedAt()) ? businessTime() : snapshot.getUpdatedAt();
-
- return new CockpitMonitoringDistributionResponse(
- --- a/pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/monitoring/CockpitMonitoringOverviewService.java
- +++ b/pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/monitoring/CockpitMonitoringOverviewService.java
- @@ -135,7 +135,7 @@
- int typeKindMismatchCount = 0;
- int nonFormalIndustryCount = 0;
- Map<String, TypeAccumulator> typeAccumulators = new LinkedHashMap<>();
- - Set<String> selectedPointCodes = new LinkedHashSet<>();
- + Set<String> selectedPointCodes = selectMonitoringPointCodes(snapshot, selectedIndustry, deviceType, types);
- Set<String> selectedDeviceCodes = new LinkedHashSet<>();
- List<CockpitMonitoringOverviewSnapshot.SourceDevice> selectedDevices = new ArrayList<>();
-
- @@ -178,12 +178,9 @@
-
- selectedDevices.add(snapshotDevice(snapshot, runtime.deviceCode));
- selectedDeviceCodes.add(runtime.deviceCode);
- - if (!isBlank(runtime.pointCode)) {
- - selectedPointCodes.add(runtime.pointCode.trim());
- - }
- - }
- -
- - int monitoringPointCount = countMonitoringPoints(snapshot, selectedIndustry, deviceType, types);
- + }
- +
- + int monitoringPointCount = selectedPointCodes.size();
- int denominator = online + offline + stale;
- BigDecimal rate = denominator == 0
- ? BigDecimal.ZERO.setScale(4)
- @@ -191,10 +188,11 @@
- .divide(BigDecimal.valueOf(denominator), 4, RoundingMode.HALF_UP);
- String rateState = denominator == 0 ? "EMPTY" : "AVAILABLE";
- String rateReason = denominator == 0 ? "NO_RESOLVED_DEVICES" : null;
- - String freshnessStatus = denominator == 0
- + boolean hasObjects = monitoringPointCount > 0 || !selectedDeviceCodes.isEmpty();
- + String freshnessStatus = !hasObjects
- ? CockpitFreshnessStatus.EMPTY.name()
- : stale == 0 ? CockpitFreshnessStatus.CURRENT.name() : CockpitFreshnessStatus.STALE.name();
- - String freshnessReason = denominator == 0
- + String freshnessReason = !hasObjects
- ? "NO_RESOLVED_DEVICES"
- : stale == 0 ? "READY" : "STALE_SAMPLES";
-
- @@ -263,33 +261,35 @@
- return null;
- }
-
- - private static int countMonitoringPoints(
- + private static Set<String> selectMonitoringPointCodes(
- CockpitMonitoringOverviewSnapshot snapshot,
- String selectedIndustry,
- String selectedDeviceType,
- Map<String, CockpitMonitoringOverviewSnapshot.SourceType> types) {
- + Set<String> unique = new LinkedHashSet<>();
- if (snapshot == null || snapshot.getMonitoringPoints() == null) {
- - return 0;
- - }
- - Set<String> unique = new LinkedHashSet<>();
- + return unique;
- + }
- for (CockpitMonitoringOverviewSnapshot.SourceMonitoringPoint point : snapshot.getMonitoringPoints()) {
- if (point == null || isBlank(point.getPointCode())) {
- continue;
- }
- CockpitMonitoringOverviewSnapshot.SourceType type = isBlank(point.getTypeCode())
- - ? null : types.get(point.getTypeCode());
- - if (type == null || !SMART_DEVICE.equals(type.getObjectKind())) {
- - continue;
- - }
- - if (selectedIndustry != null && !selectedIndustry.equals(type.getIndustry())) {
- - continue;
- - }
- - if (!isBlank(selectedDeviceType) && !selectedDeviceType.trim().equals(point.getTypeCode())) {
- + ? null : types.get(point.getTypeCode().trim());
- + boolean emptyPointWithUnknownType = type == null && point.getAssociatedDeviceCount() == 0;
- + if (!emptyPointWithUnknownType && (type == null || !SMART_DEVICE.equals(type.getObjectKind())
- + || !INDUSTRIES.contains(type.getIndustry()))) {
- + continue;
- + }
- + if (selectedIndustry != null && (type == null || !selectedIndustry.equals(type.getIndustry()))) {
- + continue;
- + }
- + if (!isBlank(selectedDeviceType) && (type == null || !selectedDeviceType.trim().equals(type.getCode()))) {
- continue;
- }
- unique.add(point.getPointCode().trim());
- }
- - return unique.size();
- + return unique;
- }
-
- private static Map<String, CockpitMonitoringOverviewSnapshot.SourceType> types(
- --- a/pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/monitoring/CockpitMonitoringOverviewSnapshot.java
- +++ b/pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/monitoring/CockpitMonitoringOverviewSnapshot.java
- @@ -73,13 +73,21 @@
- public static class SourceMonitoringPoint {
- private final String pointCode;
- private final String typeCode;
- + /** -1 means an older adapter did not publish the association fact. */
- + private final int associatedDeviceCount;
-
- public SourceMonitoringPoint(String pointCode, String typeCode) {
- + this(pointCode, typeCode, -1);
- + }
- +
- + public SourceMonitoringPoint(String pointCode, String typeCode, int associatedDeviceCount) {
- this.pointCode = pointCode;
- this.typeCode = typeCode;
- + this.associatedDeviceCount = associatedDeviceCount;
- }
-
- public String getPointCode() { return pointCode; }
- public String getTypeCode() { return typeCode; }
- + public int getAssociatedDeviceCount() { return associatedDeviceCount; }
- }
- }
|