|
|
@@ -0,0 +1,349 @@
|
|
|
+package com.zksy.web.controller.cockpit;
|
|
|
+
|
|
|
+import com.zksy.cockpit.detail.CockpitEquipmentDetailAggregateService;
|
|
|
+import com.zksy.cockpit.equipment.CockpitEquipmentArchive;
|
|
|
+import com.zksy.cockpit.equipment.CockpitEquipmentArchiveResponse;
|
|
|
+import com.zksy.cockpit.equipment.CockpitEquipmentArchiveService;
|
|
|
+import com.zksy.cockpit.equipment.CockpitEquipmentArchiveSnapshot;
|
|
|
+import com.zksy.cockpit.equipment.CockpitEquipmentImage;
|
|
|
+import com.zksy.cockpit.event.CockpitAlarmWarningRelation;
|
|
|
+import com.zksy.cockpit.event.CockpitEventAlarm;
|
|
|
+import com.zksy.cockpit.event.CockpitEventReadResponse;
|
|
|
+import com.zksy.cockpit.event.CockpitEventReadService;
|
|
|
+import com.zksy.cockpit.event.CockpitEventSnapshot;
|
|
|
+import com.zksy.cockpit.event.CockpitEventWarning;
|
|
|
+import com.zksy.cockpit.foundation.CockpitDeviceNotFoundException;
|
|
|
+import com.zksy.cockpit.foundation.CockpitReadException;
|
|
|
+import com.zksy.cockpit.type.CockpitMetric;
|
|
|
+import com.zksy.cockpit.type.CockpitTypeMetricsResponse;
|
|
|
+import com.zksy.cockpit.type.CockpitTypeMetricsService;
|
|
|
+import com.zksy.cockpit.workorder.CockpitWorkOrderEventAssociation;
|
|
|
+import com.zksy.cockpit.workorder.CockpitWorkOrderReadResponse;
|
|
|
+import com.zksy.cockpit.workorder.CockpitWorkOrderReadService;
|
|
|
+import com.zksy.cockpit.workorder.CockpitWorkOrderStateDictionary;
|
|
|
+import com.zksy.cockpit.workorder.CockpitWorkOrderSummary;
|
|
|
+import com.zksy.cockpit.workorder.CockpitWorkOrderTimelineEntry;
|
|
|
+import com.zksy.cockpit.workorder.CockpitWorkOrderTimelineResponse;
|
|
|
+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.setup.MockMvcBuilders;
|
|
|
+
|
|
|
+import java.lang.reflect.Method;
|
|
|
+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 static org.hamcrest.Matchers.hasSize;
|
|
|
+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 CockpitEquipmentDetailAggregateControllerTest {
|
|
|
+ private static final String DEVICE_CODE = "TC-PS-WS-202609020012";
|
|
|
+
|
|
|
+ private CockpitEquipmentArchiveService archiveService;
|
|
|
+ private CockpitTypeMetricsService metricsService;
|
|
|
+ private CockpitEventReadService eventService;
|
|
|
+ private CockpitWorkOrderReadService workOrderService;
|
|
|
+ private MockMvc mockMvc;
|
|
|
+
|
|
|
+ @BeforeEach
|
|
|
+ void setUp() {
|
|
|
+ archiveService = mock(CockpitEquipmentArchiveService.class);
|
|
|
+ metricsService = mock(CockpitTypeMetricsService.class);
|
|
|
+ eventService = mock(CockpitEventReadService.class);
|
|
|
+ workOrderService = mock(CockpitWorkOrderReadService.class);
|
|
|
+ CockpitEquipmentDetailAggregateService service = new CockpitEquipmentDetailAggregateService(
|
|
|
+ archiveService,
|
|
|
+ metricsService,
|
|
|
+ eventService,
|
|
|
+ workOrderService,
|
|
|
+ Clock.fixed(Instant.parse("2026-09-02T02:00:00Z"), ZoneOffset.UTC));
|
|
|
+ mockMvc = MockMvcBuilders
|
|
|
+ .standaloneSetup(new CockpitEquipmentDetailAggregateController(service))
|
|
|
+ .build();
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void aggregateReturnsIndependentSuccessfulSections() throws Exception {
|
|
|
+ stubSuccessfulSections();
|
|
|
+
|
|
|
+ mockMvc.perform(get("/api/cockpit/v1/equipments/" + DEVICE_CODE + "/detail-aggregate")
|
|
|
+ .header("X-Request-Id", "detail-aggregate-001"))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.code").value(200))
|
|
|
+ .andExpect(jsonPath("$.data.requestId").value("detail-aggregate-001"))
|
|
|
+ .andExpect(jsonPath("$.data.apiVersion").value("v1"))
|
|
|
+ .andExpect(jsonPath("$.data.readOnly").value(true))
|
|
|
+ .andExpect(jsonPath("$.data.deviceCode").value(DEVICE_CODE))
|
|
|
+ .andExpect(jsonPath("$.data.aggregateState").value("READY"))
|
|
|
+ .andExpect(jsonPath("$.data.archive.state").value("SUCCESS"))
|
|
|
+ .andExpect(jsonPath("$.data.archive.data.deviceCode").value(DEVICE_CODE))
|
|
|
+ .andExpect(jsonPath("$.data.archive.data.deviceName").value("污水环境监测设备"))
|
|
|
+ .andExpect(jsonPath("$.data.metrics.state").value("SUCCESS"))
|
|
|
+ .andExpect(jsonPath("$.data.metrics.data.deviceCode").value(DEVICE_CODE))
|
|
|
+ .andExpect(jsonPath("$.data.metrics.data.state").value("CURRENT"))
|
|
|
+ .andExpect(jsonPath("$.data.metrics.data.metrics", hasSize(1)))
|
|
|
+ .andExpect(jsonPath("$.data.warnings.state").value("SUCCESS"))
|
|
|
+ .andExpect(jsonPath("$.data.warnings.data", hasSize(1)))
|
|
|
+ .andExpect(jsonPath("$.data.alarms.state").value("SUCCESS"))
|
|
|
+ .andExpect(jsonPath("$.data.alarms.data", hasSize(1)))
|
|
|
+ .andExpect(jsonPath("$.data.alarmWarningRelations.state").value("SUCCESS"))
|
|
|
+ .andExpect(jsonPath("$.data.alarmWarningRelations.data", hasSize(1)))
|
|
|
+ .andExpect(jsonPath("$.data.workOrders.state").value("SUCCESS"))
|
|
|
+ .andExpect(jsonPath("$.data.workOrders.data", hasSize(1)))
|
|
|
+ .andExpect(jsonPath("$.data.timelines.state").value("SUCCESS"))
|
|
|
+ .andExpect(jsonPath("$.data.timelines.data", hasSize(1)))
|
|
|
+ .andExpect(jsonPath("$.data.timelines.data[0].timelineEntries", hasSize(1)))
|
|
|
+ .andExpect(jsonPath("$.data.latestImage.state").value("SUCCESS"))
|
|
|
+ .andExpect(jsonPath("$.data.latestImage.data.url").value("https://cockpit.example/latest.jpg"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void successfulEmptySectionsDoNotBecomeFailures() throws Exception {
|
|
|
+ stubEmptySections();
|
|
|
+
|
|
|
+ mockMvc.perform(get("/api/cockpit/v1/equipments/" + DEVICE_CODE + "/detail-aggregate"))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.data.aggregateState").value("EMPTY"))
|
|
|
+ .andExpect(jsonPath("$.data.archive.state").value("EMPTY"))
|
|
|
+ .andExpect(jsonPath("$.data.metrics.state").value("EMPTY"))
|
|
|
+ .andExpect(jsonPath("$.data.warnings.state").value("EMPTY"))
|
|
|
+ .andExpect(jsonPath("$.data.alarms.state").value("EMPTY"))
|
|
|
+ .andExpect(jsonPath("$.data.alarmWarningRelations.state").value("EMPTY"))
|
|
|
+ .andExpect(jsonPath("$.data.workOrders.state").value("EMPTY"))
|
|
|
+ .andExpect(jsonPath("$.data.timelines.state").value("EMPTY"))
|
|
|
+ .andExpect(jsonPath("$.data.latestImage.state").value("EMPTY"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void singleArchiveFailurePreservesSuccessfulSiblings() throws Exception {
|
|
|
+ when(archiveService.load("detail-aggregate-001", DEVICE_CODE))
|
|
|
+ .thenThrow(new CockpitReadException("archive read failed"));
|
|
|
+ stubSuccessfulSectionsExceptArchive();
|
|
|
+
|
|
|
+ mockMvc.perform(get("/api/cockpit/v1/equipments/" + DEVICE_CODE + "/detail-aggregate")
|
|
|
+ .header("X-Request-Id", "detail-aggregate-001"))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.data.aggregateState").value("PARTIAL"))
|
|
|
+ .andExpect(jsonPath("$.data.archive.state").value("FAILED"))
|
|
|
+ .andExpect(jsonPath("$.data.archive.reason").value("READ_FAILURE"))
|
|
|
+ .andExpect(jsonPath("$.data.metrics.state").value("SUCCESS"))
|
|
|
+ .andExpect(jsonPath("$.data.warnings.state").value("SUCCESS"))
|
|
|
+ .andExpect(jsonPath("$.data.alarms.state").value("SUCCESS"))
|
|
|
+ .andExpect(jsonPath("$.data.workOrders.state").value("SUCCESS"))
|
|
|
+ .andExpect(jsonPath("$.data.timelines.state").value("SUCCESS"))
|
|
|
+ .andExpect(jsonPath("$.data.latestImage.state").value("FAILED"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void multipleSectionFailuresAreReportedIndependently() throws Exception {
|
|
|
+ stubArchive();
|
|
|
+ when(metricsService.loadCurrent("detail-aggregate-001", DEVICE_CODE))
|
|
|
+ .thenThrow(new CockpitReadException("metrics failed"));
|
|
|
+ when(eventService.loadCurrent("detail-aggregate-001", DEVICE_CODE))
|
|
|
+ .thenThrow(new CockpitDeviceNotFoundException("events missing"));
|
|
|
+ when(workOrderService.loadByDevice("detail-aggregate-001", DEVICE_CODE))
|
|
|
+ .thenThrow(new CockpitReadException("work orders failed"));
|
|
|
+
|
|
|
+ mockMvc.perform(get("/api/cockpit/v1/equipments/" + DEVICE_CODE + "/detail-aggregate")
|
|
|
+ .header("X-Request-Id", "detail-aggregate-001"))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.data.aggregateState").value("PARTIAL"))
|
|
|
+ .andExpect(jsonPath("$.data.archive.state").value("SUCCESS"))
|
|
|
+ .andExpect(jsonPath("$.data.metrics.state").value("FAILED"))
|
|
|
+ .andExpect(jsonPath("$.data.metrics.reason").value("READ_FAILURE"))
|
|
|
+ .andExpect(jsonPath("$.data.warnings.state").value("FAILED"))
|
|
|
+ .andExpect(jsonPath("$.data.warnings.reason").value("NOT_FOUND"))
|
|
|
+ .andExpect(jsonPath("$.data.alarms.state").value("FAILED"))
|
|
|
+ .andExpect(jsonPath("$.data.alarms.reason").value("NOT_FOUND"))
|
|
|
+ .andExpect(jsonPath("$.data.workOrders.state").value("FAILED"))
|
|
|
+ .andExpect(jsonPath("$.data.workOrders.reason").value("READ_FAILURE"))
|
|
|
+ .andExpect(jsonPath("$.data.timelines.state").value("FAILED"))
|
|
|
+ .andExpect(jsonPath("$.data.timelines.reason").value("SOURCE_SECTION_FAILED"))
|
|
|
+ .andExpect(jsonPath("$.data.latestImage.state").value("SUCCESS"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void staleMetricsRemainVisibleWithIndependentStaleState() throws Exception {
|
|
|
+ stubArchive();
|
|
|
+ stubEventSection();
|
|
|
+ when(metricsService.loadCurrent("detail-aggregate-001", DEVICE_CODE))
|
|
|
+ .thenReturn(metricsResponse("STALE", "SAMPLE_OUTDATED"));
|
|
|
+
|
|
|
+ mockMvc.perform(get("/api/cockpit/v1/equipments/" + DEVICE_CODE + "/detail-aggregate")
|
|
|
+ .header("X-Request-Id", "detail-aggregate-001"))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.data.aggregateState").value("PARTIAL"))
|
|
|
+ .andExpect(jsonPath("$.data.metrics.state").value("STALE"))
|
|
|
+ .andExpect(jsonPath("$.data.metrics.data.state").value("STALE"))
|
|
|
+ .andExpect(jsonPath("$.data.metrics.data.metrics", hasSize(1)));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void deviceCodeMismatchDoesNotLeakForeignSectionData() throws Exception {
|
|
|
+ stubArchive();
|
|
|
+ when(metricsService.loadCurrent("detail-aggregate-001", DEVICE_CODE))
|
|
|
+ .thenReturn(metricsResponse("CURRENT", "READY"));
|
|
|
+ when(eventService.loadCurrent("detail-aggregate-001", DEVICE_CODE))
|
|
|
+ .thenReturn(eventResponse("CN-GS-202609020099", "EMPTY", "NO_EVENTS",
|
|
|
+ Collections.emptyList(), Collections.emptyList(), Collections.emptyList()));
|
|
|
+
|
|
|
+ mockMvc.perform(get("/api/cockpit/v1/equipments/" + DEVICE_CODE + "/detail-aggregate")
|
|
|
+ .header("X-Request-Id", "detail-aggregate-001"))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.data.metrics.state").value("SUCCESS"))
|
|
|
+ .andExpect(jsonPath("$.data.warnings.state").value("FAILED"))
|
|
|
+ .andExpect(jsonPath("$.data.warnings.reason").value("DEVICE_CODE_MISMATCH"))
|
|
|
+ .andExpect(jsonPath("$.data.warnings.data").doesNotExist())
|
|
|
+ .andExpect(jsonPath("$.data.alarms.state").value("FAILED"))
|
|
|
+ .andExpect(jsonPath("$.data.alarms.reason").value("DEVICE_CODE_MISMATCH"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void controllerExposesReadOnlyMappedMethodsOnly() throws Exception {
|
|
|
+ Method[] methods = CockpitEquipmentDetailAggregateController.class.getDeclaredMethods();
|
|
|
+ for (Method method : methods) {
|
|
|
+ if (method.isAnnotationPresent(org.springframework.web.bind.annotation.GetMapping.class)) {
|
|
|
+ assertTrue(!method.isAnnotationPresent(org.springframework.web.bind.annotation.PostMapping.class)
|
|
|
+ && !method.isAnnotationPresent(org.springframework.web.bind.annotation.PutMapping.class)
|
|
|
+ && !method.isAnnotationPresent(org.springframework.web.bind.annotation.PatchMapping.class)
|
|
|
+ && !method.isAnnotationPresent(org.springframework.web.bind.annotation.DeleteMapping.class));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ stubSuccessfulSections();
|
|
|
+ mockMvc.perform(post("/api/cockpit/v1/equipments/" + DEVICE_CODE + "/detail-aggregate"))
|
|
|
+ .andExpect(status().isMethodNotAllowed());
|
|
|
+ }
|
|
|
+
|
|
|
+ private void assertTrue(boolean condition) {
|
|
|
+ org.junit.jupiter.api.Assertions.assertTrue(condition);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void stubSuccessfulSections() {
|
|
|
+ stubArchive();
|
|
|
+ stubSuccessfulSectionsExceptArchive();
|
|
|
+ }
|
|
|
+
|
|
|
+ private void stubSuccessfulSectionsExceptArchive() {
|
|
|
+ when(metricsService.loadCurrent("detail-aggregate-001", DEVICE_CODE))
|
|
|
+ .thenReturn(metricsResponse("CURRENT", "READY"));
|
|
|
+ stubEventSection();
|
|
|
+ stubWorkOrderSections();
|
|
|
+ }
|
|
|
+
|
|
|
+ private void stubEmptySections() {
|
|
|
+ when(archiveService.load("detail-aggregate-001", DEVICE_CODE))
|
|
|
+ .thenReturn(new CockpitEquipmentArchiveResponse(
|
|
|
+ "detail-aggregate-001", "v1", true, new CockpitEquipmentArchive(null, "WGS84", null)));
|
|
|
+ when(metricsService.loadCurrent("detail-aggregate-001", DEVICE_CODE))
|
|
|
+ .thenReturn(metricsResponse("EMPTY", "NO_METRICS"));
|
|
|
+ when(eventService.loadCurrent("detail-aggregate-001", DEVICE_CODE))
|
|
|
+ .thenReturn(eventResponse(DEVICE_CODE, "EMPTY", "NO_EVENTS", Collections.emptyList(), Collections.emptyList(), Collections.emptyList()));
|
|
|
+ when(workOrderService.loadByDevice("detail-aggregate-001", DEVICE_CODE))
|
|
|
+ .thenReturn(workOrderResponse(DEVICE_CODE, "EMPTY", "NO_WORK_ORDERS", Collections.emptyList()));
|
|
|
+ }
|
|
|
+
|
|
|
+ private void stubSuccessfulSectionsExceptWorkOrders() {
|
|
|
+ stubArchive();
|
|
|
+ when(metricsService.loadCurrent("detail-aggregate-001", DEVICE_CODE))
|
|
|
+ .thenReturn(metricsResponse("CURRENT", "READY"));
|
|
|
+ stubEventSection();
|
|
|
+ }
|
|
|
+
|
|
|
+ private void stubArchive() {
|
|
|
+ CockpitEquipmentImage image = new CockpitEquipmentImage(
|
|
|
+ "https://cockpit.example/latest.jpg", "2026-08-30T10:20:00+08:00");
|
|
|
+ CockpitEquipmentArchiveSnapshot snapshot = new CockpitEquipmentArchiveSnapshot(
|
|
|
+ DEVICE_CODE, "污水环境监测设备", "WS-PRO-2000", "悬浮物/COD/氨氮/电导率/PH",
|
|
|
+ "沅陵专用仪表厂", "2025-12-01", "ws_sewage_environment", "污水环境监测设备",
|
|
|
+ new BigDecimal("128000"), 10, new BigDecimal("110.141"), new BigDecimal("28.452"),
|
|
|
+ "太常片区", "2026-01-10 09:30:00", "张维护", "13800000000", "沅陵县排水管理所", "演示设备",
|
|
|
+ Collections.singletonList(image));
|
|
|
+ when(archiveService.load("detail-aggregate-001", DEVICE_CODE)).thenReturn(new CockpitEquipmentArchiveResponse(
|
|
|
+ "detail-aggregate-001", "v1", true, new CockpitEquipmentArchive(snapshot, "WGS84", image)));
|
|
|
+ }
|
|
|
+
|
|
|
+ private void stubEventSection() {
|
|
|
+ when(eventService.loadCurrent("detail-aggregate-001", DEVICE_CODE)).thenReturn(eventResponse(
|
|
|
+ DEVICE_CODE, "READY", "READY",
|
|
|
+ Collections.singletonList(warning("WRN-20260902-001")),
|
|
|
+ Collections.singletonList(alarm("ALM-20260902-001")),
|
|
|
+ Collections.singletonList(new CockpitAlarmWarningRelation(
|
|
|
+ "REL-001", "WRN-20260902-001", "ALM-20260902-001", "2026-09-02T10:00:00+08:00"))));
|
|
|
+ }
|
|
|
+
|
|
|
+ private void stubWorkOrderSections() {
|
|
|
+ CockpitWorkOrderSummary summary = workOrderSummary();
|
|
|
+ when(workOrderService.loadByDevice("detail-aggregate-001", DEVICE_CODE)).thenReturn(workOrderResponse(
|
|
|
+ DEVICE_CODE, "READY", "READY", Collections.singletonList(summary)));
|
|
|
+ when(workOrderService.loadTimeline("detail-aggregate-001", "WO-2026090200001")).thenReturn(timelineResponse(summary));
|
|
|
+ }
|
|
|
+
|
|
|
+ private CockpitTypeMetricsResponse metricsResponse(String state, String stateReason) {
|
|
|
+ CockpitMetric metric = new CockpitMetric(
|
|
|
+ "COD", "COD", new BigDecimal("18.2"), "mg/L", "WARNING",
|
|
|
+ "2026-09-02T09:59:30+08:00", "CURRENT", Collections.emptyList());
|
|
|
+ return new CockpitTypeMetricsResponse(
|
|
|
+ "detail-aggregate-001", "v1", true, DEVICE_CODE, "ws_sewage_environment", "污水环境监测设备",
|
|
|
+ state, stateReason, Collections.singletonList(metric), null, null, "Asia/Shanghai", 1, 0, 1);
|
|
|
+ }
|
|
|
+
|
|
|
+ private CockpitEventReadResponse eventResponse(
|
|
|
+ String deviceCode, String state, String stateReason,
|
|
|
+ java.util.List<CockpitEventWarning> warnings,
|
|
|
+ java.util.List<CockpitEventAlarm> alarms,
|
|
|
+ java.util.List<CockpitAlarmWarningRelation> relations) {
|
|
|
+ return new CockpitEventReadResponse(
|
|
|
+ "detail-aggregate-001", "v1", true, deviceCode, "ws_sewage_environment", "污水环境监测设备",
|
|
|
+ state, stateReason, warnings, alarms, relations, null, null, "Asia/Shanghai", 1, 0, 2);
|
|
|
+ }
|
|
|
+
|
|
|
+ private CockpitWorkOrderReadResponse workOrderResponse(
|
|
|
+ String deviceCode, String state, String stateReason,
|
|
|
+ java.util.List<CockpitWorkOrderSummary> workOrders) {
|
|
|
+ return new CockpitWorkOrderReadResponse(
|
|
|
+ "detail-aggregate-001", "v1", true, deviceCode, null, null,
|
|
|
+ state, stateReason, CockpitWorkOrderStateDictionary.states(), workOrders);
|
|
|
+ }
|
|
|
+
|
|
|
+ private CockpitWorkOrderTimelineResponse timelineResponse(CockpitWorkOrderSummary summary) {
|
|
|
+ return new CockpitWorkOrderTimelineResponse(
|
|
|
+ "detail-aggregate-001", "v1", true, summary.getWorkOrderCode(),
|
|
|
+ CockpitWorkOrderStateDictionary.states(), summary,
|
|
|
+ Collections.singletonList(new CockpitWorkOrderTimelineEntry(
|
|
|
+ 1L, summary.getWorkOrderCode(), 4, "处理中", "创建工单", "处理异常", "系统",
|
|
|
+ "2026-09-02T09:00:00+08:00")));
|
|
|
+ }
|
|
|
+
|
|
|
+ private CockpitWorkOrderSummary workOrderSummary() {
|
|
|
+ return new CockpitWorkOrderSummary(
|
|
|
+ "WO-2026090200001", DEVICE_CODE, 1, "故障维修", 1, "处理异常", 4, "处理中",
|
|
|
+ "2026-09-02T09:00:00+08:00", "2026-09-02T10:00:00+08:00",
|
|
|
+ "2026-09-03T09:00:00+08:00", null,
|
|
|
+ new CockpitWorkOrderEventAssociation("ALARM", "ALM-20260902-001", "2026-09-02T10:00:00+08:00"));
|
|
|
+ }
|
|
|
+
|
|
|
+ private CockpitEventWarning warning(String warningNo) {
|
|
|
+ return new CockpitEventWarning(
|
|
|
+ warningNo, "污水COD预警", "WS_ENV", 1, "RED", "PROCESSING",
|
|
|
+ "2026-09-02T09:59:30+08:00", new BigDecimal("18.2"), "mg/L",
|
|
|
+ DEVICE_CODE, "污水环境监测设备", "太常片区");
|
|
|
+ }
|
|
|
+
|
|
|
+ private CockpitEventAlarm alarm(String alarmId) {
|
|
|
+ return new CockpitEventAlarm(
|
|
|
+ alarmId, "WS_ENV_HIGH", "污水COD报警", "WS_ENV", 1, "RED", "UNHANDLED",
|
|
|
+ "2026-09-02T10:00:00+08:00", new BigDecimal("42"), new BigDecimal("40"),
|
|
|
+ new BigDecimal("50"), "mg/L", DEVICE_CODE, "污水环境监测设备", "太常片区");
|
|
|
+ }
|
|
|
+}
|