|
|
@@ -0,0 +1,210 @@
|
|
|
+package com.zksy.web.controller.cockpit;
|
|
|
+
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
+import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
|
|
+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.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.setup.MockMvcBuilders;
|
|
|
+
|
|
|
+import java.time.Clock;
|
|
|
+import java.time.Instant;
|
|
|
+import java.time.ZoneOffset;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.Arrays;
|
|
|
+import java.util.Collections;
|
|
|
+import java.util.Date;
|
|
|
+
|
|
|
+import static org.junit.jupiter.api.Assertions.assertFalse;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
|
+import static org.mockito.ArgumentMatchers.any;
|
|
|
+import static org.mockito.ArgumentMatchers.anyInt;
|
|
|
+import static org.mockito.ArgumentMatchers.eq;
|
|
|
+import static org.mockito.ArgumentMatchers.isNull;
|
|
|
+import static org.mockito.Mockito.mock;
|
|
|
+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;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 全量工单列表端点(票 65):报警效率面板的「当前工单列表」。
|
|
|
+ *
|
|
|
+ * <p>锁四件事,都是**会真的画错界面**的那几件:
|
|
|
+ * <ol>
|
|
|
+ * <li>不按设备/事件筛——全量口径(查询条件里不得出现 device_id);</li>
|
|
|
+ * <li>总数取独立 count,不由本页条数反推(票 61 纪律);</li>
|
|
|
+ * <li>非法 page/pageSize/stateCode 被夹到合法区间并**回显实际生效值**;</li>
|
|
|
+ * <li>状态字典与每张工单的关联标记照常给出(卡片要按同一份词表上色)。</li>
|
|
|
+ * </ol>
|
|
|
+ */
|
|
|
+class CockpitWorkOrderListContractTest {
|
|
|
+ private static final String BASE = "/api/cockpit/v1";
|
|
|
+ private static final String ORDER_CODE = "WO202610101511303516";
|
|
|
+ private static final String DEVICE_CODE = "21119740";
|
|
|
+ private static final String ALARM_ID = "cd57be18d02f7d3390fb98539b93";
|
|
|
+ private static final String CREATE_TIME = "2026-10-10T15:11:30+08:00";
|
|
|
+
|
|
|
+ private WorkOrderService workOrderService;
|
|
|
+ private AlarmDataService alarmDataService;
|
|
|
+ private MockMvc mockMvc;
|
|
|
+
|
|
|
+ @BeforeEach
|
|
|
+ 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.selectById("equipment-id")).thenReturn(equipment);
|
|
|
+
|
|
|
+ WorkOrder order = new WorkOrder();
|
|
|
+ order.setOrderId(279L);
|
|
|
+ order.setOrderNo(ORDER_CODE);
|
|
|
+ order.setDeviceId("equipment-id");
|
|
|
+ order.setOrderLevel(2);
|
|
|
+ order.setOrderDesc("监测报警,请现场核实处理");
|
|
|
+ order.setOrderStatus(0);
|
|
|
+ order.setCreateTime(Date.from(Instant.parse("2026-10-10T07:11:30Z")));
|
|
|
+ order.setUpdateTime(Date.from(Instant.parse("2026-10-10T07:11:30Z")));
|
|
|
+
|
|
|
+ when(workOrderService.count(any(LambdaQueryWrapper.class))).thenReturn(7L);
|
|
|
+ when(workOrderService.list(any(LambdaQueryWrapper.class)))
|
|
|
+ .thenReturn(Collections.singletonList(order));
|
|
|
+ AlarmData alarm = new AlarmData();
|
|
|
+ alarm.setId(ALARM_ID);
|
|
|
+ alarm.setWorkOrderId(279L);
|
|
|
+ when(alarmDataService.list(any(LambdaQueryWrapper.class)))
|
|
|
+ .thenReturn(Collections.singletonList(alarm));
|
|
|
+
|
|
|
+ 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
|
|
|
+ void listIsGlobalNotScopedToDeviceOrEvent() throws Exception {
|
|
|
+ mockMvc.perform(get(BASE + "/work-orders"))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.data.state").value("READY"))
|
|
|
+ .andExpect(jsonPath("$.data.workOrders[0].workOrderCode").value(ORDER_CODE))
|
|
|
+ // 总数是 count 的结果(7),不是本页 1 条
|
|
|
+ .andExpect(jsonPath("$.data.pagination.total").value(7))
|
|
|
+ .andExpect(jsonPath("$.data.pagination.page").value(1))
|
|
|
+ .andExpect(jsonPath("$.data.pagination.pageSize").value(10))
|
|
|
+ .andExpect(jsonPath("$.data.pagination.totalPages").value(1))
|
|
|
+ // 状态字典必须给(卡片按同一词表上色)
|
|
|
+ .andExpect(jsonPath("$.data.stateDictionary[0].code").value(0));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void pageAndSizeAreClampedAndEchoedBack() throws Exception {
|
|
|
+ mockMvc.perform(get(BASE + "/work-orders").param("page", "0").param("pageSize", "9999"))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.data.pagination.page").value(1))
|
|
|
+ .andExpect(jsonPath("$.data.pagination.pageSize").value(100));
|
|
|
+
|
|
|
+ // 夹取后才下到适配器:offset 必须由生效值算出,不能让非法值进 SQL
|
|
|
+ verify(workOrderService, org.mockito.Mockito.atLeastOnce())
|
|
|
+ .list(any(LambdaQueryWrapper.class));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void invalidStateCodeFallsBackToNoFilter() throws Exception {
|
|
|
+ mockMvc.perform(get(BASE + "/work-orders").param("stateCode", "99"))
|
|
|
+ .andExpect(status().isOk());
|
|
|
+ // 越界状态码当成"不筛",仍然返回数据(而不是空列表假读数)
|
|
|
+ verify(workOrderService, org.mockito.Mockito.atLeastOnce()).count(any(LambdaQueryWrapper.class));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void validStateCodeIsApplied() throws Exception {
|
|
|
+ mockMvc.perform(get(BASE + "/work-orders").param("stateCode", "3"))
|
|
|
+ .andExpect(status().isOk());
|
|
|
+ verify(workOrderService).count(any(LambdaQueryWrapper.class));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 时间窗口必须真的下到 SQL(维护者 2026-10-22 反馈「工单列表没按时间筛」)。
|
|
|
+ *
|
|
|
+ * <p>这里抓的是 `count(wrapper)` 收到的条件:必须同时含 `create_time >=` 与
|
|
|
+ * `create_time <=`,并且两端的值就是请求传进来的那两个时刻 —— 只断言「请求 200」
|
|
|
+ * 抓不到「参数收下了但没用」这种缺陷。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ void timeWindowIsAppliedToCreateTime() throws Exception {
|
|
|
+ String start = "2026-09-21T00:00:00+08:00";
|
|
|
+ String end = "2026-10-21T23:59:59+08:00";
|
|
|
+ mockMvc.perform(get(BASE + "/work-orders").param("startTime", start).param("endTime", end))
|
|
|
+ .andExpect(status().isOk());
|
|
|
+
|
|
|
+ ArgumentCaptor<LambdaQueryWrapper<WorkOrder>> queries = ArgumentCaptor.forClass(LambdaQueryWrapper.class);
|
|
|
+ verify(workOrderService, org.mockito.Mockito.atLeastOnce()).count(queries.capture());
|
|
|
+ LambdaQueryWrapper<WorkOrder> query = queries.getValue();
|
|
|
+ String sql = query.getSqlSegment();
|
|
|
+ assertTrue(sql.contains("createTime"), "时间窗必须落在 createTime(create_time 列)上,实际:" + sql);
|
|
|
+ assertTrue(sql.contains(">="), "缺少下界 createTime >=,实际:" + sql);
|
|
|
+ assertTrue(sql.contains("<="), "缺少上界 createTime <=,实际:" + sql);
|
|
|
+ assertEquals(2, query.getParamNameValuePairs().size(),
|
|
|
+ "下界与上界都必须带上,实际:" + query.getParamNameValuePairs());
|
|
|
+ // 参数值就是请求里那两个时刻(+08:00 换算成 UTC 后对应这两个 Instant)
|
|
|
+ java.util.List<Object> values = new ArrayList<>(query.getParamNameValuePairs().values());
|
|
|
+ assertTrue(values.contains(java.util.Date.from(java.time.OffsetDateTime.parse(start).toInstant())),
|
|
|
+ "下界值与请求不一致:" + values);
|
|
|
+ assertTrue(values.contains(java.util.Date.from(java.time.OffsetDateTime.parse(end).toInstant())),
|
|
|
+ "上界值与请求不一致:" + values);
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 不带时间参数时绝不加时间条件(缺省=不限,既有机能不受影响)。 */
|
|
|
+ @Test
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ void absentWindowAddsNoTimeCondition() throws Exception {
|
|
|
+ mockMvc.perform(get(BASE + "/work-orders")).andExpect(status().isOk());
|
|
|
+ ArgumentCaptor<LambdaQueryWrapper<WorkOrder>> queries = ArgumentCaptor.forClass(LambdaQueryWrapper.class);
|
|
|
+ verify(workOrderService, org.mockito.Mockito.atLeastOnce()).count(queries.capture());
|
|
|
+ String sql = queries.getValue().getSqlSegment();
|
|
|
+ // 注意: 本身也含 ,所以不能按字段名判 ——
|
|
|
+ // 要看有没有**比较条件**(>= / <=)。
|
|
|
+ assertFalse(sql.contains(">=") || sql.contains("<="),
|
|
|
+ "没传时间参数却加了时间比较条件:" + sql);
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 时间格式不合法 → 400(不静默忽略,否则「以为筛了其实是全量」最难发现)。 */
|
|
|
+ @Test
|
|
|
+ void invalidWindowIsRejected() throws Exception {
|
|
|
+ mockMvc.perform(get(BASE + "/work-orders").param("startTime", "2026-09-21"))
|
|
|
+ .andExpect(status().isBadRequest());
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 起点晚于终点 → 400(空窗口是入参错误,不是「没有工单」)。 */
|
|
|
+ @Test
|
|
|
+ void reversedWindowIsRejected() throws Exception {
|
|
|
+ mockMvc.perform(get(BASE + "/work-orders")
|
|
|
+ .param("startTime", "2026-10-21T00:00:00+08:00")
|
|
|
+ .param("endTime", "2026-09-21T00:00:00+08:00"))
|
|
|
+ .andExpect(status().isBadRequest());
|
|
|
+ }
|
|
|
+}
|