| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305 |
- --- a/pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/cockpit/CockpitMapSnapshotController.java
- +++ b/pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/cockpit/CockpitMapSnapshotController.java
- @@ -37,9 +37,11 @@
- @RequestParam(value = "industry", required = false) String industry,
- @RequestParam(value = "district", required = false) String district,
- @RequestParam(value = "deviceType", required = false) String deviceType,
- + @RequestParam(value = "pointStatus", required = false) String[] pointStatus,
- @RequestHeader(value = REQUEST_ID_HEADER, required = false) String requestId) {
- CockpitMapSnapshotResponse response = mapSnapshotService.load(
- - resolveRequestId(requestId), page, pageSize, snapshotAt, industry, district, deviceType);
- + resolveRequestId(requestId), page, pageSize, snapshotAt, industry, district, deviceType,
- + pointStatus == null ? null : String.join(",", pointStatus));
- return ResponseEntity.ok(AjaxResult.success(response));
- }
-
- --- a/pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/cockpit/CockpitMonitoringDistributionController.java
- +++ b/pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/cockpit/CockpitMonitoringDistributionController.java
- @@ -33,7 +33,7 @@
- public ResponseEntity<AjaxResult> monitoringDistribution(
- @RequestParam(value = "industry", required = false) String industry,
- @RequestParam(value = "deviceType", required = false) String deviceType,
- - @RequestParam(value = "pointStatus", required = false) String pointStatus,
- + @RequestParam(value = "pointStatus", required = false) String[] pointStatus,
- // 范围维度自执行票 12 起为片区;旧自由串 region 不再是正式契约,签名里已去掉
- @RequestParam(value = "district", required = false) String district,
- @RequestParam(value = "page", defaultValue = "1") Integer page,
- @@ -45,7 +45,7 @@
- resolveRequestId(requestId),
- industry,
- deviceType,
- - pointStatus,
- + pointStatus == null ? null : String.join(",", pointStatus),
- district,
- page,
- pageSize,
- --- a/pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/BusinessCockpitDeviceFactResolverTest.java
- +++ b/pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/BusinessCockpitDeviceFactResolverTest.java
- @@ -24,6 +24,7 @@
-
- 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;
- @@ -131,6 +132,26 @@
- assertFalse(resolver.resolve(device("DEV-5"), null).isWarning(), "CLOSED 预警不算预警中");
- }
-
- + @Test
- + void publishedOfflineAndMissingStatusAreDistinctAndDoNotDependOnTimestamp() {
- + EquipmentStatus offline = status(1, 0, 0);
- + offline.setStatusUpdateTime(null);
- + when(equipmentStatusService.getByDeviceCode("DEV-PUBLISHED")).thenReturn(offline);
- + assertEquals("OFFLINE", resolver.resolve(device("DEV-PUBLISHED"), null).getPublishedDeviceStatus());
- + assertNull(resolver.resolve(device("DEV-MISSING"), null).getPublishedDeviceStatus());
- + }
- +
- + @Test
- + void cancelledStatusSevenNeverPublishesWorkOrderState() {
- + when(equipmentStatusService.getByDeviceCode("DEV-CANCELLED")).thenReturn(status(1, 0, 1));
- + when(workOrderService.list(any(Wrapper.class))).thenReturn(Collections.singletonList(order(7)));
- + CockpitMonitoringPointDeviceSnapshot fact = resolver.resolve(device("DEV-CANCELLED"), null);
- + assertFalse(fact.isWorkOrderInProgress());
- + assertEquals("NORMAL", fact.getPublishedDeviceStatus());
- + assertEquals(CockpitMonitoringPointStatus.NORMAL,
- + CockpitMonitoringPointStatusResolver.resolve(Collections.singletonList(fact)));
- + }
- +
- // ---- fixtures ----
-
- private static EquipmentBase device(String code) {
- --- 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
- @@ -63,7 +63,7 @@
- }
-
- @Test
- - void distributionExposesEnvelopeSixStateDistributionDistrictIndustryTypeAndMapFilter() throws Exception {
- + void distributionExposesEnvelopeGlobalEightStateDistributionAndMapFilter() throws Exception {
- mockMvc.perform(get(ENDPOINT)
- .param("snapshotAt", SNAPSHOT_AT)
- .header("X-Request-Id", "monitoring-distribution-001"))
- @@ -98,17 +98,17 @@
- .andExpect(jsonPath("$.data.unassociatedEquipment[1].pointStatus").value("WARNING"))
- .andExpect(jsonPath("$.data.unassociatedEquipment[2].deviceCode").value("EQ-FREE-003"))
- .andExpect(jsonPath("$.data.unassociatedEquipment[2].pointStatus").value("OFFLINE"))
- - .andExpect(jsonPath("$.data.pointStatusDistribution", hasSize(6)))
- + .andExpect(jsonPath("$.data.pointStatusDistribution", hasSize(8)))
- .andExpect(jsonPath("$.data.pointStatusDistribution[0].status").value("ALARM"))
- .andExpect(jsonPath("$.data.pointStatusDistribution[0].count").value(1))
- .andExpect(jsonPath("$.data.pointStatusDistribution[1].status").value("WARNING"))
- - .andExpect(jsonPath("$.data.pointStatusDistribution[1].count").value(2))
- + .andExpect(jsonPath("$.data.pointStatusDistribution[1].count").value(1))
- .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(2))
- .andExpect(jsonPath("$.data.pointStatusDistribution[4].status").value("OFFLINE"))
- - .andExpect(jsonPath("$.data.pointStatusDistribution[4].count").value(2))
- + .andExpect(jsonPath("$.data.pointStatusDistribution[4].count").value(1))
- .andExpect(jsonPath("$.data.pointStatusDistribution[5].status").value("EMPTY"))
- .andExpect(jsonPath("$.data.pointStatusDistribution[5].count").value(1))
- .andExpect(jsonPath("$.data.districtDistribution", hasSize(3)))
- @@ -194,9 +194,9 @@
- .andExpect(status().isOk())
- .andExpect(jsonPath("$.data.pagination.total").value(2))
- .andExpect(jsonPath("$.data.mapFilter.pointStatus").value("WARNING"))
- - .andExpect(jsonPath("$.data.pointStatusDistribution[0].count").value(0))
- - .andExpect(jsonPath("$.data.pointStatusDistribution[1].count").value(2))
- - .andExpect(jsonPath("$.data.pointStatusDistribution[4].count").value(0));
- + .andExpect(jsonPath("$.data.pointStatusDistribution[0].count").value(1))
- + .andExpect(jsonPath("$.data.pointStatusDistribution[1].count").value(1))
- + .andExpect(jsonPath("$.data.pointStatusDistribution[4].count").value(1));
-
- mockMvc.perform(get(ENDPOINT))
- .andExpect(status().isOk())
- @@ -253,7 +253,7 @@
- .andExpect(status().isOk())
- .andExpect(jsonPath("$.data.pagination.total").value(0))
- .andExpect(jsonPath("$.data.monitoringPoints", hasSize(0)))
- - .andExpect(jsonPath("$.data.pointStatusDistribution[0].count").value(0))
- + .andExpect(jsonPath("$.data.pointStatusDistribution[0].count").value(1))
- .andExpect(jsonPath("$.data.mapFilter.deviceType").value("NOT_A_TYPE"))
- .andExpect(jsonPath("$.data.excluded.invalidCoordinateCount").value(0))
- .andExpect(jsonPath("$.data.freshness.status").value("EMPTY"))
- @@ -279,7 +279,7 @@
- .andExpect(jsonPath("$.code").value(400))
- .andExpect(jsonPath("$.data.category").value("INVALID_REQUEST"))
- .andExpect(jsonPath("$.data.message").value(
- - "pointStatus must be ALARM, WARNING, WORK_ORDER, NORMAL, OFFLINE or EMPTY"));
- + "pointStatus must be ALARM, WARNING, WORK_ORDER, NORMAL, OFFLINE, EMPTY, UNKNOWN or UNASSOCIATED"));
-
- verifyNoInteractions(adapter);
- }
- @@ -351,7 +351,7 @@
- void webSocketDeltaRecomputesStatusCountsAndIgnoresOldOrUnknownDeltas() {
- 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.OFFLINE));
- assertEquals(2, statusCount(initial, CockpitMonitoringPointStatus.NORMAL));
-
- CockpitMonitoringDistributionResponse applied = service.applyDeviceStatusDelta(
- @@ -360,7 +360,7 @@
- "GAS-003", null, "ONLINE", "2026-09-02T09:59:00+08:00",
- "2026-09-02T10:05:00+08:00"));
-
- - assertEquals(1, statusCount(applied, CockpitMonitoringPointStatus.OFFLINE));
- + assertEquals(0, statusCount(applied, CockpitMonitoringPointStatus.OFFLINE));
- assertEquals(3, statusCount(applied, CockpitMonitoringPointStatus.NORMAL));
-
- CockpitMonitoringDistributionResponse ignoredOld = service.applyDeviceStatusDelta(
- @@ -369,7 +369,7 @@
- "GAS-003", null, "OFFLINE", "2026-09-02T09:59:00+08:00",
- "2026-09-02T10:04:00+08:00"));
-
- - assertEquals(1, statusCount(ignoredOld, CockpitMonitoringPointStatus.OFFLINE));
- + assertEquals(0, statusCount(ignoredOld, CockpitMonitoringPointStatus.OFFLINE));
- assertEquals(3, statusCount(ignoredOld, CockpitMonitoringPointStatus.NORMAL));
-
- CockpitMonitoringDistributionResponse ignoredUnknown = service.applyDeviceStatusDelta(
- @@ -378,7 +378,7 @@
- "NO-SUCH-DEVICE", null, "ONLINE", "2026-09-02T09:59:00+08:00",
- "2026-09-02T10:06:00+08:00"));
-
- - assertEquals(1, statusCount(ignoredUnknown, CockpitMonitoringPointStatus.OFFLINE));
- + assertEquals(0, statusCount(ignoredUnknown, CockpitMonitoringPointStatus.OFFLINE));
- assertEquals(3, statusCount(ignoredUnknown, CockpitMonitoringPointStatus.NORMAL));
- }
-
- @@ -455,7 +455,7 @@
- assertEquals(0, all.getExcluded().getInvalidCoordinateCount());
- assertEquals("READY", all.getFreshness().getReason());
- assertEquals(1, statusCount(all, CockpitMonitoringPointStatus.ALARM));
- - assertEquals(1, statusCount(all, CockpitMonitoringPointStatus.NORMAL));
- + assertEquals(0, statusCount(all, CockpitMonitoringPointStatus.NORMAL));
-
- CockpitMonitoringDistributionResponse normal = service.load("normal", null, null, "NORMAL", null, 1, 100, null);
- assertEquals(Collections.singletonList("FREE"), normal.getMapFilter().getDeviceCodes());
- --- /dev/null
- +++ b/pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/CockpitPointStatusContractTest.java
- @@ -0,0 +1,191 @@
- +package com.zksy.web.controller.cockpit;
- +
- +import com.fasterxml.jackson.databind.JsonNode;
- +import com.fasterxml.jackson.databind.ObjectMapper;
- +import com.zksy.cockpit.map.*;
- +import com.zksy.cockpit.monitoring.*;
- +import com.zksy.cockpit.point.CockpitMonitoringPointDeviceSnapshot;
- +import org.junit.jupiter.api.BeforeEach;
- +import org.junit.jupiter.api.Test;
- +import org.springframework.test.web.servlet.MockMvc;
- +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
- +import org.springframework.test.web.servlet.setup.MockMvcBuilders;
- +
- +import java.math.BigDecimal;
- +import java.time.Clock;
- +import java.time.Instant;
- +import java.time.ZoneOffset;
- +import java.util.Arrays;
- +import java.util.Collections;
- +import java.util.List;
- +
- +import static org.junit.jupiter.api.Assertions.assertEquals;
- +import static org.mockito.Mockito.*;
- +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
- +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
- +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
- +
- +/** 同一事实集穿过两个真实控制器/服务,锁定 HTTP 绑定、回显、分页前计数和匹配语义。 */
- +class CockpitPointStatusContractTest {
- + private static final String TOKEN = "2026-09-02T10:00:00+08:00";
- + private static final String MAP = "/api/cockpit/v1/map-snapshot";
- + private static final String DISTRIBUTION = "/api/cockpit/v1/monitoring/distribution";
- + private final ObjectMapper json = new ObjectMapper();
- + private MockMvc mvc;
- + private CockpitMapSnapshotAdapter mapAdapter;
- + private CockpitMonitoringDistributionAdapter distributionAdapter;
- +
- + @BeforeEach
- + void setUp() {
- + Clock clock = Clock.fixed(Instant.parse("2026-09-02T02:00:00Z"), ZoneOffset.UTC);
- + mapAdapter = mock(CockpitMapSnapshotAdapter.class);
- + distributionAdapter = mock(CockpitMonitoringDistributionAdapter.class);
- + when(mapAdapter.load(any())).thenReturn(mapFixture());
- + when(distributionAdapter.load(any())).thenReturn(distributionFixture());
- + mvc = MockMvcBuilders.standaloneSetup(
- + new CockpitMapSnapshotController(new CockpitMapSnapshotService(mapAdapter, clock)),
- + new CockpitMonitoringDistributionController(
- + new CockpitMonitoringDistributionService(distributionAdapter, clock))).build();
- + }
- +
- + @Test
- + void csvAndRepeatedParametersAreOrDeduplicatedAndEchoedIdentically() throws Exception {
- + for (String endpoint : Arrays.asList(MAP, DISTRIBUTION)) {
- + JsonNode result = read(endpoint, "WARNING,ALARM", "ALARM");
- + assertEquals("ALARM,WARNING", result.path("mapFilter").path("pointStatus").asText());
- + assertEquals(3, total(result));
- + assertEquals(2, result.path("monitoringPoints").size());
- + assertEquals(1, result.path("unassociatedEquipment").size());
- + assertEquals("D-A", result.path("unassociatedEquipment").get(0).path("deviceCode").asText());
- + assertEquals(3, result.path("mapFilter").path("deviceCodes").size());
- + }
- + assertEquals(read(MAP, "ALARM,WARNING").path("mapFilter"),
- + read(DISTRIBUTION, "WARNING", "ALARM").path("mapFilter"));
- + }
- +
- + @Test
- + void globalEightBucketsDoNotChangeWithAnyFilterOrPageAndNeverDoubleCountDevices() throws Exception {
- + JsonNode baseline = read(MAP);
- + JsonNode counts = baseline.path("pointStatusDistribution");
- + assertEquals(8, counts.size());
- + assertEquals(1, count(counts, "ALARM"));
- + assertEquals(1, count(counts, "WARNING"));
- + assertEquals(1, count(counts, "NORMAL"));
- + assertEquals(1, count(counts, "EMPTY"));
- + assertEquals(0, count(counts, "UNKNOWN"));
- + assertEquals(4, count(counts, "UNASSOCIATED"));
- + int sum = 0;
- + for (JsonNode row : counts) sum += row.path("count").asInt();
- + assertEquals(8, sum);
- + for (String endpoint : Arrays.asList(MAP, DISTRIBUTION)) {
- + assertEquals(8, total(read(endpoint)));
- + assertEquals(counts, read(endpoint, "ALARM").path("pointStatusDistribution"));
- + JsonNode filtered = response(get(endpoint).param("pointStatus", "ALARM")
- + .param("industry", "GAS").param("district", "0").param("deviceType", "GAS_SENSOR")
- + .param("pageSize", "1").param("page", "2").param("snapshotAt", TOKEN));
- + assertEquals(2, total(filtered));
- + assertEquals(counts, filtered.path("pointStatusDistribution"));
- + JsonNode empty = response(get(endpoint).param("deviceType", "NOT_A_TYPE"));
- + assertEquals(0, total(empty));
- + assertEquals(counts, empty.path("pointStatusDistribution"));
- + }
- + }
- +
- + @Test
- + void emptyUnknownAndUnassociatedUseTheirOwnObjectSemantics() throws Exception {
- + for (String endpoint : Arrays.asList(MAP, DISTRIBUTION)) {
- + JsonNode empty = read(endpoint, "EMPTY");
- + assertEquals(1, total(empty));
- + assertEquals("P-E", empty.path("monitoringPoints").get(0).path("pointCode").asText());
- + assertEquals(0, empty.path("unassociatedEquipment").size());
- + JsonNode unknown = read(endpoint, "UNKNOWN");
- + assertEquals(1, total(unknown));
- + assertEquals("D-U", unknown.path("unassociatedEquipment").get(0).path("deviceCode").asText());
- + assertEquals("UNKNOWN", unknown.path("unassociatedEquipment").get(0).path("deviceStatus").asText());
- + assertEquals(0, unknown.path("monitoringPoints").size());
- + assertEquals(4, total(read(endpoint, "UNASSOCIATED")));
- + JsonNode onlyAlarmDevices = read(endpoint, "UNASSOCIATED", "ALARM");
- + assertEquals(1, total(onlyAlarmDevices));
- + assertEquals(0, onlyAlarmDevices.path("monitoringPoints").size());
- + assertEquals(1, total(read(endpoint, "UNASSOCIATED,UNKNOWN")));
- + // 明确离线不能冒充 UNKNOWN;EMPTY 不适用于设备自身。
- + assertEquals("D-O", read(endpoint, "UNASSOCIATED,OFFLINE")
- + .path("unassociatedEquipment").get(0).path("deviceCode").asText());
- + assertEquals(0, total(read(endpoint, "UNASSOCIATED,EMPTY")));
- + }
- + }
- +
- + @Test
- + void invalidValuesIncludingOneBadRepeatedParameterAreRejectedBeforeReading() throws Exception {
- + for (String endpoint : Arrays.asList(MAP, DISTRIBUTION)) {
- + for (String value : Arrays.asList("BOGUS", "ALARM,BOGUS", "ALARM,", "EMPTY,,WARNING")) {
- + mvc.perform(get(endpoint).param("pointStatus", value))
- + .andExpect(status().isBadRequest())
- + .andExpect(jsonPath("$.data.category").value("INVALID_REQUEST"));
- + }
- + mvc.perform(get(endpoint).param("pointStatus", "ALARM", "BOGUS"))
- + .andExpect(status().isBadRequest())
- + .andExpect(jsonPath("$.data.category").value("INVALID_REQUEST"));
- + }
- + verifyNoInteractions(mapAdapter, distributionAdapter);
- + }
- +
- + private JsonNode read(String endpoint, String... statuses) throws Exception {
- + MockHttpServletRequestBuilder request = get(endpoint);
- + if (statuses.length > 0) request.param("pointStatus", statuses);
- + return response(request);
- + }
- +
- + private JsonNode response(MockHttpServletRequestBuilder request) throws Exception {
- + String content = mvc.perform(request).andExpect(status().isOk())
- + .andReturn().getResponse().getContentAsString();
- + return json.readTree(content).path("data");
- + }
- +
- + private static int total(JsonNode data) {
- + return data.has("pagination") ? data.path("pagination").path("total").asInt() : data.path("total").asInt();
- + }
- +
- + private static int count(JsonNode rows, String status) {
- + for (JsonNode row : rows) if (status.equals(row.path("status").asText())) return row.path("count").asInt();
- + throw new AssertionError("Missing bucket: " + status);
- + }
- +
- + private static List<CockpitMonitoringPointDeviceSnapshot> devices(String code) {
- + if ("P-E".equals(code)) return Collections.emptyList();
- + return Collections.singletonList(new CockpitMonitoringPointDeviceSnapshot(
- + "D-" + code, code, "传感器", true, "P-A".equals(code), "P-W".equals(code), false,
- + Collections.emptyList(), null)); // 缺状态时间不代表未发布。
- + }
- +
- + private static CockpitMapSnapshot mapFixture() {
- + List<CockpitMapSnapshot.SourceMonitoringPoint> points = new java.util.ArrayList<>();
- + for (String code : Arrays.asList("P-A", "P-W", "P-E", "P-N")) {
- + points.add(new CockpitMapSnapshot.SourceMonitoringPoint(code, code, 0,
- + "P-E".equals(code) ? null : "GAS_SENSOR", null, null, false, devices(code)));
- + }
- + List<CockpitMapSnapshot.SourceSmartDevice> devices = new java.util.ArrayList<>();
- + for (String code : Arrays.asList("D-A", "D-N", "D-O", "D-U", "D-P-A")) {
- + devices.add(new CockpitMapSnapshot.SourceSmartDevice(code, code, 0, "GAS_SENSOR", null, null, false,
- + "D-A".equals(code) ? "ALARM" : "D-O".equals(code) ? "OFFLINE" : "D-U".equals(code) ? null : "NORMAL"));
- + }
- + return new CockpitMapSnapshot(TOKEN, Collections.singletonList(
- + new CockpitMapSnapshot.SourceType("GAS_SENSOR", "传感器", "GAS", "SMART_DEVICE")), points, devices);
- + }
- +
- + private static CockpitMonitoringDistributionSnapshot distributionFixture() {
- + List<CockpitMonitoringDistributionSnapshot.SourceMonitoringPoint> points = new java.util.ArrayList<>();
- + for (String code : Arrays.asList("P-A", "P-W", "P-E", "P-N")) {
- + points.add(new CockpitMonitoringDistributionSnapshot.SourceMonitoringPoint(code, code, 0,
- + "P-E".equals(code) ? null : "GAS_SENSOR", null, null, false, devices(code)));
- + }
- + List<CockpitMonitoringDistributionSnapshot.SourceSmartDevice> devices = new java.util.ArrayList<>();
- + for (String code : Arrays.asList("D-A", "D-N", "D-O", "D-U", "D-P-A")) {
- + devices.add(new CockpitMonitoringDistributionSnapshot.SourceSmartDevice(code, code, 0, "GAS_SENSOR",
- + null, null, false, !"D-O".equals(code), "D-A".equals(code), false, false,
- + Collections.emptyList(), null, !"D-U".equals(code)));
- + }
- + return new CockpitMonitoringDistributionSnapshot(TOKEN, TOKEN, Collections.singletonList(
- + new CockpitMonitoringDistributionSnapshot.SourceType("GAS_SENSOR", "传感器", "GAS", "SMART_DEVICE")), points, devices);
- + }
- +}
- --- /dev/null
- +++ b/pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/CockpitPointStatusSnapshotSerializationTest.java
- @@ -0,0 +1,46 @@
- +package com.zksy.web.controller.cockpit;
- +
- +import com.zksy.cockpit.foundation.CockpitCoordinateValidator;
- +import com.zksy.cockpit.map.CockpitMapSnapshot;
- +import com.zksy.cockpit.point.CockpitMonitoringPointDeviceSnapshot;
- +import com.zksy.cockpit.point.CockpitMonitoringPointStatus;
- +import com.zksy.cockpit.point.CockpitMonitoringPointStatusResolver;
- +import com.zksy.framework.config.FastJson2JsonRedisSerializer;
- +import org.junit.jupiter.api.Test;
- +import java.nio.charset.StandardCharsets;
- +import java.util.Arrays;
- +import java.util.Collections;
- +import static org.junit.jupiter.api.Assertions.*;
- +
- +class CockpitPointStatusSnapshotSerializationTest {
- + private final FastJson2JsonRedisSerializer<Object> serializer = new FastJson2JsonRedisSerializer<>(Object.class);
- +
- + @Test
- + void publishedAndMissingDeviceStatesSurviveTheRealRedisSerializer() {
- + CockpitMonitoringPointDeviceSnapshot unpublished = new CockpitMonitoringPointDeviceSnapshot(
- + "BOUND", "设备", null, null, null, CockpitCoordinateValidator.validate(null, null, false),
- + false, false, false, false, Collections.emptyList(), null, false);
- + CockpitMapSnapshot snapshot = new CockpitMapSnapshot("2026-09-02T10:00:00+08:00", Collections.emptyList(),
- + Collections.singletonList(new CockpitMapSnapshot.SourceMonitoringPoint(
- + "P", "点位", null, null, null, null, false, Collections.singletonList(unpublished))),
- + Arrays.asList(new CockpitMapSnapshot.SourceSmartDevice("ALARM", "报警", null, null, null, null, false, "ALARM"),
- + new CockpitMapSnapshot.SourceSmartDevice("UNKNOWN", "未发布", null, null, null, null, false, null)));
- + CockpitMapSnapshot restored = (CockpitMapSnapshot) serializer.deserialize(serializer.serialize(snapshot));
- + assertEquals("ALARM", restored.getSmartDevices().get(0).getDeviceStatus());
- + assertNull(restored.getSmartDevices().get(1).getDeviceStatus());
- + assertFalse(restored.getMonitoringPoints().get(0).getDevices().get(0).isStatusPublished());
- + // 新设备发布标记不改变既有点位六态聚合。
- + assertEquals(CockpitMonitoringPointStatus.OFFLINE,
- + CockpitMonitoringPointStatusResolver.resolve(restored.getMonitoringPoints().get(0).getDevices()));
- + }
- +
- + @Test
- + void legacySnapshotWithoutNewFieldsDoesNotInventDeviceStatus() {
- + String payload = "{\"@type\":\"com.zksy.cockpit.map.CockpitMapSnapshot\","
- + + "\"snapshotAt\":\"2026-09-02T10:00:00+08:00\",\"types\":[],\"monitoringPoints\":[],"
- + + "\"smartDevices\":[{\"deviceCode\":\"LEGACY\",\"deviceName\":\"旧设备\",\"coordinateVerified\":false}]}";
- + CockpitMapSnapshot restored = (CockpitMapSnapshot) serializer.deserialize(payload.getBytes(StandardCharsets.UTF_8));
- + assertEquals("LEGACY", restored.getSmartDevices().get(0).getDeviceCode());
- + assertNull(restored.getSmartDevices().get(0).getDeviceStatus());
- + }
- +}
- --- a/pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/map/BusinessCockpitMapSnapshotAdapter.java
- +++ b/pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/map/BusinessCockpitMapSnapshotAdapter.java
- @@ -257,7 +257,9 @@
- type == null || type.getId() == null ? null : String.valueOf(type.getId()),
- device.getLongitude(),
- device.getLatitude(),
- - false));
- + false,
- + deviceFactResolver.resolve(device, type == null ? null : type.getTypeName())
- + .getPublishedDeviceStatus()));
- }
-
- List<CockpitMapSnapshot.SourceType> sourceTypes = types.values().stream()
- --- a/pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/map/CockpitMapSnapshot.java
- +++ b/pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/map/CockpitMapSnapshot.java
- @@ -105,9 +105,15 @@
- private final BigDecimal longitude;
- private final BigDecimal latitude;
- private final boolean coordinateVerified;
- + private final String deviceStatus;
-
- public SourceSmartDevice(String deviceCode, String deviceName, Integer district, String typeCode,
- BigDecimal longitude, BigDecimal latitude, boolean coordinateVerified) {
- + this(deviceCode, deviceName, district, typeCode, longitude, latitude, coordinateVerified, null);
- + }
- +
- + public SourceSmartDevice(String deviceCode, String deviceName, Integer district, String typeCode,
- + BigDecimal longitude, BigDecimal latitude, boolean coordinateVerified, String deviceStatus) {
- this.deviceCode = deviceCode;
- this.deviceName = deviceName;
- this.district = district;
- @@ -115,8 +121,10 @@
- this.longitude = longitude;
- this.latitude = latitude;
- this.coordinateVerified = coordinateVerified;
- + this.deviceStatus = deviceStatus;
- }
-
- + public String getDeviceStatus() { return deviceStatus; }
- public String getDeviceCode() { return deviceCode; }
- public String getDeviceName() { return deviceName; }
- public Integer getDistrict() { return district; }
- --- a/pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/map/CockpitMapSnapshotResponse.java
- +++ b/pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/map/CockpitMapSnapshotResponse.java
- @@ -1,6 +1,7 @@
- package com.zksy.cockpit.map;
-
- import java.util.List;
- +import com.zksy.cockpit.point.CockpitPointStatusQuery;
-
- public class CockpitMapSnapshotResponse {
- private final String requestId;
- @@ -16,13 +17,14 @@
- private final List<CockpitUnassociatedEquipmentSummary> unassociatedEquipment;
- private final CockpitCoordinateValidationSummary coordinateValidation;
- private final MapFilter mapFilter;
- + private final List<CockpitPointStatusQuery.Count> pointStatusDistribution;
-
- public CockpitMapSnapshotResponse(String requestId, String apiVersion, boolean readOnly, int page, int pageSize,
- String snapshotAt, int total, String batchUpdatedAt, boolean complete,
- List<CockpitMonitoringPointSummary> monitoringPoints,
- List<CockpitUnassociatedEquipmentSummary> unassociatedEquipment,
- CockpitCoordinateValidationSummary coordinateValidation,
- - MapFilter mapFilter) {
- + MapFilter mapFilter, List<CockpitPointStatusQuery.Count> pointStatusDistribution) {
- this.requestId = requestId;
- this.apiVersion = apiVersion;
- this.readOnly = readOnly;
- @@ -36,6 +38,7 @@
- this.unassociatedEquipment = unassociatedEquipment;
- this.coordinateValidation = coordinateValidation;
- this.mapFilter = mapFilter;
- + this.pointStatusDistribution = pointStatusDistribution;
- }
-
- public String getRequestId() { return requestId; }
- @@ -50,6 +53,7 @@
- public List<CockpitMonitoringPointSummary> getMonitoringPoints() { return monitoringPoints; }
- public List<CockpitUnassociatedEquipmentSummary> getUnassociatedEquipment() { return unassociatedEquipment; }
- public CockpitCoordinateValidationSummary getCoordinateValidation() { return coordinateValidation; }
- + public List<CockpitPointStatusQuery.Count> getPointStatusDistribution() { return pointStatusDistribution; }
- public MapFilter getMapFilter() { return mapFilter; }
-
- /**
- @@ -66,10 +70,11 @@
- private final String districtName;
- private final String districtFilter;
- private final String deviceType;
- + private final String pointStatus;
- private final List<String> pointCodes;
- private final List<String> deviceCodes;
-
- - public MapFilter(String industry, Integer district, String districtFilter, String deviceType,
- + public MapFilter(String industry, Integer district, String districtFilter, String deviceType, String pointStatus,
- List<String> pointCodes, List<String> deviceCodes) {
- this.industry = industry;
- this.district = district;
- @@ -77,6 +82,7 @@
- this.districtName = district == null ? null
- : com.zksy.cockpit.foundation.CockpitDistrict.nameOf(district);
- this.deviceType = deviceType;
- + this.pointStatus = pointStatus;
- this.pointCodes = pointCodes;
- this.deviceCodes = deviceCodes;
- }
- @@ -88,6 +94,7 @@
- public String getDistrictName() { return districtName; }
- /** `NONE`(未筛)/ `ASSIGNED`(按片区)/ `UNASSIGNED`(只看未分区)。 */
- public String getDistrictFilter() { return districtFilter; }
- + public String getPointStatus() { return pointStatus; }
- public String getDeviceType() { return deviceType; }
- public List<String> getPointCodes() { return pointCodes; }
- public List<String> getDeviceCodes() { return deviceCodes; }
- --- a/pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/map/CockpitMapSnapshotService.java
- +++ b/pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/map/CockpitMapSnapshotService.java
- @@ -11,6 +11,7 @@
- import com.zksy.cockpit.point.CockpitMonitoringPointDeviceSnapshot;
- import com.zksy.cockpit.point.CockpitMonitoringPointStatus;
- import com.zksy.cockpit.point.CockpitMonitoringPointStatusResolver;
- +import com.zksy.cockpit.point.CockpitPointStatusQuery;
- import org.springframework.stereotype.Service;
-
- import java.math.BigDecimal;
- @@ -55,6 +56,12 @@
- */
- public CockpitMapSnapshotResponse load(String requestId, Integer page, Integer pageSize, String snapshotAt,
- String industry, String district, String deviceType) {
- + return load(requestId, page, pageSize, snapshotAt, industry, district, deviceType, null);
- + }
- +
- + public CockpitMapSnapshotResponse load(String requestId, Integer page, Integer pageSize, String snapshotAt,
- + String industry, String district, String deviceType, String pointStatus) {
- + CockpitPointStatusQuery statusQuery = CockpitPointStatusQuery.parse(pointStatus);
- int safePage = page == null ? 1 : page;
- int safePageSize = pageSize == null ? DEFAULT_PAGE_SIZE : pageSize;
- validatePagination(safePage, safePageSize, snapshotAt);
- @@ -86,6 +93,15 @@
- snapshot, types, selectedIndustry, selectedDistrict, selectedDeviceType);
- List<CockpitUnassociatedEquipmentSummary> equipment = unassociatedEquipment(
- snapshot, types, selectedIndustry, selectedDistrict, selectedDeviceType);
- + CockpitPointStatusQuery.Distribution global = new CockpitPointStatusQuery.Distribution();
- + monitoringPoints(snapshot, types, null, CockpitDistrict.parseQuery(null), null)
- + .forEach(point -> global.addPoint(point.getPointStatus()));
- + unassociatedEquipment(snapshot, types, null, CockpitDistrict.parseQuery(null), null)
- + .forEach(device -> global.addUnassociatedDevice());
- + points = points.stream().filter(point -> statusQuery.matchesPoint(point.getPointStatus()))
- + .collect(Collectors.toList());
- + equipment = equipment.stream().filter(device -> statusQuery.matchesDevice(device.getDeviceStatus()))
- + .collect(Collectors.toList());
- int total = points.size() + equipment.size();
- int fromIndex = Math.min((safePage - 1) * safePageSize, total);
- int toIndex = Math.min(fromIndex + safePageSize, total);
- @@ -109,7 +125,8 @@
- pagePoints,
- pageEquipment,
- coordinateValidation(points, equipment),
- - mapFilter(selectedIndustry, selectedDistrict, selectedDeviceType, points, equipment));
- + mapFilter(selectedIndustry, selectedDistrict, selectedDeviceType, statusQuery.echo(), points, equipment),
- + global.rows());
- }
-
- public CockpitErrorPayload invalidRequest(String requestId, String message) {
- @@ -237,6 +254,7 @@
- }
- return snapshot.getMonitoringPoints().stream()
- .filter(Objects::nonNull)
- + .filter(source -> !isBlank(source.getPointCode()))
- .filter(source -> matchesFilters(
- industry, district, deviceType, source.getTypeCode(), source.getDistrict(), types))
- .map(source -> monitoringPoint(source, types))
- @@ -250,7 +268,8 @@
- Map<String, CockpitMapSnapshot.SourceType> types) {
- List<CockpitMonitoringPointDeviceSnapshot> devices = source.getDevices() == null
- ? Collections.emptyList()
- - : source.getDevices();
- + : source.getDevices().stream().filter(Objects::nonNull)
- + .filter(device -> !isBlank(device.getDeviceCode())).collect(Collectors.toList());
- CockpitMonitoringPointStatus pointStatus = CockpitMonitoringPointStatusResolver.resolve(devices);
- List<String> deviceCodes = devices.stream()
- .filter(Objects::nonNull)
- @@ -287,8 +306,8 @@
- Set<String> associatedDeviceCodes = associatedDeviceCodes(snapshot);
- return snapshot.getSmartDevices().stream()
- .filter(Objects::nonNull)
- - .filter(device -> device.getDeviceCode() == null
- - || !associatedDeviceCodes.contains(device.getDeviceCode()))
- + .filter(device -> !isBlank(device.getDeviceCode()))
- + .filter(device -> !associatedDeviceCodes.contains(device.getDeviceCode().trim()))
- .filter(source -> matchesFilters(
- industry, district, deviceType, source.getTypeCode(), source.getDistrict(), types))
- .map(source -> unassociatedEquipment(source, types))
- @@ -302,11 +321,15 @@
- return Collections.emptySet();
- }
- return snapshot.getMonitoringPoints().stream()
- + .filter(Objects::nonNull)
- .map(CockpitMapSnapshot.SourceMonitoringPoint::getDevices)
- .filter(Objects::nonNull)
- .flatMap(List::stream)
- + .filter(Objects::nonNull)
- .map(CockpitMonitoringPointDeviceSnapshot::getDeviceCode)
- .filter(Objects::nonNull)
- + .map(String::trim)
- + .filter(code -> !code.isEmpty())
- .collect(Collectors.toSet());
- }
-
- @@ -324,11 +347,12 @@
- source.getLongitude(),
- source.getLatitude(),
- CockpitCoordinateValidator.validate(
- - source.getLongitude(), source.getLatitude(), source.isCoordinateVerified()));
- + source.getLongitude(), source.getLatitude(), source.isCoordinateVerified()),
- + CockpitPointStatusQuery.deviceBucket(source.getDeviceStatus()));
- }
-
- private static CockpitMapSnapshotResponse.MapFilter mapFilter(
- - String industry, CockpitDistrict.Query district, String deviceType,
- + String industry, CockpitDistrict.Query district, String deviceType, String pointStatus,
- List<CockpitMonitoringPointSummary> points,
- List<CockpitUnassociatedEquipmentSummary> equipment) {
- Set<String> pointCodes = new LinkedHashSet<>();
- @@ -354,7 +378,7 @@
- industry,
- district.active() ? district.code() : null,
- !district.active() ? "NONE" : (district.unassignedOnly() ? "UNASSIGNED" : "ASSIGNED"),
- - deviceType, sorted(pointCodes), sorted(deviceCodes));
- + deviceType, pointStatus, sorted(pointCodes), sorted(deviceCodes));
- }
-
- private static List<String> sorted(Set<String> values) {
- --- a/pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/map/CockpitUnassociatedEquipmentSummary.java
- +++ b/pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/map/CockpitUnassociatedEquipmentSummary.java
- @@ -15,10 +15,12 @@
- private final BigDecimal longitude;
- private final BigDecimal latitude;
- private final CockpitCoordinateValidation coordinateValidation;
- + private final String deviceStatus;
-
- public CockpitUnassociatedEquipmentSummary(String deviceCode, String deviceName,
- Integer district, String industry, String typeCode, String typeName,
- - BigDecimal longitude, BigDecimal latitude, CockpitCoordinateValidation coordinateValidation) {
- + BigDecimal longitude, BigDecimal latitude, CockpitCoordinateValidation coordinateValidation,
- + String deviceStatus) {
- this.deviceCode = deviceCode;
- this.deviceName = deviceName;
- this.district = CockpitDistrict.codeOf(district);
- @@ -28,8 +30,10 @@
- this.longitude = longitude;
- this.latitude = latitude;
- this.coordinateValidation = coordinateValidation;
- + this.deviceStatus = deviceStatus;
- }
-
- + public String getDeviceStatus() { return deviceStatus; }
- public String getDeviceCode() { return deviceCode; }
- public String getDeviceName() { return deviceName; }
- /** 片区编码(0-城南 1-城北 2-太常);未分区为 {@code null}。 */
- --- a/pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/monitoring/BusinessCockpitMonitoringDistributionAdapter.java
- +++ b/pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/monitoring/BusinessCockpitMonitoringDistributionAdapter.java
- @@ -137,7 +137,8 @@
- fact.isWarning(),
- fact.isWorkOrderInProgress(),
- fact.getStatusReasons(),
- - fact.getStatusTime()));
- + fact.getStatusTime(),
- + fact.isStatusPublished()));
- }
-
- List<CockpitMonitoringDistributionSnapshot.SourceType> sourceTypes = types.values().stream()
- --- a/pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/monitoring/CockpitMonitoringDistributionResponse.java
- +++ b/pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/monitoring/CockpitMonitoringDistributionResponse.java
- @@ -3,6 +3,7 @@
- import com.zksy.cockpit.foundation.CockpitCoordinateValidation;
- import com.zksy.cockpit.foundation.CockpitDistrict;
- import com.zksy.cockpit.point.CockpitMonitoringPointStatus;
- +import com.zksy.cockpit.point.CockpitPointStatusQuery;
-
- import java.math.BigDecimal;
- import java.util.List;
- @@ -19,7 +20,7 @@
- private final List<DistrictDistribution> districtDistribution;
- private final List<IndustryDistribution> industryDistribution;
- private final List<DeviceTypeDistribution> deviceTypeDistribution;
- - private final List<PointStatusCount> pointStatusDistribution;
- + private final List<CockpitPointStatusQuery.Count> pointStatusDistribution;
- private final MapFilter mapFilter;
- private final Pagination pagination;
- private final List<MonitoringPointMarker> monitoringPoints;
- @@ -38,7 +39,7 @@
- List<DistrictDistribution> districtDistribution,
- List<IndustryDistribution> industryDistribution,
- List<DeviceTypeDistribution> deviceTypeDistribution,
- - List<PointStatusCount> pointStatusDistribution,
- + List<CockpitPointStatusQuery.Count> pointStatusDistribution,
- MapFilter mapFilter,
- Pagination pagination,
- List<MonitoringPointMarker> monitoringPoints,
- @@ -74,7 +75,7 @@
- public List<DistrictDistribution> getDistrictDistribution() { return districtDistribution; }
- public List<IndustryDistribution> getIndustryDistribution() { return industryDistribution; }
- public List<DeviceTypeDistribution> getDeviceTypeDistribution() { return deviceTypeDistribution; }
- - public List<PointStatusCount> getPointStatusDistribution() { return pointStatusDistribution; }
- + public List<CockpitPointStatusQuery.Count> getPointStatusDistribution() { return pointStatusDistribution; }
- public MapFilter getMapFilter() { return mapFilter; }
- public Pagination getPagination() { return pagination; }
- public List<MonitoringPointMarker> getMonitoringPoints() { return monitoringPoints; }
- @@ -220,19 +221,6 @@
- public String getName() { return name; }
- }
-
- - public static class PointStatusCount {
- - private final CockpitMonitoringPointStatus status;
- - private final int count;
- -
- - public PointStatusCount(CockpitMonitoringPointStatus status, int count) {
- - this.status = status;
- - this.count = count;
- - }
- -
- - public CockpitMonitoringPointStatus getStatus() { return status; }
- - public int getCount() { return count; }
- - }
- -
- /**
- * 生效范围回显。片区维度自执行票 12 起为 {@code district}:
- * 「未筛」与「只看未分区」的编码都是 {@code null},靠 {@code districtFilter}
- @@ -368,6 +356,9 @@
- }
-
- public String getDeviceCode() { return deviceCode; }
- + public String getDeviceStatus() {
- + return CockpitPointStatusQuery.deviceBucket(pointStatus == null ? null : pointStatus.name());
- + }
- public String getDeviceName() { return deviceName; }
- public Integer getDistrict() { return district; }
- public String getDistrictName() { return districtName; }
- --- 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
- @@ -11,6 +11,7 @@
- import com.zksy.cockpit.point.CockpitMonitoringPointDeviceSnapshot;
- import com.zksy.cockpit.point.CockpitMonitoringPointStatus;
- import com.zksy.cockpit.point.CockpitMonitoringPointStatusResolver;
- +import com.zksy.cockpit.point.CockpitPointStatusQuery;
- import org.springframework.stereotype.Service;
-
- import java.math.BigDecimal;
- @@ -32,9 +33,8 @@
- /**
- * Read-only aggregation for 运行监测 / 监测分布.
- *
- - * <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.
- + * <p>业务对象参与计数、台账与筛选,坐标校验仅决定能否上图。
- + * 八桶分布取全局未筛全集(六态/UNKNOWN 数点位,UNASSOCIATED 数未关联设备)。
- */
- @Service
- public class CockpitMonitoringDistributionService {
- @@ -74,7 +74,7 @@
- Integer pageSize,
- String snapshotAt) {
- String selectedIndustry = resolveIndustry(industry);
- - CockpitMonitoringPointStatus selectedStatus = resolvePointStatus(pointStatus);
- + CockpitPointStatusQuery selectedStatus = CockpitPointStatusQuery.parse(pointStatus);
- CockpitDistrict.Query selectedDistrict = resolveDistrict(district);
- int safePage = page == null ? 1 : page;
- int safePageSize = pageSize == null ? DEFAULT_PAGE_SIZE : pageSize;
- @@ -102,7 +102,7 @@
- String snapshotAt,
- CockpitMonitoringDistributionDeviceStatusDelta delta) {
- String selectedIndustry = resolveIndustry(industry);
- - CockpitMonitoringPointStatus selectedStatus = resolvePointStatus(pointStatus);
- + CockpitPointStatusQuery selectedStatus = CockpitPointStatusQuery.parse(pointStatus);
- CockpitDistrict.Query selectedDistrict = resolveDistrict(district);
- int safePage = page == null ? 1 : page;
- int safePageSize = pageSize == null ? DEFAULT_PAGE_SIZE : pageSize;
- @@ -231,7 +231,7 @@
- String requestId,
- String selectedIndustry,
- String deviceType,
- - CockpitMonitoringPointStatus selectedStatus,
- + CockpitPointStatusQuery selectedStatus,
- CockpitDistrict.Query selectedDistrict,
- int page,
- int pageSize) {
- @@ -241,22 +241,14 @@
- 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())));
- - }
- - }
- - }
- + Set<String> associatedCodes = new LinkedHashSet<>();
- + CockpitPointStatusQuery.Distribution global = new CockpitPointStatusQuery.Distribution();
-
- if (snapshot.getMonitoringPoints() != null) {
- for (CockpitMonitoringDistributionSnapshot.SourceMonitoringPoint point : snapshot.getMonitoringPoints()) {
- @@ -264,54 +256,39 @@
- continue;
- }
- CockpitMonitoringDistributionSnapshot.SourceType type = resolveType(types, point.getTypeCode());
- - String mismatch = classify(type);
- - List<CockpitMonitoringPointDeviceSnapshot> devices = safeDevices(point.getDevices());
- - boolean emptyPointWithUnknownType = UNKNOWN_TYPE.equals(mismatch) && devices.isEmpty();
- - if (mismatch != null && !emptyPointWithUnknownType) {
- - if (UNKNOWN_TYPE.equals(mismatch)) {
- - unknownTypeCount++;
- - } else if (TYPE_KIND_MISMATCH.equals(mismatch)) {
- - typeKindMismatchCount++;
- - } else {
- - nonFormalIndustryCount++;
- - }
- - continue;
- - }
- - 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());
- - // 点位片区按「经其上挂设备归纳」取得(适配器已归纳,这里只做归一与筛选)
- - Integer markerDistrict = CockpitDistrict.codeOf(point.getDistrict());
- - if (!CockpitDistrict.matches(selectedDistrict, markerDistrict)) {
- - continue;
- - }
- List<CockpitMonitoringPointDeviceSnapshot> effective = new ArrayList<>();
- - for (CockpitMonitoringPointDeviceSnapshot device : devices) {
- - if (device == null || isBlank(device.getDeviceCode())) {
- - continue;
- - }
- + for (CockpitMonitoringPointDeviceSnapshot device : safeDevices(point.getDevices())) {
- + if (device == null || isBlank(device.getDeviceCode())) continue;
- + associatedCodes.add(device.getDeviceCode().trim());
- effective.add(effectiveDevice(device));
- }
- CockpitMonitoringPointStatus status = CockpitMonitoringPointStatusResolver.resolve(effective);
- - if (selectedStatus != null && selectedStatus != status) {
- + global.addPoint(status);
- + String mismatch = classify(type);
- + boolean emptyPointWithUnknownType = UNKNOWN_TYPE.equals(mismatch) && effective.isEmpty();
- + if (mismatch != null && !emptyPointWithUnknownType) {
- + if (UNKNOWN_TYPE.equals(mismatch)) unknownTypeCount++;
- + else if (TYPE_KIND_MISMATCH.equals(mismatch)) typeKindMismatchCount++;
- + else nonFormalIndustryCount++;
- continue;
- }
- + String markerIndustry = industryOf(type);
- + String markerTypeCode = point.getTypeCode();
- + Integer markerDistrict = CockpitDistrict.codeOf(point.getDistrict());
- + if (!matchesFilters(selectedIndustry, deviceType, selectedDistrict,
- + markerIndustry, point.getTypeCode(), markerDistrict, type)
- + || !selectedStatus.matchesPoint(status)) continue;
- + CockpitCoordinateValidation validation = CockpitCoordinateValidator.validate(
- + point.getLongitude(), point.getLatitude(), point.isCoordinateVerified());
- + deviceCodes.addAll(codes(effective));
- pointCodes.add(point.getPointCode().trim());
- - deviceCodes.addAll(codes(effective));
- pointMarkers.add(new Marker(
- point.getPointCode().trim(),
- point.getPointName(),
- markerDistrict,
- markerIndustry,
- markerTypeCode,
- - type == null ? "未知" : type.getName(),
- + type == null ? null : type.getName(),
- status,
- CockpitMonitoringPointStatusResolver.summaryReasons(status, effective),
- point.getLongitude(),
- @@ -324,39 +301,30 @@
-
- if (snapshot.getSmartDevices() != null) {
- for (CockpitMonitoringDistributionSnapshot.SourceSmartDevice device : snapshot.getSmartDevices()) {
- - if (device == null || isBlank(device.getDeviceCode())
- - || associatedDeviceCodes.contains(device.getDeviceCode().trim())) {
- + if (device == null || isBlank(device.getDeviceCode())) {
- continue;
- }
- + if (associatedCodes.contains(device.getDeviceCode().trim())) continue;
- + global.addUnassociatedDevice();
- CockpitMonitoringDistributionSnapshot.SourceType type = resolveType(types, device.getTypeCode());
- String mismatch = classify(type);
- if (mismatch != null) {
- - if (UNKNOWN_TYPE.equals(mismatch)) {
- - unknownTypeCount++;
- - } else if (TYPE_KIND_MISMATCH.equals(mismatch)) {
- - typeKindMismatchCount++;
- - } else {
- - nonFormalIndustryCount++;
- - }
- + if (UNKNOWN_TYPE.equals(mismatch)) unknownTypeCount++;
- + else if (TYPE_KIND_MISMATCH.equals(mismatch)) typeKindMismatchCount++;
- + else nonFormalIndustryCount++;
- continue;
- }
- - if (selectedIndustry != null && !selectedIndustry.equals(type.getIndustry())) {
- - continue;
- - }
- - if (!isBlank(deviceType) && !deviceType.trim().equals(type.getCode())) {
- - continue;
- - }
- + String markerIndustry = industryOf(type);
- + String markerTypeCode = device.getTypeCode();
- + Integer markerDistrict = CockpitDistrict.codeOf(device.getDistrict());
- + if (!matchesFilters(selectedIndustry, deviceType, selectedDistrict,
- + markerIndustry, device.getTypeCode(), markerDistrict, type)) continue;
- CockpitCoordinateValidation validation = CockpitCoordinateValidator.validate(
- device.getLongitude(), device.getLatitude(), device.isCoordinateVerified());
- - // 设备自身片区(equipment_base.district)即权威来源
- - Integer markerDistrict = CockpitDistrict.codeOf(device.getDistrict());
- - if (!CockpitDistrict.matches(selectedDistrict, markerDistrict)) {
- - continue;
- - }
- CockpitMonitoringPointDeviceSnapshot fact = new CockpitMonitoringPointDeviceSnapshot(
- device.getDeviceCode().trim(),
- device.getDeviceName(),
- - type.getName(),
- + type == null ? null : type.getName(),
- device.getLongitude(),
- device.getLatitude(),
- validation,
- @@ -365,23 +333,24 @@
- device.isWarning(),
- device.isWorkOrderInProgress(),
- device.getStatusReasons(),
- - device.getStatusTime());
- + device.getStatusTime(), device.isStatusPublished());
- CockpitMonitoringPointDeviceSnapshot applied = effectiveDevice(fact);
- - CockpitMonitoringPointStatus status = CockpitMonitoringPointStatusResolver.deviceStatus(applied);
- - if (selectedStatus != null && selectedStatus != status) {
- - continue;
- - }
- + String publishedStatus = publishedStatus(applied);
- + CockpitMonitoringPointStatus status = "UNKNOWN".equals(publishedStatus)
- + ? null : CockpitMonitoringPointStatus.valueOf(publishedStatus);
- + if (!selectedStatus.matchesDevice(publishedStatus)) continue;
- deviceCodes.add(applied.getDeviceCode().trim());
- deviceMarkers.add(new Marker(
- applied.getDeviceCode().trim(),
- applied.getDeviceName(),
- markerDistrict,
- - type.getIndustry(),
- - type.getCode(),
- - type.getName(),
- + markerIndustry,
- + markerTypeCode,
- + type == null ? null : type.getName(),
- status,
- - CockpitMonitoringPointStatusResolver.summaryReasons(
- - status, Collections.singletonList(applied)),
- + status == null ? applied.getStatusReasons()
- + : CockpitMonitoringPointStatusResolver.summaryReasons(
- + status, Collections.singletonList(applied)),
- device.getLongitude(),
- device.getLatitude(),
- validation,
- @@ -397,18 +366,15 @@
- Map<Integer, Accumulator> byDistrict = new LinkedHashMap<>();
- Map<String, Accumulator> byIndustry = new LinkedHashMap<>();
- Map<String, Accumulator> byType = new LinkedHashMap<>();
- - int[] statusCounts = new int[CockpitMonitoringPointStatus.values().length];
- for (Marker marker : pointMarkers) {
- accumulate(byDistrict, marker.district, marker);
- accumulate(byIndustry, marker.industry, marker);
- accumulate(byType, marker.typeCode, marker);
- - statusCounts[marker.status.ordinal()]++;
- }
- for (Marker marker : deviceMarkers) {
- accumulate(byDistrict, marker.district, marker);
- accumulate(byIndustry, marker.industry, marker);
- accumulate(byType, marker.typeCode, marker);
- - statusCounts[marker.status.ordinal()]++;
- }
-
- List<CockpitMonitoringDistributionResponse.DistrictDistribution> districtRows =
- @@ -417,12 +383,7 @@
- industryRows(byIndustry);
- List<CockpitMonitoringDistributionResponse.DeviceTypeDistribution> typeRows =
- typeRows(byType);
- - List<CockpitMonitoringDistributionResponse.PointStatusCount> statusRows =
- - new ArrayList<>();
- - for (CockpitMonitoringPointStatus status : CockpitMonitoringPointStatus.values()) {
- - statusRows.add(new CockpitMonitoringDistributionResponse.PointStatusCount(
- - status, statusCounts[status.ordinal()]));
- - }
- + List<CockpitPointStatusQuery.Count> statusRows = global.rows();
-
- int total = pointMarkers.size() + deviceMarkers.size();
- int totalPages = total == 0 ? 0 : (total + pageSize - 1) / pageSize;
- @@ -468,7 +429,7 @@
- new CockpitMonitoringDistributionResponse.MapFilter(
- selectedIndustry,
- isBlank(deviceType) ? null : deviceType.trim(),
- - selectedStatus == null ? null : selectedStatus.name(),
- + selectedStatus.echo(),
- // 未筛与「只看未分区」的编码都是 null,靠 districtFilter 区分(口径同 map-snapshot)
- selectedDistrict.code(),
- !selectedDistrict.active() ? "NONE"
- @@ -502,7 +463,10 @@
- source.isWarning(),
- source.isWorkOrderInProgress(),
- source.getStatusReasons(),
- - source.getStatusTime());
- + source.getStatusTime(),
- + runtime == null || runtime.serverUpdatedAt == null
- + ? source.isStatusPublished()
- + : "ONLINE".equals(runtime.monitoringState) || "OFFLINE".equals(runtime.monitoringState));
- }
-
- /**
- @@ -637,18 +601,20 @@
- return query;
- }
-
- - private static CockpitMonitoringPointStatus resolvePointStatus(String pointStatus) {
- - if (isBlank(pointStatus)) {
- - return null;
- - }
- - String resolved = pointStatus.trim();
- - for (CockpitMonitoringPointStatus status : CockpitMonitoringPointStatus.values()) {
- - if (status.name().equals(resolved)) {
- - return status;
- - }
- - }
- - throw new CockpitInvalidRequestException(
- - "pointStatus must be ALARM, WARNING, WORK_ORDER, NORMAL, OFFLINE or EMPTY");
- + private String industryOf(CockpitMonitoringDistributionSnapshot.SourceType type) {
- + return classify(type) == null ? type.getIndustry() : null;
- + }
- +
- + private static boolean matchesFilters(String industry, String deviceType, CockpitDistrict.Query district,
- + String actualIndustry, String typeCode, Integer actualDistrict,
- + CockpitMonitoringDistributionSnapshot.SourceType type) {
- + return (industry == null || industry.equals(actualIndustry))
- + && (isBlank(deviceType) || (type != null && deviceType.trim().equals(typeCode)))
- + && CockpitDistrict.matches(district, actualDistrict);
- + }
- +
- + private static String publishedStatus(CockpitMonitoringPointDeviceSnapshot device) {
- + return CockpitPointStatusQuery.deviceBucket(device.getPublishedDeviceStatus());
- }
-
- private static void validatePagination(int page, int pageSize, String snapshotAt) {
- @@ -822,7 +788,7 @@
- } else {
- deviceCount++;
- }
- - statusCounts[marker.status.ordinal()]++;
- + if (marker.status != null) statusCounts[marker.status.ordinal()]++;
- if (typeName == null) {
- typeName = marker.typeName;
- }
- --- a/pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/monitoring/CockpitMonitoringDistributionSnapshot.java
- +++ b/pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/monitoring/CockpitMonitoringDistributionSnapshot.java
- @@ -173,6 +173,7 @@
- private final boolean workOrderInProgress;
- private final List<String> statusReasons;
- private final String statusTime;
- + private final boolean statusPublished;
-
- public SourceSmartDevice(
- String deviceCode,
- @@ -188,6 +189,14 @@
- boolean workOrderInProgress,
- List<String> statusReasons,
- String statusTime) {
- + this(deviceCode, deviceName, district, typeCode, longitude, latitude, coordinateVerified,
- + online, alarm, warning, workOrderInProgress, statusReasons, statusTime, true);
- + }
- +
- + public SourceSmartDevice(String deviceCode, String deviceName, Integer district, String typeCode,
- + BigDecimal longitude, BigDecimal latitude, boolean coordinateVerified,
- + boolean online, boolean alarm, boolean warning, boolean workOrderInProgress,
- + List<String> statusReasons, String statusTime, boolean statusPublished) {
- this.deviceCode = deviceCode;
- this.deviceName = deviceName;
- this.district = district;
- @@ -201,6 +210,7 @@
- this.workOrderInProgress = workOrderInProgress;
- this.statusReasons = statusReasons;
- this.statusTime = statusTime;
- + this.statusPublished = statusPublished;
- }
-
- public String getDeviceCode() {
- @@ -252,6 +262,8 @@
- return statusReasons;
- }
-
- + public boolean isStatusPublished() { return statusPublished; }
- +
- public String getStatusTime() {
- return statusTime;
- }
- --- a/pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/point/BusinessCockpitDeviceFactResolver.java
- +++ b/pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/point/BusinessCockpitDeviceFactResolver.java
- @@ -4,6 +4,7 @@
- import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
- import com.zksy.base.domain.EquipmentBase;
- import com.zksy.base.domain.EquipmentStatus;
- +import com.zksy.cockpit.foundation.CockpitCoordinateValidator;
- import com.zksy.base.service.EquipmentStatusService;
- import com.zksy.base.warning.domain.EarlyWarning;
- import com.zksy.base.warning.service.EarlyWarningService;
- @@ -77,7 +78,7 @@
- deviceTypeName,
- device.getLongitude(),
- device.getLatitude(),
- - false,
- + CockpitCoordinateValidator.validate(device.getLongitude(), device.getLatitude(), false),
- online,
- alarm,
- hasOpenWarning(deviceCode),
- @@ -86,7 +87,8 @@
- status == null || status.getStatusUpdateTime() == null
- ? null
- : TIME_FORMATTER.format(status.getStatusUpdateTime()
- - .atZone(BUSINESS_ZONE).toOffsetDateTime()));
- + .atZone(BUSINESS_ZONE).toOffsetDateTime()),
- + status != null);
- }
-
- private boolean hasOpenWarning(String deviceCode) {
- --- a/pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/point/CockpitMonitoringPointDeviceSnapshot.java
- +++ b/pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/point/CockpitMonitoringPointDeviceSnapshot.java
- @@ -19,6 +19,7 @@
- private final boolean workOrderInProgress;
- private final List<String> statusReasons;
- private final String statusTime;
- + private final boolean statusPublished;
-
- public CockpitMonitoringPointDeviceSnapshot(String deviceCode, String deviceName, String deviceTypeName,
- boolean online, boolean alarm, boolean warning, boolean workOrderInProgress,
- @@ -40,6 +41,14 @@
- BigDecimal longitude, BigDecimal latitude, CockpitCoordinateValidation coordinateValidation,
- boolean online, boolean alarm, boolean warning, boolean workOrderInProgress,
- List<String> statusReasons, String statusTime) {
- + this(deviceCode, deviceName, deviceTypeName, longitude, latitude, coordinateValidation,
- + online, alarm, warning, workOrderInProgress, statusReasons, statusTime, true);
- + }
- +
- + public CockpitMonitoringPointDeviceSnapshot(String deviceCode, String deviceName, String deviceTypeName,
- + BigDecimal longitude, BigDecimal latitude, CockpitCoordinateValidation coordinateValidation,
- + boolean online, boolean alarm, boolean warning, boolean workOrderInProgress,
- + List<String> statusReasons, String statusTime, boolean statusPublished) {
- this.deviceCode = deviceCode;
- this.deviceName = deviceName;
- this.deviceTypeName = deviceTypeName;
- @@ -52,6 +61,7 @@
- this.workOrderInProgress = workOrderInProgress;
- this.statusReasons = statusReasons;
- this.statusTime = statusTime;
- + this.statusPublished = statusPublished;
- }
-
- public String getDeviceCode() { return deviceCode; }
- @@ -66,4 +76,9 @@
- public boolean isWorkOrderInProgress() { return workOrderInProgress; }
- public List<String> getStatusReasons() { return statusReasons; }
- public String getStatusTime() { return statusTime; }
- + public boolean isStatusPublished() { return statusPublished; }
- + /** 未发布状态与明确离线分离;点位六态聚合仍沿用既有解析器。 */
- + public String getPublishedDeviceStatus() {
- + return statusPublished ? CockpitMonitoringPointStatusResolver.deviceStatus(this).name() : null;
- + }
- }
- --- /dev/null
- +++ b/pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/point/CockpitPointStatusQuery.java
- @@ -0,0 +1,96 @@
- +package com.zksy.cockpit.point;
- +
- +import com.zksy.cockpit.foundation.CockpitInvalidRequestException;
- +import java.util.ArrayList;
- +import java.util.Arrays;
- +import java.util.Collections;
- +import java.util.LinkedHashSet;
- +import java.util.List;
- +import java.util.Set;
- +
- +/** 八桶查询与全局分布。六态/UNKNOWN 数点位,UNASSOCIATED 数未关联设备,互不重复。 */
- +public final class CockpitPointStatusQuery {
- + public static final List<String> BUCKETS = Collections.unmodifiableList(Arrays.asList(
- + "ALARM", "WARNING", "WORK_ORDER", "NORMAL", "OFFLINE", "EMPTY", "UNKNOWN", "UNASSOCIATED"));
- + private static final List<String> DEVICE_STATUSES = Arrays.asList(
- + "ALARM", "WARNING", "WORK_ORDER", "NORMAL", "OFFLINE");
- + private final List<String> selected;
- +
- + private CockpitPointStatusQuery(List<String> selected) {
- + this.selected = selected;
- + }
- +
- + public static CockpitPointStatusQuery parse(String raw) {
- + Set<String> values = new LinkedHashSet<>();
- + if (raw != null && !raw.trim().isEmpty()) {
- + for (String token : raw.split(",", -1)) {
- + String value = token.trim();
- + if (!BUCKETS.contains(value)) {
- + throw new CockpitInvalidRequestException(
- + "pointStatus must be ALARM, WARNING, WORK_ORDER, NORMAL, OFFLINE, EMPTY, UNKNOWN or UNASSOCIATED");
- + }
- + values.add(value);
- + }
- + }
- + // 同一集合不因入参顺序不同而有不同回显。
- + List<String> selected = new ArrayList<>(BUCKETS);
- + selected.retainAll(values);
- + return new CockpitPointStatusQuery(Collections.unmodifiableList(selected));
- + }
- +
- + public String echo() {
- + return selected.isEmpty() ? null : String.join(",", selected);
- + }
- +
- + public boolean matchesPoint(CockpitMonitoringPointStatus status) {
- + return !selected.contains("UNASSOCIATED")
- + && (selected.isEmpty() || selected.contains(pointBucket(status)));
- + }
- +
- + public boolean matchesDevice(String status) {
- + return selected.isEmpty()
- + || (selected.size() == 1 && selected.contains("UNASSOCIATED"))
- + || selected.contains(deviceBucket(status));
- + }
- +
- + public static String pointBucket(CockpitMonitoringPointStatus status) {
- + return status == null ? "UNKNOWN" : status.name();
- + }
- +
- + public static String deviceBucket(String status) {
- + return status != null && DEVICE_STATUSES.contains(status) ? status : "UNKNOWN";
- + }
- +
- + public static class Count {
- + private final String status;
- + private final int count;
- +
- + public Count(String status, int count) {
- + this.status = status;
- + this.count = count;
- + }
- +
- + public String getStatus() { return status; }
- + public int getCount() { return count; }
- + }
- +
- + public static class Distribution {
- + private final int[] counts = new int[BUCKETS.size()];
- +
- + public void addPoint(CockpitMonitoringPointStatus status) {
- + counts[BUCKETS.indexOf(pointBucket(status))]++;
- + }
- +
- + public void addUnassociatedDevice() {
- + counts[BUCKETS.indexOf("UNASSOCIATED")]++;
- + }
- +
- + public List<Count> rows() {
- + List<Count> rows = new ArrayList<>();
- + for (int index = 0; index < BUCKETS.size(); index++) {
- + rows.add(new Count(BUCKETS.get(index), counts[index]));
- + }
- + return rows;
- + }
- + }
- +}
|