|
|
@@ -0,0 +1,425 @@
|
|
|
+package com.zksy.web.controller.cockpit;
|
|
|
+
|
|
|
+import com.zksy.cockpit.event.CockpitHandlingEfficiencyAdapter;
|
|
|
+import com.zksy.cockpit.event.CockpitHandlingEfficiencyService;
|
|
|
+import com.zksy.cockpit.event.CockpitHandlingEfficiencySnapshot;
|
|
|
+import com.zksy.cockpit.foundation.CockpitReadException;
|
|
|
+import org.junit.jupiter.api.BeforeEach;
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
+import org.springframework.security.access.prepost.PreAuthorize;
|
|
|
+import org.springframework.test.web.servlet.MockMvc;
|
|
|
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
|
|
+import org.springframework.web.bind.annotation.DeleteMapping;
|
|
|
+import org.springframework.web.bind.annotation.GetMapping;
|
|
|
+import org.springframework.web.bind.annotation.PatchMapping;
|
|
|
+import org.springframework.web.bind.annotation.PostMapping;
|
|
|
+import org.springframework.web.bind.annotation.PutMapping;
|
|
|
+
|
|
|
+import java.lang.reflect.Method;
|
|
|
+import java.time.Clock;
|
|
|
+import java.time.Instant;
|
|
|
+import java.time.ZoneOffset;
|
|
|
+import java.util.Arrays;
|
|
|
+import java.util.Collections;
|
|
|
+
|
|
|
+import static org.hamcrest.Matchers.hasSize;
|
|
|
+import static org.hamcrest.Matchers.nullValue;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
|
+import static org.mockito.Mockito.mock;
|
|
|
+import static org.mockito.Mockito.verifyNoInteractions;
|
|
|
+import static org.mockito.Mockito.when;
|
|
|
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
|
|
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
|
|
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
|
|
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
|
|
+
|
|
|
+class CockpitHandlingEfficiencyControllerTest {
|
|
|
+
|
|
|
+ private static final String ENDPOINT = "/api/cockpit/v1/events/handling-efficiency";
|
|
|
+ private static final String START = "2026-09-02T08:00:00+08:00";
|
|
|
+ private static final String END = "2026-09-02T12:00:00+08:00";
|
|
|
+
|
|
|
+ private CockpitHandlingEfficiencyAdapter adapter;
|
|
|
+ private MockMvc mockMvc;
|
|
|
+
|
|
|
+ @BeforeEach
|
|
|
+ void setUp() {
|
|
|
+ adapter = mock(CockpitHandlingEfficiencyAdapter.class);
|
|
|
+ CockpitHandlingEfficiencyService service = new CockpitHandlingEfficiencyService(
|
|
|
+ adapter,
|
|
|
+ Clock.fixed(Instant.parse("2026-09-02T02:00:00Z"), ZoneOffset.UTC));
|
|
|
+ mockMvc = MockMvcBuilders
|
|
|
+ .standaloneSetup(new CockpitHandlingEfficiencyController(service))
|
|
|
+ .build();
|
|
|
+ when(adapter.load()).thenReturn(fixture());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void warningFamilyExposesSummaryExplicitBasisSevenStatesTrendAndEvents() throws Exception {
|
|
|
+ mockMvc.perform(get(ENDPOINT)
|
|
|
+ .param("eventFamily", "WARNING")
|
|
|
+ .param("startTime", START)
|
|
|
+ .param("endTime", END)
|
|
|
+ .header("X-Request-Id", "efficiency-001"))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.code").value(200))
|
|
|
+ .andExpect(jsonPath("$.data.requestId").value("efficiency-001"))
|
|
|
+ .andExpect(jsonPath("$.data.apiVersion").value("v1"))
|
|
|
+ .andExpect(jsonPath("$.data.readOnly").value(true))
|
|
|
+ .andExpect(jsonPath("$.data.displayName").value("预警效率"))
|
|
|
+ .andExpect(jsonPath("$.data.timezone").value("Asia/Shanghai"))
|
|
|
+ .andExpect(jsonPath("$.data.eventFamily").value("WARNING"))
|
|
|
+ .andExpect(jsonPath("$.data.filters.eventFamily").value("WARNING"))
|
|
|
+ .andExpect(jsonPath("$.data.filters.workOrderState").value(nullValue()))
|
|
|
+ .andExpect(jsonPath("$.data.summary.totalEvents").value(8))
|
|
|
+ .andExpect(jsonPath("$.data.summary.linkedWorkOrderCount").value(6))
|
|
|
+ .andExpect(jsonPath("$.data.summary.missingWorkOrderCount").value(2))
|
|
|
+ .andExpect(jsonPath("$.data.summary.handledCount").value(2))
|
|
|
+ .andExpect(jsonPath("$.data.summary.inProgressCount").value(3))
|
|
|
+ .andExpect(jsonPath("$.data.summary.cancelledCount").value(1))
|
|
|
+ .andExpect(jsonPath("$.data.summary.handlingEfficiency.value").value("0.2500"))
|
|
|
+ .andExpect(jsonPath("$.data.summary.handlingEfficiency.numerator").value(2))
|
|
|
+ .andExpect(jsonPath("$.data.summary.handlingEfficiency.denominator").value(8))
|
|
|
+ .andExpect(jsonPath("$.data.summary.handlingEfficiency.state").value("AVAILABLE"))
|
|
|
+ .andExpect(jsonPath("$.data.summary.handlingEfficiency.basis")
|
|
|
+ .value("CLOSED_WORK_ORDERS_OVER_TOTAL_EVENTS"))
|
|
|
+ .andExpect(jsonPath("$.data.summary.averageDuration.value").value("1.67"))
|
|
|
+ .andExpect(jsonPath("$.data.summary.averageDuration.unit").value("HOURS"))
|
|
|
+ .andExpect(jsonPath("$.data.summary.averageDuration.sampleCount").value(2))
|
|
|
+ .andExpect(jsonPath("$.data.summary.averageDuration.state").value("AVAILABLE"))
|
|
|
+ .andExpect(jsonPath("$.data.workOrderStateCounts", hasSize(7)))
|
|
|
+ .andExpect(jsonPath("$.data.workOrderStateCounts[0].stateCode").value(1))
|
|
|
+ .andExpect(jsonPath("$.data.workOrderStateCounts[0].stateName").value("待派单"))
|
|
|
+ .andExpect(jsonPath("$.data.workOrderStateCounts[0].count").value(1))
|
|
|
+ .andExpect(jsonPath("$.data.workOrderStateCounts[1].stateName").value("待接单"))
|
|
|
+ .andExpect(jsonPath("$.data.workOrderStateCounts[1].count").value(0))
|
|
|
+ .andExpect(jsonPath("$.data.workOrderStateCounts[2].stateName").value("已接单"))
|
|
|
+ .andExpect(jsonPath("$.data.workOrderStateCounts[2].count").value(0))
|
|
|
+ .andExpect(jsonPath("$.data.workOrderStateCounts[3].stateName").value("处理中"))
|
|
|
+ .andExpect(jsonPath("$.data.workOrderStateCounts[3].count").value(1))
|
|
|
+ .andExpect(jsonPath("$.data.workOrderStateCounts[4].stateName").value("待验收"))
|
|
|
+ .andExpect(jsonPath("$.data.workOrderStateCounts[4].count").value(1))
|
|
|
+ .andExpect(jsonPath("$.data.workOrderStateCounts[5].stateName").value("已结案"))
|
|
|
+ .andExpect(jsonPath("$.data.workOrderStateCounts[5].count").value(2))
|
|
|
+ .andExpect(jsonPath("$.data.workOrderStateCounts[6].stateName").value("已取消"))
|
|
|
+ .andExpect(jsonPath("$.data.workOrderStateCounts[6].count").value(1))
|
|
|
+ .andExpect(jsonPath("$.data.events", hasSize(8)))
|
|
|
+ .andExpect(jsonPath("$.data.events[0].eventNo").value("EW-007"))
|
|
|
+ .andExpect(jsonPath("$.data.events[0].hasWorkOrder").value(true))
|
|
|
+ .andExpect(jsonPath("$.data.pagination.total").value(8))
|
|
|
+ .andExpect(jsonPath("$.data.durationTrend", hasSize(5)))
|
|
|
+ .andExpect(jsonPath("$.data.durationTrend[0].timeBucket").value("2026-09-02T08:00:00+08:00"))
|
|
|
+ .andExpect(jsonPath("$.data.durationTrend[4].timeBucket").value("2026-09-02T12:00:00+08:00"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void alarmFamilyIsIndependentFromWarnings() throws Exception {
|
|
|
+ mockMvc.perform(get(ENDPOINT)
|
|
|
+ .param("eventFamily", "ALARM")
|
|
|
+ .param("startTime", START)
|
|
|
+ .param("endTime", END))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.data.eventFamily").value("ALARM"))
|
|
|
+ .andExpect(jsonPath("$.data.summary.totalEvents").value(2))
|
|
|
+ .andExpect(jsonPath("$.data.summary.handledCount").value(1))
|
|
|
+ .andExpect(jsonPath("$.data.summary.handlingEfficiency.value").value("0.5000"))
|
|
|
+ .andExpect(jsonPath("$.data.workOrderStateCounts[5].count").value(1))
|
|
|
+ .andExpect(jsonPath("$.data.workOrderStateCounts[3].count").value(1));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void everyWorkOrderStateIsReportedUnmerged() throws Exception {
|
|
|
+ mockMvc.perform(get(ENDPOINT)
|
|
|
+ .param("eventFamily", "WARNING")
|
|
|
+ .param("startTime", START)
|
|
|
+ .param("endTime", END))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.data.workOrderStateCounts", hasSize(7)))
|
|
|
+ .andExpect(jsonPath("$.data.workOrderStateCounts[3].count").value(1))
|
|
|
+ .andExpect(jsonPath("$.data.workOrderStateCounts[4].count").value(1))
|
|
|
+ .andExpect(jsonPath("$.data.workOrderStateCounts[6].count").value(1));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void emptyWindowReportsEmptyEfficiencyAndDurationStates() throws Exception {
|
|
|
+ mockMvc.perform(get(ENDPOINT)
|
|
|
+ .param("eventFamily", "WARNING")
|
|
|
+ .param("startTime", "2026-09-05T00:00:00+08:00")
|
|
|
+ .param("endTime", "2026-09-05T02:00:00+08:00"))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.data.summary.totalEvents").value(0))
|
|
|
+ .andExpect(jsonPath("$.data.summary.handlingEfficiency.value").value("0.0000"))
|
|
|
+ .andExpect(jsonPath("$.data.summary.handlingEfficiency.state").value("EMPTY"))
|
|
|
+ .andExpect(jsonPath("$.data.summary.handlingEfficiency.reason").value("NO_EVENTS"))
|
|
|
+ .andExpect(jsonPath("$.data.summary.averageDuration.value").value("0.00"))
|
|
|
+ .andExpect(jsonPath("$.data.summary.averageDuration.sampleCount").value(0))
|
|
|
+ .andExpect(jsonPath("$.data.summary.averageDuration.state").value("EMPTY"))
|
|
|
+ .andExpect(jsonPath("$.data.summary.averageDuration.reason")
|
|
|
+ .value("NO_CLOSED_WORK_ORDERS"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void eventsWithoutWorkOrdersAreCountedAsMissingAndKeepStateCountsStable() throws Exception {
|
|
|
+ mockMvc.perform(get(ENDPOINT)
|
|
|
+ .param("eventFamily", "WARNING")
|
|
|
+ .param("startTime", START)
|
|
|
+ .param("endTime", END))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.data.summary.missingWorkOrderCount").value(2))
|
|
|
+ .andExpect(jsonPath("$.data.events", hasSize(8)))
|
|
|
+ .andExpect(jsonPath("$.data.summary.totalEvents").value(8));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void legacyStateCodesResolveThroughTheDictionaryWithoutDroppingEvents() throws Exception {
|
|
|
+ when(adapter.load()).thenReturn(fixtureWithLegacyStateCodes());
|
|
|
+
|
|
|
+ mockMvc.perform(get(ENDPOINT)
|
|
|
+ .param("eventFamily", "WARNING")
|
|
|
+ .param("startTime", START)
|
|
|
+ .param("endTime", END))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.data.summary.totalEvents").value(2))
|
|
|
+ .andExpect(jsonPath("$.data.summary.missingWorkOrderCount").value(0))
|
|
|
+ .andExpect(jsonPath("$.data.summary.cancelledCount").value(2))
|
|
|
+ .andExpect(jsonPath("$.data.workOrderStateCounts[6].count").value(2))
|
|
|
+ .andExpect(jsonPath("$.data.events[0].workOrderStateName").value("已取消"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void averageDurationIgnoresOpenOrdersAndOrdersMissingTimestamps() throws Exception {
|
|
|
+ when(adapter.load()).thenReturn(fixtureWithMixedDurations());
|
|
|
+
|
|
|
+ mockMvc.perform(get(ENDPOINT)
|
|
|
+ .param("eventFamily", "WARNING")
|
|
|
+ .param("startTime", START)
|
|
|
+ .param("endTime", END))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.data.summary.handledCount").value(3))
|
|
|
+ .andExpect(jsonPath("$.data.summary.averageDuration.sampleCount").value(2))
|
|
|
+ .andExpect(jsonPath("$.data.summary.averageDuration.value").value("2.00"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void workOrderStateFilterNarrowsEventsAndMapFilterButNotStateCounts() throws Exception {
|
|
|
+ mockMvc.perform(get(ENDPOINT)
|
|
|
+ .param("eventFamily", "WARNING")
|
|
|
+ .param("workOrderState", "6")
|
|
|
+ .param("startTime", START)
|
|
|
+ .param("endTime", END))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.data.filters.workOrderState").value(6))
|
|
|
+ .andExpect(jsonPath("$.data.pagination.total").value(2))
|
|
|
+ .andExpect(jsonPath("$.data.events", hasSize(2)))
|
|
|
+ .andExpect(jsonPath("$.data.mapFilter.workOrderState").value(6))
|
|
|
+ .andExpect(jsonPath("$.data.mapFilter.deviceCodes", hasSize(2)))
|
|
|
+ .andExpect(jsonPath("$.data.workOrderStateCounts[6].count").value(1))
|
|
|
+ .andExpect(jsonPath("$.data.workOrderStateCounts[5].count").value(2));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void noTruthfulnessOrAccuracyJudgementFieldsExist() throws Exception {
|
|
|
+ mockMvc.perform(get(ENDPOINT)
|
|
|
+ .param("eventFamily", "WARNING")
|
|
|
+ .param("startTime", START)
|
|
|
+ .param("endTime", END))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.data.summary.accuracy").doesNotExist())
|
|
|
+ .andExpect(jsonPath("$.data.summary.truthfulness").doesNotExist())
|
|
|
+ .andExpect(jsonPath("$.data.summary.falsePositiveCount").doesNotExist())
|
|
|
+ .andExpect(jsonPath("$.data.summary.correctAlarmCount").doesNotExist())
|
|
|
+ .andExpect(jsonPath("$.data.accuracy").doesNotExist())
|
|
|
+ .andExpect(jsonPath("$.data.falsePositiveRate").doesNotExist());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void invalidFamilyIndustryLevelStateOrWindowReturnsTypedBadRequestBeforeAdapter() throws Exception {
|
|
|
+ mockMvc.perform(get(ENDPOINT))
|
|
|
+ .andExpect(status().isBadRequest())
|
|
|
+ .andExpect(jsonPath("$.code").value(400))
|
|
|
+ .andExpect(jsonPath("$.data.category").value("INVALID_REQUEST"))
|
|
|
+ .andExpect(jsonPath("$.data.message").value("eventFamily must be WARNING or ALARM"));
|
|
|
+
|
|
|
+ mockMvc.perform(get(ENDPOINT).param("eventFamily", "WHATEVER"))
|
|
|
+ .andExpect(status().isBadRequest());
|
|
|
+
|
|
|
+ mockMvc.perform(get(ENDPOINT).param("eventFamily", "WARNING").param("industry", "MANHOLE"))
|
|
|
+ .andExpect(status().isBadRequest());
|
|
|
+
|
|
|
+ mockMvc.perform(get(ENDPOINT).param("eventFamily", "WARNING").param("level", "9"))
|
|
|
+ .andExpect(status().isBadRequest())
|
|
|
+ .andExpect(jsonPath("$.data.message").value("level must be an integer between 1 and 4"));
|
|
|
+
|
|
|
+ mockMvc.perform(get(ENDPOINT).param("eventFamily", "WARNING").param("workOrderState", "9"))
|
|
|
+ .andExpect(status().isBadRequest())
|
|
|
+ .andExpect(jsonPath("$.data.message").value(
|
|
|
+ "workOrderState must be an integer between 1 and 7"));
|
|
|
+
|
|
|
+ mockMvc.perform(get(ENDPOINT).param("eventFamily", "WARNING").param("startTime", START))
|
|
|
+ .andExpect(status().isBadRequest())
|
|
|
+ .andExpect(jsonPath("$.data.message").value(
|
|
|
+ "start time and end time must be provided together"));
|
|
|
+
|
|
|
+ mockMvc.perform(get(ENDPOINT)
|
|
|
+ .param("eventFamily", "WARNING")
|
|
|
+ .param("startTime", END)
|
|
|
+ .param("endTime", START))
|
|
|
+ .andExpect(status().isBadRequest())
|
|
|
+ .andExpect(jsonPath("$.data.message").value("start time must not be after end time"));
|
|
|
+
|
|
|
+ mockMvc.perform(get(ENDPOINT)
|
|
|
+ .param("eventFamily", "WARNING")
|
|
|
+ .param("page", "0")
|
|
|
+ .param("pageSize", "1001"))
|
|
|
+ .andExpect(status().isBadRequest())
|
|
|
+ .andExpect(jsonPath("$.data.message").value("预警效率分页参数不合法"));
|
|
|
+
|
|
|
+ verifyNoInteractions(adapter);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void defaultWindowCoversSevenDaysEndingAtBusinessTime() throws Exception {
|
|
|
+ mockMvc.perform(get(ENDPOINT).param("eventFamily", "WARNING"))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.data.range.endTime").value("2026-09-02T10:00:00+08:00"))
|
|
|
+ .andExpect(jsonPath("$.data.range.startTime").value("2026-08-26T10:00:00+08:00"))
|
|
|
+ .andExpect(jsonPath("$.data.durationTrend", hasSize(169)));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void serverFailureReturnsTypedServerError() throws Exception {
|
|
|
+ when(adapter.load()).thenThrow(new CockpitReadException("upstream unavailable"));
|
|
|
+
|
|
|
+ mockMvc.perform(get(ENDPOINT)
|
|
|
+ .param("eventFamily", "WARNING")
|
|
|
+ .header("X-Request-Id", "efficiency-500"))
|
|
|
+ .andExpect(status().isInternalServerError())
|
|
|
+ .andExpect(jsonPath("$.code").value(500))
|
|
|
+ .andExpect(jsonPath("$.data.category").value("SERVER_ERROR"))
|
|
|
+ .andExpect(jsonPath("$.data.message").value("驾驶舱预警效率数据暂不可用"))
|
|
|
+ .andExpect(jsonPath("$.data.requestId").value("efficiency-500"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void mutatingRequestIsRejectedBeforeReadAdapterIsCalled() throws Exception {
|
|
|
+ mockMvc.perform(post(ENDPOINT))
|
|
|
+ .andExpect(status().isMethodNotAllowed());
|
|
|
+
|
|
|
+ verifyNoInteractions(adapter);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void controllerOnlyExposesAuthenticatedReadOnlyOperations() {
|
|
|
+ assertTrue(Arrays.stream(CockpitHandlingEfficiencyController.class.getDeclaredMethods())
|
|
|
+ .filter(this::hasRequestMappingAnnotation)
|
|
|
+ .allMatch(CockpitHandlingEfficiencyControllerTest::isReadOnlyMappedMethod));
|
|
|
+
|
|
|
+ assertTrue(Arrays.stream(CockpitHandlingEfficiencyController.class.getDeclaredMethods())
|
|
|
+ .anyMatch(method -> method.isAnnotationPresent(PreAuthorize.class)
|
|
|
+ && "isAuthenticated()".equals(
|
|
|
+ method.getAnnotation(PreAuthorize.class).value())));
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean hasRequestMappingAnnotation(Method method) {
|
|
|
+ return method.isAnnotationPresent(org.springframework.web.bind.annotation.RequestMapping.class)
|
|
|
+ || method.isAnnotationPresent(GetMapping.class)
|
|
|
+ || method.isAnnotationPresent(PostMapping.class)
|
|
|
+ || method.isAnnotationPresent(PutMapping.class)
|
|
|
+ || method.isAnnotationPresent(PatchMapping.class)
|
|
|
+ || method.isAnnotationPresent(DeleteMapping.class);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static boolean isReadOnlyMappedMethod(Method method) {
|
|
|
+ return method.isAnnotationPresent(GetMapping.class)
|
|
|
+ && !method.isAnnotationPresent(PostMapping.class)
|
|
|
+ && !method.isAnnotationPresent(PutMapping.class)
|
|
|
+ && !method.isAnnotationPresent(PatchMapping.class)
|
|
|
+ && !method.isAnnotationPresent(DeleteMapping.class);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static CockpitHandlingEfficiencySnapshot fixture() {
|
|
|
+ return new CockpitHandlingEfficiencySnapshot(
|
|
|
+ "2026-09-02T11:59:50+08:00",
|
|
|
+ types(),
|
|
|
+ Arrays.asList(
|
|
|
+ warning("EW-001", 1, "2026-09-02T08:10:00+08:00", "GAS_SENSOR", "D-GAS-1", "燃气浓度计-001"),
|
|
|
+ warning("EW-002", 2, "2026-09-02T08:40:00+08:00", "GAS_SENSOR", "D-GAS-2", "燃气浓度计-002"),
|
|
|
+ warning("EW-003", 2, "2026-09-02T09:20:00+08:00", "WATER_SUPPLY_SENSOR", "D-WS-1", "供水压力计-001"),
|
|
|
+ warning("EW-004", 3, "2026-09-02T10:05:00+08:00", "SEWAGE_ENV", "D-DR-1", "污水环境监测仪-001"),
|
|
|
+ warning("EW-005", 4, "2026-09-02T10:45:00+08:00", "WATER_SUPPLY_SENSOR", "D-WS-2", "供水压力计-002"),
|
|
|
+ warning("EW-006", 1, "2026-09-02T11:10:00+08:00", "GAS_SENSOR", "D-GAS-3", "燃气浓度计-003"),
|
|
|
+ warning("EW-007", 2, "2026-09-02T11:35:00+08:00", "SEWAGE_ENV", "D-DR-2", "污水环境监测仪-002"),
|
|
|
+ warning("EW-008", 1, "2026-09-02T09:55:00+08:00", "SEWAGE_ENV", "D-DR-3", "污水环境监测仪-003"),
|
|
|
+ warning("EW-LEGACY", 1, "2026-09-02T09:05:00+08:00", "LEGACY", "D-LEGACY", "旧设备")),
|
|
|
+ Arrays.asList(
|
|
|
+ alarm("EA-001", 1, "2026-09-02T10:20:00+08:00", "GAS_SENSOR", "D-GAS-9", "燃气浓度计-009"),
|
|
|
+ alarm("EA-002", 2, "2026-09-02T11:00:00+08:00", "WATER_SUPPLY_SENSOR", "D-WS-9", "供水压力计-009")),
|
|
|
+ Arrays.asList(
|
|
|
+ link("EW-001", 6, "2026-09-02T08:10:00+08:00", "2026-09-02T10:10:00+08:00"),
|
|
|
+ link("EW-002", 4, "2026-09-02T08:40:00+08:00", null),
|
|
|
+ link("EW-003", 5, "2026-09-02T09:20:00+08:00", null),
|
|
|
+ link("EW-004", 1, "2026-09-02T10:05:00+08:00", null),
|
|
|
+ link("EW-006", 6, "2026-09-02T11:10:00+08:00", "2026-09-02T12:30:00+08:00"),
|
|
|
+ link("EW-007", 7, "2026-09-02T11:35:00+08:00", null),
|
|
|
+ link("EA-001", 6, "2026-09-02T10:20:00+08:00", "2026-09-02T14:20:00+08:00"),
|
|
|
+ link("EA-002", 4, "2026-09-02T11:00:00+08:00", null)));
|
|
|
+ }
|
|
|
+
|
|
|
+ private static CockpitHandlingEfficiencySnapshot fixtureWithLegacyStateCodes() {
|
|
|
+ return new CockpitHandlingEfficiencySnapshot(
|
|
|
+ "2026-09-02T11:59:50+08:00",
|
|
|
+ types(),
|
|
|
+ Arrays.asList(
|
|
|
+ warning("EW-001", 1, "2026-09-02T08:10:00+08:00", "GAS_SENSOR", "D-GAS-1", "燃气浓度计-001"),
|
|
|
+ warning("EW-002", 1, "2026-09-02T09:10:00+08:00", "GAS_SENSOR", "D-GAS-2", "燃气浓度计-002")),
|
|
|
+ Collections.emptyList(),
|
|
|
+ Arrays.asList(
|
|
|
+ link("EW-001", 99, "2026-09-02T08:10:00+08:00", null),
|
|
|
+ link("EW-002", 7, "2026-09-02T09:10:00+08:00", null)));
|
|
|
+ }
|
|
|
+
|
|
|
+ private static CockpitHandlingEfficiencySnapshot fixtureWithMixedDurations() {
|
|
|
+ return new CockpitHandlingEfficiencySnapshot(
|
|
|
+ "2026-09-02T11:59:50+08:00",
|
|
|
+ types(),
|
|
|
+ Arrays.asList(
|
|
|
+ warning("EW-001", 1, "2026-09-02T08:00:00+08:00", "GAS_SENSOR", "D-GAS-1", "燃气浓度计-001"),
|
|
|
+ warning("EW-002", 1, "2026-09-02T09:00:00+08:00", "GAS_SENSOR", "D-GAS-2", "燃气浓度计-002"),
|
|
|
+ warning("EW-003", 1, "2026-09-02T10:00:00+08:00", "GAS_SENSOR", "D-GAS-3", "燃气浓度计-003"),
|
|
|
+ warning("EW-004", 1, "2026-09-02T11:00:00+08:00", "GAS_SENSOR", "D-GAS-4", "燃气浓度计-004")),
|
|
|
+ Collections.emptyList(),
|
|
|
+ Arrays.asList(
|
|
|
+ link("EW-001", 6, "2026-09-02T08:00:00+08:00", "2026-09-02T10:00:00+08:00"),
|
|
|
+ link("EW-002", 6, "2026-09-02T09:00:00+08:00", "2026-09-02T11:00:00+08:00"),
|
|
|
+ link("EW-003", 6, "2026-09-02T10:00:00+08:00", null),
|
|
|
+ link("EW-004", 4, "2026-09-02T11:00:00+08:00", null)));
|
|
|
+ }
|
|
|
+
|
|
|
+ private static java.util.List<CockpitHandlingEfficiencySnapshot.SourceType> types() {
|
|
|
+ return Arrays.asList(
|
|
|
+ new CockpitHandlingEfficiencySnapshot.SourceType(
|
|
|
+ "GAS_SENSOR", "燃气浓度计", "GAS", "SMART_DEVICE"),
|
|
|
+ new CockpitHandlingEfficiencySnapshot.SourceType(
|
|
|
+ "WATER_SUPPLY_SENSOR", "供水压力计", "WATER_SUPPLY", "SMART_DEVICE"),
|
|
|
+ new CockpitHandlingEfficiencySnapshot.SourceType(
|
|
|
+ "SEWAGE_ENV", "污水环境监测", "DRAINAGE", "SMART_DEVICE"));
|
|
|
+ }
|
|
|
+
|
|
|
+ private static CockpitHandlingEfficiencySnapshot.SourceWarning warning(
|
|
|
+ String no, Integer level, String eventTime, String typeCode, String deviceCode, String deviceName) {
|
|
|
+ return new CockpitHandlingEfficiencySnapshot.SourceWarning(
|
|
|
+ no, level, eventTime, typeCode, deviceCode, deviceName);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static CockpitHandlingEfficiencySnapshot.SourceAlarm alarm(
|
|
|
+ String id, Integer level, String eventTime, String typeCode, String deviceCode, String deviceName) {
|
|
|
+ return new CockpitHandlingEfficiencySnapshot.SourceAlarm(
|
|
|
+ id, level, eventTime, typeCode, deviceCode, deviceName);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static CockpitHandlingEfficiencySnapshot.SourceWorkOrderLink link(
|
|
|
+ String eventNo, Integer stateCode, String createTime, String realFinishTime) {
|
|
|
+ return new CockpitHandlingEfficiencySnapshot.SourceWorkOrderLink(
|
|
|
+ eventNo, stateCode, createTime, realFinishTime);
|
|
|
+ }
|
|
|
+}
|