|
|
@@ -0,0 +1,243 @@
|
|
|
+package com.zksy.web.controller.cockpit;
|
|
|
+
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
+import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
|
|
+import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
|
|
|
+import com.zksy.base.alarm.domain.AlarmData;
|
|
|
+import com.zksy.base.alarm.service.AlarmDataService;
|
|
|
+import com.zksy.base.domain.EquipmentBase;
|
|
|
+import com.zksy.base.mapper.EquipmentBaseMapper;
|
|
|
+import com.zksy.base.warning.mapper.AlarmWarningRelMapper;
|
|
|
+import com.zksy.base.warning.service.EarlyWarningService;
|
|
|
+import com.zksy.cockpit.workorder.BusinessCockpitWorkOrderReadAdapter;
|
|
|
+import com.zksy.cockpit.workorder.CockpitWorkOrderReadService;
|
|
|
+import com.zksy.ops.domain.WorkOrder;
|
|
|
+import com.zksy.ops.service.WorkOrderService;
|
|
|
+import org.apache.ibatis.builder.MapperBuilderAssistant;
|
|
|
+import org.apache.ibatis.exceptions.PersistenceException;
|
|
|
+import org.springframework.dao.DataAccessResourceFailureException;
|
|
|
+import org.apache.ibatis.session.Configuration;
|
|
|
+import org.junit.jupiter.api.BeforeEach;
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
+import org.mockito.ArgumentCaptor;
|
|
|
+import org.springframework.test.web.servlet.MockMvc;
|
|
|
+import org.springframework.test.web.servlet.ResultActions;
|
|
|
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
|
|
+
|
|
|
+import java.time.Clock;
|
|
|
+import java.time.Instant;
|
|
|
+import java.time.ZoneOffset;
|
|
|
+import java.util.Arrays;
|
|
|
+import java.util.Collections;
|
|
|
+import java.util.Date;
|
|
|
+import java.util.List;
|
|
|
+
|
|
|
+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.ArgumentMatchers.any;
|
|
|
+import static org.mockito.Mockito.mock;
|
|
|
+import static org.mockito.Mockito.never;
|
|
|
+import static org.mockito.Mockito.times;
|
|
|
+import static org.mockito.Mockito.verify;
|
|
|
+import static org.mockito.Mockito.when;
|
|
|
+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 入口,锁定执行票 21 的关联口径。 */
|
|
|
+class CockpitWorkOrderAssociationContractTest {
|
|
|
+ private static final String DEVICE_CODE = "21119740";
|
|
|
+ private static final String EQUIPMENT_ID = "equipment-id";
|
|
|
+ private static final String ORDER_CODE = "WO202610101511303516";
|
|
|
+ private static final String ALARM_ID = "cd57be42bd18d02f7d3390fb98539b93";
|
|
|
+ private static final String ALARM_NO = "BJ202610101511305660";
|
|
|
+ private static final String ASSOCIATED_AT = "2026-10-10T15:11:30+08:00";
|
|
|
+ private static final String BASE = "/api/cockpit/v1";
|
|
|
+
|
|
|
+ private WorkOrderService workOrderService;
|
|
|
+ private AlarmDataService alarmDataService;
|
|
|
+ private LambdaQueryChainWrapper<WorkOrder> orderQuery;
|
|
|
+ private MockMvc mockMvc;
|
|
|
+ private WorkOrder linkedOrder;
|
|
|
+ private AlarmData linkedAlarm;
|
|
|
+
|
|
|
+ @BeforeEach
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ void setUp() {
|
|
|
+ for (Class<?> entity : Arrays.asList(EquipmentBase.class, WorkOrder.class, AlarmData.class)) {
|
|
|
+ TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new Configuration(), ""), entity);
|
|
|
+ }
|
|
|
+ workOrderService = mock(WorkOrderService.class);
|
|
|
+ alarmDataService = mock(AlarmDataService.class);
|
|
|
+ EquipmentBaseMapper equipmentMapper = mock(EquipmentBaseMapper.class);
|
|
|
+ EquipmentBase equipment = new EquipmentBase();
|
|
|
+ equipment.setEquipmentId(EQUIPMENT_ID);
|
|
|
+ equipment.setEquipmentCode(DEVICE_CODE);
|
|
|
+ when(equipmentMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(equipment);
|
|
|
+ when(equipmentMapper.selectById(EQUIPMENT_ID)).thenReturn(equipment);
|
|
|
+
|
|
|
+ linkedOrder = order(279L, ORDER_CODE);
|
|
|
+ linkedAlarm = new AlarmData();
|
|
|
+ linkedAlarm.setId(ALARM_ID);
|
|
|
+ linkedAlarm.setAlarmNo(ALARM_NO);
|
|
|
+ linkedAlarm.setWorkOrderId(279L);
|
|
|
+ when(workOrderService.list(any(LambdaQueryWrapper.class)))
|
|
|
+ .thenReturn(Collections.singletonList(linkedOrder));
|
|
|
+ when(alarmDataService.list(any(LambdaQueryWrapper.class)))
|
|
|
+ .thenReturn(Collections.singletonList(linkedAlarm));
|
|
|
+ when(alarmDataService.getById(ALARM_ID)).thenReturn(linkedAlarm);
|
|
|
+ when(workOrderService.listLogs(279L)).thenReturn(Collections.emptyList());
|
|
|
+ orderQuery = mock(LambdaQueryChainWrapper.class);
|
|
|
+ when(workOrderService.lambdaQuery()).thenReturn(orderQuery);
|
|
|
+ when(orderQuery.eq(any(), any())).thenReturn(orderQuery);
|
|
|
+ when(orderQuery.one()).thenReturn(linkedOrder);
|
|
|
+
|
|
|
+ BusinessCockpitWorkOrderReadAdapter adapter = new BusinessCockpitWorkOrderReadAdapter(
|
|
|
+ workOrderService, mock(EarlyWarningService.class), mock(AlarmWarningRelMapper.class),
|
|
|
+ alarmDataService, equipmentMapper);
|
|
|
+ CockpitWorkOrderReadService service = new CockpitWorkOrderReadService(
|
|
|
+ adapter, Clock.fixed(Instant.parse("2026-10-10T07:11:30Z"), ZoneOffset.UTC));
|
|
|
+ mockMvc = MockMvcBuilders.standaloneSetup(new CockpitWorkOrderReadController(service)).build();
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ void sameOrderHasIdenticalAssociationAcrossDeviceAlarmAndTimeline() throws Exception {
|
|
|
+ expectLinked(mockMvc.perform(get(BASE + "/equipments/" + DEVICE_CODE + "/work-orders")),
|
|
|
+ "$.data.workOrders[0]")
|
|
|
+ .andExpect(jsonPath("$.data.unassociatedWorkOrderCount").value(0));
|
|
|
+ expectLinked(mockMvc.perform(get(BASE + "/events/ALARM/" + ALARM_ID + "/work-orders")),
|
|
|
+ "$.data.workOrders[0]")
|
|
|
+ .andExpect(jsonPath("$.data.eventCode").value(ALARM_ID))
|
|
|
+ .andExpect(jsonPath("$.data.unassociatedWorkOrderCount").value(0));
|
|
|
+ expectLinked(mockMvc.perform(get(BASE + "/work-orders/" + ORDER_CODE + "/timeline")),
|
|
|
+ "$.data.workOrder")
|
|
|
+ .andExpect(jsonPath("$.data.timelineEntries", hasSize(0)));
|
|
|
+
|
|
|
+ // 三个入口均反查真实工单主键,不能靠设备、工单号或时间猜测。
|
|
|
+ ArgumentCaptor<LambdaQueryWrapper<AlarmData>> queries = ArgumentCaptor.forClass(LambdaQueryWrapper.class);
|
|
|
+ verify(alarmDataService, times(3)).list(queries.capture());
|
|
|
+ for (LambdaQueryWrapper<AlarmData> query : queries.getAllValues()) {
|
|
|
+ assertTrue(query.getSqlSegment().contains("work_order_id IN"));
|
|
|
+ assertEquals(Collections.singletonList(279L),
|
|
|
+ new java.util.ArrayList<>(query.getParamNameValuePairs().values()));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void alarmNumberAliasReturnsTheSameAssociationAsDeviceAndTimeline() throws Exception {
|
|
|
+ expectLinked(mockMvc.perform(get(BASE + "/events/ALARM/" + ALARM_NO + "/work-orders")),
|
|
|
+ "$.data.workOrders[0]")
|
|
|
+ .andExpect(jsonPath("$.data.eventCode").value(ALARM_ID));
|
|
|
+ expectLinked(mockMvc.perform(get(BASE + "/equipments/" + DEVICE_CODE + "/work-orders")),
|
|
|
+ "$.data.workOrders[0]");
|
|
|
+ expectLinked(mockMvc.perform(get(BASE + "/work-orders/" + ORDER_CODE + "/timeline")),
|
|
|
+ "$.data.workOrder");
|
|
|
+ ArgumentCaptor<LambdaQueryWrapper<AlarmData>> queries = ArgumentCaptor.forClass(LambdaQueryWrapper.class);
|
|
|
+ verify(alarmDataService, times(4)).list(queries.capture());
|
|
|
+ LambdaQueryWrapper<AlarmData> aliasQuery = queries.getAllValues().get(0);
|
|
|
+ assertTrue(aliasQuery.getSqlSegment().contains("alarm_no ="));
|
|
|
+ assertEquals(Collections.singletonList(ALARM_NO),
|
|
|
+ new java.util.ArrayList<>(aliasQuery.getParamNameValuePairs().values()));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void unlinkedOrderIsExplicitlyFalseInDeviceAndTimelineAndAbsentFromAlarm() throws Exception {
|
|
|
+ when(alarmDataService.list(any(LambdaQueryWrapper.class))).thenReturn(Collections.emptyList());
|
|
|
+ linkedAlarm.setWorkOrderId(null);
|
|
|
+ expectUnlinked(mockMvc.perform(get(BASE + "/equipments/" + DEVICE_CODE + "/work-orders")),
|
|
|
+ "$.data.workOrders[0]")
|
|
|
+ .andExpect(jsonPath("$.data.unassociatedWorkOrderCount").value(1));
|
|
|
+ expectUnlinked(mockMvc.perform(get(BASE + "/work-orders/" + ORDER_CODE + "/timeline")),
|
|
|
+ "$.data.workOrder");
|
|
|
+ mockMvc.perform(get(BASE + "/events/ALARM/" + ALARM_ID + "/work-orders"))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.data.workOrders", hasSize(0)))
|
|
|
+ .andExpect(jsonPath("$.data.state").value("EMPTY"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void anotherOrderOnSameDeviceDoesNotInheritLinkedOrdersAlarm() throws Exception {
|
|
|
+ WorkOrder orphan = order(281L, "WO-UNLINKED");
|
|
|
+ List<WorkOrder> orders = Arrays.asList(linkedOrder, orphan);
|
|
|
+ when(workOrderService.list(any(LambdaQueryWrapper.class))).thenReturn(orders);
|
|
|
+ expectLinked(mockMvc.perform(get(BASE + "/equipments/" + DEVICE_CODE + "/work-orders")),
|
|
|
+ "$.data.workOrders[0]");
|
|
|
+ expectUnlinked(mockMvc.perform(get(BASE + "/equipments/" + DEVICE_CODE + "/work-orders")),
|
|
|
+ "$.data.workOrders[1]")
|
|
|
+ .andExpect(jsonPath("$.data.unassociatedWorkOrderCount").value(1));
|
|
|
+ when(orderQuery.one()).thenReturn(orphan);
|
|
|
+ expectUnlinked(mockMvc.perform(get(BASE + "/work-orders/WO-UNLINKED/timeline")),
|
|
|
+ "$.data.workOrder");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void missingOrderReturnsNotFoundBeforeReadingAssociations() throws Exception {
|
|
|
+ when(orderQuery.one()).thenReturn(null);
|
|
|
+ mockMvc.perform(get(BASE + "/work-orders/MISSING/timeline"))
|
|
|
+ .andExpect(status().isNotFound());
|
|
|
+ verify(alarmDataService, never()).list(any(LambdaQueryWrapper.class));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void duplicateAlarmReferencesFailAcrossAllScopesRegardlessOfOrder() throws Exception {
|
|
|
+ AlarmData otherAlarm = new AlarmData();
|
|
|
+ otherAlarm.setId("another-alarm");
|
|
|
+ otherAlarm.setWorkOrderId(279L);
|
|
|
+ when(alarmDataService.getById(otherAlarm.getId())).thenReturn(otherAlarm);
|
|
|
+ for (List<AlarmData> alarms : Arrays.asList(
|
|
|
+ Arrays.asList(linkedAlarm, otherAlarm), Arrays.asList(otherAlarm, linkedAlarm))) {
|
|
|
+ when(alarmDataService.list(any(LambdaQueryWrapper.class))).thenReturn(alarms);
|
|
|
+ expectAssociationFailure(mockMvc.perform(get(BASE + "/equipments/" + DEVICE_CODE + "/work-orders")));
|
|
|
+ expectAssociationFailure(mockMvc.perform(get(BASE + "/work-orders/" + ORDER_CODE + "/timeline")));
|
|
|
+ expectAssociationFailure(mockMvc.perform(get(BASE + "/events/ALARM/" + ALARM_ID + "/work-orders")));
|
|
|
+ expectAssociationFailure(mockMvc.perform(get(BASE + "/events/ALARM/" + otherAlarm.getId() + "/work-orders")));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void databaseAssociationReadFailureDoesNotBecomeSuccessfulUnassociatedOrder() throws Exception {
|
|
|
+ for (RuntimeException failure : Arrays.asList(
|
|
|
+ new DataAccessResourceFailureException("database unavailable"),
|
|
|
+ new PersistenceException("association query failed"))) {
|
|
|
+ org.mockito.Mockito.doThrow(failure).when(alarmDataService).list(any(LambdaQueryWrapper.class));
|
|
|
+ expectAssociationFailure(mockMvc.perform(get(BASE + "/equipments/" + DEVICE_CODE + "/work-orders")));
|
|
|
+ expectAssociationFailure(mockMvc.perform(get(BASE + "/work-orders/" + ORDER_CODE + "/timeline")));
|
|
|
+ expectAssociationFailure(mockMvc.perform(get(BASE + "/events/ALARM/" + ALARM_ID + "/work-orders")));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static ResultActions expectAssociationFailure(ResultActions result) throws Exception {
|
|
|
+ return result.andExpect(status().isInternalServerError())
|
|
|
+ .andExpect(jsonPath("$.data.category").value("SERVER_ERROR"))
|
|
|
+ .andExpect(jsonPath("$.data.workOrder").doesNotExist())
|
|
|
+ .andExpect(jsonPath("$.data.workOrders").doesNotExist());
|
|
|
+ }
|
|
|
+
|
|
|
+ private static ResultActions expectLinked(ResultActions result, String path) throws Exception {
|
|
|
+ return result.andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath(path + ".workOrderCode").value(ORDER_CODE))
|
|
|
+ .andExpect(jsonPath(path + ".associatedWithCurrentEvent").value(true))
|
|
|
+ .andExpect(jsonPath(path + ".eventAssociation.eventSource").value("ALARM"))
|
|
|
+ .andExpect(jsonPath(path + ".eventAssociation.eventCode").value(ALARM_ID))
|
|
|
+ .andExpect(jsonPath(path + ".eventAssociation.associatedAt").value(ASSOCIATED_AT));
|
|
|
+ }
|
|
|
+
|
|
|
+ private static ResultActions expectUnlinked(ResultActions result, String path) throws Exception {
|
|
|
+ return result.andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath(path + ".associatedWithCurrentEvent").value(false))
|
|
|
+ .andExpect(jsonPath(path + ".eventAssociation").value(nullValue()));
|
|
|
+ }
|
|
|
+
|
|
|
+ private static WorkOrder order(Long id, String code) {
|
|
|
+ WorkOrder order = new WorkOrder();
|
|
|
+ order.setOrderId(id);
|
|
|
+ order.setOrderNo(code);
|
|
|
+ order.setDeviceId(EQUIPMENT_ID);
|
|
|
+ order.setOrderStatus(5);
|
|
|
+ order.setCreateTime(Date.from(Instant.parse("2026-10-10T07:11:30Z")));
|
|
|
+ return order;
|
|
|
+ }
|
|
|
+}
|