--- 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 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 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 deviceTypes = new LinkedHashSet<>(); + Set 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 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 运行监测 / 监测分布. * - *

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. + *

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 pointMarkers = new ArrayList<>(); List deviceMarkers = new ArrayList<>(); Set pointCodes = new LinkedHashSet<>(); Set deviceCodes = new LinkedHashSet<>(); + // Association is a source fact, independent of all display filters. + Set 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 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 typeAccumulators = new LinkedHashMap<>(); - Set selectedPointCodes = new LinkedHashSet<>(); + Set selectedPointCodes = selectMonitoringPointCodes(snapshot, selectedIndustry, deviceType, types); Set selectedDeviceCodes = new LinkedHashSet<>(); List 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 selectMonitoringPointCodes( CockpitMonitoringOverviewSnapshot snapshot, String selectedIndustry, String selectedDeviceType, Map types) { + Set unique = new LinkedHashSet<>(); if (snapshot == null || snapshot.getMonitoringPoints() == null) { - return 0; - } - Set 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 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; } } }