|
|
@@ -0,0 +1,430 @@
|
|
|
+package com.zksy.web.controller.cockpit;
|
|
|
+
|
|
|
+import com.zksy.cockpit.event.CockpitMultidimensionalAnalysisAdapter;
|
|
|
+import com.zksy.cockpit.event.CockpitMultidimensionalAnalysisResponse;
|
|
|
+import com.zksy.cockpit.event.CockpitMultidimensionalAnalysisService;
|
|
|
+import com.zksy.cockpit.event.CockpitMultidimensionalAnalysisSnapshot;
|
|
|
+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.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.hamcrest.Matchers.nullValue;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
|
+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 CockpitMultidimensionalAnalysisControllerTest {
|
|
|
+
|
|
|
+ private static final String ENDPOINT = "/api/cockpit/v1/events/multidimensional-analysis";
|
|
|
+ private static final String START = "2026-09-01T00:00:00+08:00";
|
|
|
+ private static final String END = "2026-09-02T23:59:00+08:00";
|
|
|
+
|
|
|
+ private CockpitMultidimensionalAnalysisAdapter adapter;
|
|
|
+ private MockMvc mockMvc;
|
|
|
+
|
|
|
+ @BeforeEach
|
|
|
+ void setUp() {
|
|
|
+ adapter = mock(CockpitMultidimensionalAnalysisAdapter.class);
|
|
|
+ CockpitMultidimensionalAnalysisService service = new CockpitMultidimensionalAnalysisService(
|
|
|
+ adapter,
|
|
|
+ Clock.fixed(Instant.parse("2026-09-02T02:00:00Z"), ZoneOffset.UTC));
|
|
|
+ mockMvc = MockMvcBuilders
|
|
|
+ .standaloneSetup(new CockpitMultidimensionalAnalysisController(service))
|
|
|
+ .build();
|
|
|
+ when(adapter.load()).thenReturn(fixture());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void warningFamilyExposesSevenDimensionsWithCodesLabelsAndPercentages() throws Exception {
|
|
|
+ mockMvc.perform(get(ENDPOINT)
|
|
|
+ .param("eventFamily", "WARNING")
|
|
|
+ .param("startTime", START)
|
|
|
+ .param("endTime", END)
|
|
|
+ .header("X-Request-Id", "analysis-001"))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.code").value(200))
|
|
|
+ .andExpect(jsonPath("$.data.requestId").value("analysis-001"))
|
|
|
+ .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.totalCount").value(8))
|
|
|
+ .andExpect(jsonPath("$.data.dimensions", hasSize(7)))
|
|
|
+ .andExpect(jsonPath("$.data.dimensions[0].dimensionCode").value("EVENT_TYPE"))
|
|
|
+ .andExpect(jsonPath("$.data.dimensions[0].dimensionLabel").value("事件类型"))
|
|
|
+ .andExpect(jsonPath("$.data.dimensions[1].dimensionCode").value("CAUSE"))
|
|
|
+ .andExpect(jsonPath("$.data.dimensions[2].dimensionCode").value("INDUSTRY"))
|
|
|
+ .andExpect(jsonPath("$.data.dimensions[3].dimensionCode").value("DEVICE_TYPE"))
|
|
|
+ .andExpect(jsonPath("$.data.dimensions[4].dimensionCode").value("LEVEL"))
|
|
|
+ .andExpect(jsonPath("$.data.dimensions[5].dimensionCode").value("REGION"))
|
|
|
+ .andExpect(jsonPath("$.data.dimensions[6].dimensionCode").value("TIME"))
|
|
|
+ .andExpect(jsonPath("$.data.range.type").value("ANALYSIS_WINDOW"))
|
|
|
+ .andExpect(jsonPath("$.data.range.startTime").value(START))
|
|
|
+ .andExpect(jsonPath("$.data.range.endTime").value(END))
|
|
|
+ .andExpect(jsonPath("$.data.filters.eventFamily").value("WARNING"))
|
|
|
+ .andExpect(jsonPath("$.data.mapFilter.eventFamily").value("WARNING"))
|
|
|
+ .andExpect(jsonPath("$.data.mapFilter.deviceCodes", hasSize(8)));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void alarmFamilyIsIsolatedFromWarnings() 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.totalCount").value(2))
|
|
|
+ .andExpect(jsonPath("$.data.dimensions[2].buckets[0].bucketCode").value("GAS"))
|
|
|
+ .andExpect(jsonPath("$.data.dimensions[2].buckets[0].count").value(1))
|
|
|
+ .andExpect(jsonPath("$.data.dimensions[2].buckets[1].bucketCode").value("WATER_SUPPLY"))
|
|
|
+ .andExpect(jsonPath("$.data.dimensions[2].buckets[1].count").value(1))
|
|
|
+ .andExpect(jsonPath("$.data.dimensions[2].buckets[2].bucketCode").value("DRAINAGE"))
|
|
|
+ .andExpect(jsonPath("$.data.dimensions[2].buckets[2].count").value(0));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void percentagesMatchCountsOverTotal() throws Exception {
|
|
|
+ mockMvc.perform(get(ENDPOINT)
|
|
|
+ .param("eventFamily", "WARNING")
|
|
|
+ .param("startTime", START)
|
|
|
+ .param("endTime", END))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.data.totalCount").value(8))
|
|
|
+ .andExpect(jsonPath("$.data.dimensions[4].totalCount").value(8))
|
|
|
+ .andExpect(jsonPath("$.data.dimensions[4].buckets[0].bucketCode").value("1"))
|
|
|
+ .andExpect(jsonPath("$.data.dimensions[4].buckets[0].bucketLabel").value("RED"))
|
|
|
+ .andExpect(jsonPath("$.data.dimensions[4].buckets[0].count").value(2))
|
|
|
+ .andExpect(jsonPath("$.data.dimensions[4].buckets[0].percentage").value("25.00"))
|
|
|
+ .andExpect(jsonPath("$.data.dimensions[4].buckets[1].bucketLabel").value("YELLOW"))
|
|
|
+ .andExpect(jsonPath("$.data.dimensions[4].buckets[1].count").value(3))
|
|
|
+ .andExpect(jsonPath("$.data.dimensions[4].buckets[1].percentage").value("37.50"))
|
|
|
+ .andExpect(jsonPath("$.data.dimensions[4].buckets[2].bucketLabel").value("BLUE"))
|
|
|
+ .andExpect(jsonPath("$.data.dimensions[4].buckets[2].count").value(1))
|
|
|
+ .andExpect(jsonPath("$.data.dimensions[4].buckets[3].bucketLabel").value("GREEN"))
|
|
|
+ .andExpect(jsonPath("$.data.dimensions[4].buckets[3].count").value(1))
|
|
|
+ .andExpect(jsonPath("$.data.dimensions[4].buckets[4].bucketCode").value("UNKNOWN"))
|
|
|
+ .andExpect(jsonPath("$.data.dimensions[4].buckets[4].count").value(1));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void unknownCauseKeepsAnExplicitBucketWithARealCount() throws Exception {
|
|
|
+ mockMvc.perform(get(ENDPOINT)
|
|
|
+ .param("eventFamily", "WARNING")
|
|
|
+ .param("startTime", START)
|
|
|
+ .param("endTime", END))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.data.dimensions[1].dimensionCode").value("CAUSE"))
|
|
|
+ .andExpect(jsonPath("$.data.dimensions[1].buckets[?(@.bucketCode=='UNKNOWN_CAUSE')].bucketLabel")
|
|
|
+ .value("未知/缺失成因"))
|
|
|
+ .andExpect(jsonPath("$.data.dimensions[1].buckets[?(@.bucketCode=='UNKNOWN_CAUSE')].count")
|
|
|
+ .value(2));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void levelDictionaryIsFixedOrderRedYellowBlueGreenUnknown() throws Exception {
|
|
|
+ mockMvc.perform(get(ENDPOINT)
|
|
|
+ .param("eventFamily", "WARNING")
|
|
|
+ .param("startTime", START)
|
|
|
+ .param("endTime", END))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.data.dimensions[4].buckets", hasSize(5)))
|
|
|
+ .andExpect(jsonPath("$.data.dimensions[4].buckets[0].bucketCode").value("1"))
|
|
|
+ .andExpect(jsonPath("$.data.dimensions[4].buckets[0].bucketLabel").value("RED"))
|
|
|
+ .andExpect(jsonPath("$.data.dimensions[4].buckets[1].bucketCode").value("2"))
|
|
|
+ .andExpect(jsonPath("$.data.dimensions[4].buckets[1].bucketLabel").value("YELLOW"))
|
|
|
+ .andExpect(jsonPath("$.data.dimensions[4].buckets[2].bucketCode").value("3"))
|
|
|
+ .andExpect(jsonPath("$.data.dimensions[4].buckets[2].bucketLabel").value("BLUE"))
|
|
|
+ .andExpect(jsonPath("$.data.dimensions[4].buckets[3].bucketCode").value("4"))
|
|
|
+ .andExpect(jsonPath("$.data.dimensions[4].buckets[3].bucketLabel").value("GREEN"))
|
|
|
+ .andExpect(jsonPath("$.data.dimensions[4].buckets[4].bucketCode").value("UNKNOWN"))
|
|
|
+ .andExpect(jsonPath("$.data.dimensions[4].buckets[4].bucketLabel").value("UNKNOWN"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void industryAndLevelFiltersNarrowEveryDimensionAndMapFilter() throws Exception {
|
|
|
+ mockMvc.perform(get(ENDPOINT)
|
|
|
+ .param("eventFamily", "WARNING")
|
|
|
+ .param("industry", "GAS")
|
|
|
+ .param("level", "1")
|
|
|
+ .param("startTime", START)
|
|
|
+ .param("endTime", END))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.data.filters.industry").value("GAS"))
|
|
|
+ .andExpect(jsonPath("$.data.filters.level").value(1))
|
|
|
+ .andExpect(jsonPath("$.data.totalCount").value(2))
|
|
|
+ .andExpect(jsonPath("$.data.dimensions[0].totalCount").value(2))
|
|
|
+ .andExpect(jsonPath("$.data.dimensions[2].buckets[0].bucketCode").value("GAS"))
|
|
|
+ .andExpect(jsonPath("$.data.dimensions[2].buckets[0].count").value(2))
|
|
|
+ .andExpect(jsonPath("$.data.dimensions[2].buckets[1].count").value(0))
|
|
|
+ .andExpect(jsonPath("$.data.dimensions[2].buckets[2].count").value(0))
|
|
|
+ .andExpect(jsonPath("$.data.mapFilter.industry").value("GAS"))
|
|
|
+ .andExpect(jsonPath("$.data.mapFilter.level").value(1))
|
|
|
+ .andExpect(jsonPath("$.data.mapFilter.deviceCodes", hasSize(2)));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void causeRegionEventTypeAndDeviceTypeFiltersNarrowConsistently() throws Exception {
|
|
|
+ mockMvc.perform(get(ENDPOINT)
|
|
|
+ .param("eventFamily", "WARNING")
|
|
|
+ .param("cause", "浓度超限")
|
|
|
+ .param("startTime", START)
|
|
|
+ .param("endTime", END))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.data.totalCount").value(3))
|
|
|
+ .andExpect(jsonPath("$.data.filters.cause").value("浓度超限"));
|
|
|
+
|
|
|
+ mockMvc.perform(get(ENDPOINT)
|
|
|
+ .param("eventFamily", "WARNING")
|
|
|
+ .param("region", "城区B")
|
|
|
+ .param("startTime", START)
|
|
|
+ .param("endTime", END))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.data.totalCount").value(2))
|
|
|
+ .andExpect(jsonPath("$.data.filters.region").value("城区B"));
|
|
|
+
|
|
|
+ mockMvc.perform(get(ENDPOINT)
|
|
|
+ .param("eventFamily", "WARNING")
|
|
|
+ .param("eventType", "GAS_CONCENTRATION")
|
|
|
+ .param("startTime", START)
|
|
|
+ .param("endTime", END))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.data.totalCount").value(3))
|
|
|
+ .andExpect(jsonPath("$.data.filters.eventType").value("GAS_CONCENTRATION"));
|
|
|
+
|
|
|
+ mockMvc.perform(get(ENDPOINT)
|
|
|
+ .param("eventFamily", "WARNING")
|
|
|
+ .param("deviceType", "WATER_SUPPLY_SENSOR")
|
|
|
+ .param("startTime", START)
|
|
|
+ .param("endTime", END))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.data.totalCount").value(3))
|
|
|
+ .andExpect(jsonPath("$.data.filters.deviceType").value("WATER_SUPPLY_SENSOR"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void emptyWindowKeepsEveryDimensionWithZeroPercentages() throws Exception {
|
|
|
+ mockMvc.perform(get(ENDPOINT)
|
|
|
+ .param("eventFamily", "WARNING")
|
|
|
+ .param("startTime", "2026-09-10T00:00:00+08:00")
|
|
|
+ .param("endTime", "2026-09-11T00:00:00+08:00"))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.data.totalCount").value(0))
|
|
|
+ .andExpect(jsonPath("$.data.dimensions", hasSize(7)))
|
|
|
+ .andExpect(jsonPath("$.data.dimensions[4].buckets[0].count").value(0))
|
|
|
+ .andExpect(jsonPath("$.data.dimensions[4].buckets[0].percentage").value("0.00"))
|
|
|
+ .andExpect(jsonPath("$.data.dimensions[1].buckets[0].percentage").value("0.00"))
|
|
|
+ .andExpect(jsonPath("$.data.dimensions[2].buckets[0].percentage").value("0.00"))
|
|
|
+ .andExpect(jsonPath("$.data.mapFilter.deviceCodes", hasSize(0)));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void timeDimensionUsesAsiaShanghaiDayBoundaries() throws Exception {
|
|
|
+ mockMvc.perform(get(ENDPOINT)
|
|
|
+ .param("eventFamily", "WARNING")
|
|
|
+ .param("startTime", START)
|
|
|
+ .param("endTime", END))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.data.dimensions[6].buckets", hasSize(2)))
|
|
|
+ .andExpect(jsonPath("$.data.dimensions[6].buckets[0].bucketCode")
|
|
|
+ .value("2026-09-01T00:00:00+08:00"))
|
|
|
+ .andExpect(jsonPath("$.data.dimensions[6].buckets[0].count").value(3))
|
|
|
+ .andExpect(jsonPath("$.data.dimensions[6].buckets[1].bucketCode")
|
|
|
+ .value("2026-09-02T00:00:00+08:00"))
|
|
|
+ .andExpect(jsonPath("$.data.dimensions[6].buckets[1].count").value(5));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void noAccuracyOrTruthfulnessFieldsExist() throws Exception {
|
|
|
+ mockMvc.perform(get(ENDPOINT).param("eventFamily", "WARNING"))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.data.accuracy").doesNotExist())
|
|
|
+ .andExpect(jsonPath("$.data.truthfulness").doesNotExist())
|
|
|
+ .andExpect(jsonPath("$.data.falsePositiveRate").doesNotExist())
|
|
|
+ .andExpect(jsonPath("$.data.dimensions[?(@.dimensionCode=='ACCURACY')]").isEmpty());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void invalidFamilyIndustryLevelOrWindowReturnsTypedBadRequestBeforeAdapter() 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("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("startTime", "2026-08-01T00:00:00+08:00")
|
|
|
+ .param("endTime", "2026-09-02T00:00:00+08:00"))
|
|
|
+ .andExpect(status().isBadRequest())
|
|
|
+ .andExpect(jsonPath("$.data.message").value("analysis window must not exceed 31 days"));
|
|
|
+
|
|
|
+ verifyNoInteractions(adapter);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void defaultWindowCoversSevenDays() 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.dimensions[6].buckets", hasSize(8)));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void serverFailureReturnsTypedServerError() throws Exception {
|
|
|
+ when(adapter.load()).thenThrow(new CockpitReadException("upstream unavailable"));
|
|
|
+
|
|
|
+ mockMvc.perform(get(ENDPOINT)
|
|
|
+ .param("eventFamily", "WARNING")
|
|
|
+ .header("X-Request-Id", "analysis-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("analysis-500"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void mutatingRequestIsRejectedBeforeReadAdapterIsCalled() throws Exception {
|
|
|
+ mockMvc.perform(post(ENDPOINT))
|
|
|
+ .andExpect(status().isMethodNotAllowed());
|
|
|
+
|
|
|
+ verifyNoInteractions(adapter);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void controllerOnlyExposesAuthenticatedReadOnlyOperations() {
|
|
|
+ assertTrue(Arrays.stream(CockpitMultidimensionalAnalysisController.class.getDeclaredMethods())
|
|
|
+ .filter(this::hasRequestMappingAnnotation)
|
|
|
+ .allMatch(CockpitMultidimensionalAnalysisControllerTest::isReadOnlyMappedMethod));
|
|
|
+
|
|
|
+ assertTrue(Arrays.stream(CockpitMultidimensionalAnalysisController.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 CockpitMultidimensionalAnalysisSnapshot fixture() {
|
|
|
+ return new CockpitMultidimensionalAnalysisSnapshot(
|
|
|
+ "2026-09-02T23:59:50+08:00",
|
|
|
+ Arrays.asList(
|
|
|
+ type("GAS_SENSOR", "燃气浓度计", "GAS"),
|
|
|
+ type("WATER_SUPPLY_SENSOR", "供水压力计", "WATER_SUPPLY"),
|
|
|
+ type("SEWAGE_ENV", "污水环境监测", "DRAINAGE"),
|
|
|
+ new CockpitMultidimensionalAnalysisSnapshot.SourceType(
|
|
|
+ "WATER_PLANT", "水厂", "WATER_SUPPLY", "FACILITY"),
|
|
|
+ new CockpitMultidimensionalAnalysisSnapshot.SourceType(
|
|
|
+ "SMART_MANHOLE", "智能井盖", null, "SMART_DEVICE")),
|
|
|
+ Arrays.asList(
|
|
|
+ warning("MW-001", "GAS_CONCENTRATION", "浓度超限", 1,
|
|
|
+ "2026-09-01T09:00:00+08:00", "城区A", "GAS_SENSOR", "D-GAS-1"),
|
|
|
+ warning("MW-002", "GAS_CONCENTRATION", "浓度超限", 1,
|
|
|
+ "2026-09-01T10:00:00+08:00", "城区A", "GAS_SENSOR", "D-GAS-2"),
|
|
|
+ warning("MW-003", "WATER_PRESSURE", null, 2,
|
|
|
+ "2026-09-01T11:00:00+08:00", "城区B", "WATER_SUPPLY_SENSOR", "D-WS-1"),
|
|
|
+ warning("MW-004", "WATER_PRESSURE", "压力波动", 2,
|
|
|
+ "2026-09-02T09:00:00+08:00", "城区B", "WATER_SUPPLY_SENSOR", "D-WS-2"),
|
|
|
+ warning("MW-005", "SEWAGE_CONCENTRATION", "浓度超限", 2,
|
|
|
+ "2026-09-02T10:00:00+08:00", "城区C", "SEWAGE_ENV", "D-DR-1"),
|
|
|
+ warning("MW-006", "SEWAGE_CONCENTRATION", "设备故障", 3,
|
|
|
+ "2026-09-02T11:00:00+08:00", "城区C", "SEWAGE_ENV", "D-DR-2"),
|
|
|
+ warning("MW-007", "WATER_PRESSURE", null, 4,
|
|
|
+ "2026-09-02T12:00:00+08:00", null, "WATER_SUPPLY_SENSOR", "D-WS-3"),
|
|
|
+ warning("MW-008", "GAS_CONCENTRATION", "设备故障", 9,
|
|
|
+ "2026-09-02T13:00:00+08:00", "城区A", "GAS_SENSOR", "D-GAS-3"),
|
|
|
+ warning("MW-LEGACY", "LEGACY_TYPE", "old", 1,
|
|
|
+ "2026-09-02T13:30:00+08:00", "城区A", "LEGACY", "D-LEGACY"),
|
|
|
+ warning("MW-MISMATCH", "FACILITY_TYPE", "x", 1,
|
|
|
+ "2026-09-02T13:40:00+08:00", "城区A", "WATER_PLANT", "D-PLANT"),
|
|
|
+ warning("MW-MH", "MANHOLE_TYPE", "x", 1,
|
|
|
+ "2026-09-02T13:50:00+08:00", "城区A", "SMART_MANHOLE", "D-MH")),
|
|
|
+ Arrays.asList(
|
|
|
+ alarm("MA-001", "GAS_CONCENTRATION", "浓度超限", 1,
|
|
|
+ "2026-09-02T14:00:00+08:00", "城区A", "GAS_SENSOR", "D-GAS-9"),
|
|
|
+ alarm("MA-002", "WATER_PRESSURE", null, 2,
|
|
|
+ "2026-09-02T15:00:00+08:00", "城区B", "WATER_SUPPLY_SENSOR", "D-WS-9")));
|
|
|
+ }
|
|
|
+
|
|
|
+ private static CockpitMultidimensionalAnalysisSnapshot.SourceType type(
|
|
|
+ String code, String name, String industry) {
|
|
|
+ return new CockpitMultidimensionalAnalysisSnapshot.SourceType(
|
|
|
+ code, name, industry, "SMART_DEVICE");
|
|
|
+ }
|
|
|
+
|
|
|
+ private static CockpitMultidimensionalAnalysisSnapshot.SourceWarning warning(
|
|
|
+ String no, String eventType, String cause, Integer level, String eventTime,
|
|
|
+ String region, String typeCode, String deviceCode) {
|
|
|
+ return new CockpitMultidimensionalAnalysisSnapshot.SourceWarning(
|
|
|
+ no, eventType, cause, level, eventTime, region, typeCode, deviceCode);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static CockpitMultidimensionalAnalysisSnapshot.SourceAlarm alarm(
|
|
|
+ String id, String eventType, String cause, Integer level, String eventTime,
|
|
|
+ String region, String typeCode, String deviceCode) {
|
|
|
+ return new CockpitMultidimensionalAnalysisSnapshot.SourceAlarm(
|
|
|
+ id, eventType, cause, level, eventTime, region, typeCode, deviceCode);
|
|
|
+ }
|
|
|
+}
|