Explorar el Código

Merge remote-tracking branch 'origin/master'

zengy hace 1 semana
padre
commit
6b446af2f2

+ 121 - 0
pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/CockpitMonitoringPointStatusOrderTest.java

@@ -0,0 +1,121 @@
+package com.zksy.web.controller.cockpit;
+
+import com.zksy.cockpit.point.CockpitMonitoringPointDeviceSnapshot;
+import com.zksy.cockpit.point.CockpitMonitoringPointStatus;
+import com.zksy.cockpit.point.CockpitMonitoringPointStatusResolver;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * 票 71 契约:单设备业务处置序 =「未转工单的报警 > 工单处置 > 预警」(维护者 2026-10-22 裁定 Q1=A)。
+ *
+ * <p>覆盖本次新增的二路(无报警时「有未结束工单」压过「有未解决预警」)、票 52 的窄三路
+ * (全部未处理报警被未结束工单承接 → WORK_ORDER),并钉住两条同日裁定:
+ * Q2(枚举 priority 与点位聚合语义不变)、Q3({@code monitoring/situation} 不跟随,另行在适配器注释留档)。
+ *
+ * <p>注:这些是判定逻辑的纯测试;真实后端复验(改序后 map-snapshot 读数)需后端部署后再做。
+ */
+class CockpitMonitoringPointStatusOrderTest {
+
+    @Test
+    void openWorkOrderOutranksOpenWarningWhenNoAlarm() {
+        assertEquals(CockpitMonitoringPointStatus.WORK_ORDER,
+                CockpitMonitoringPointStatusResolver.deviceStatus(device(true, false, true, true)),
+                "无报警:未结束工单压过未解决预警(本次新增)");
+    }
+
+    @Test
+    void openWarningStillWinsWhenNoOpenWorkOrder() {
+        assertEquals(CockpitMonitoringPointStatus.WARNING,
+                CockpitMonitoringPointStatusResolver.deviceStatus(device(true, false, true, false)),
+                "只剩预警时仍判预警,不拿已结案/已取消工单倒推");
+    }
+
+    @Test
+    void openWorkOrderWithoutWarningStaysWorkOrder() {
+        assertEquals(CockpitMonitoringPointStatus.WORK_ORDER,
+                CockpitMonitoringPointStatusResolver.deviceStatus(device(true, false, false, true)));
+    }
+
+    @Test
+    void alarmsCoveredByOpenWorkOrdersOutrankWarning() {
+        assertEquals(CockpitMonitoringPointStatus.WORK_ORDER,
+                CockpitMonitoringPointStatusResolver.deviceStatus(alarmDevice(true, true, true)),
+                "票 52 窄三路保留:报警全被未结束工单承接 + 预警仍在 → 工单处置");
+    }
+
+    @Test
+    void uncoveredAlarmOutranksWorkOrderAndWarning() {
+        assertEquals(CockpitMonitoringPointStatus.ALARM,
+                CockpitMonitoringPointStatusResolver.deviceStatus(alarmDevice(true, true, false)),
+                "存在未转工单的报警时报警最高(未结束工单/预警都不能压过它)");
+        assertEquals(CockpitMonitoringPointStatus.ALARM,
+                CockpitMonitoringPointStatusResolver.deviceStatus(alarmDevice(false, true, false)),
+                "报警标志为真但无工单承接(含「查不到未处理报警行」)→ 报警,不能渲染成处置中");
+    }
+
+    @Test
+    void normalAndOfflineKeepCurrentOrder() {
+        assertEquals(CockpitMonitoringPointStatus.NORMAL,
+                CockpitMonitoringPointStatusResolver.deviceStatus(device(true, false, false, false)));
+        assertEquals(CockpitMonitoringPointStatus.OFFLINE,
+                CockpitMonitoringPointStatusResolver.deviceStatus(device(false, false, false, false)));
+    }
+
+    /** Q2:枚举优先级一个字都没动(前端两张词表 / 图例排序 / t09 闸门因此零改动)。 */
+    @Test
+    void enumPriorityIsUnchanged() {
+        assertEquals(1, CockpitMonitoringPointStatus.ALARM.getPriority());
+        assertEquals(2, CockpitMonitoringPointStatus.WARNING.getPriority());
+        assertEquals(3, CockpitMonitoringPointStatus.WORK_ORDER.getPriority());
+        assertEquals(4, CockpitMonitoringPointStatus.NORMAL.getPriority());
+        assertEquals(5, CockpitMonitoringPointStatus.OFFLINE.getPriority());
+        assertEquals(6, CockpitMonitoringPointStatus.EMPTY.getPriority());
+    }
+
+    /** Q2:点位聚合仍取 min(priority),不被单设备业务序倒灌。 */
+    @Test
+    void pointAggregationStillUsesEnumPriority() {
+        CockpitMonitoringPointDeviceSnapshot warningOnly = device("D-W", true, false, true, false);
+        CockpitMonitoringPointDeviceSnapshot warningWithOpenWorkOrder = device("D-WO", true, false, true, true);
+
+        assertEquals(CockpitMonitoringPointStatus.WARNING,
+                CockpitMonitoringPointStatusResolver.resolve(
+                        Arrays.asList(warningOnly, warningWithOpenWorkOrder)),
+                "两台设备时聚合取 WARNING(2) < WORK_ORDER(3)");
+    }
+
+    /** 单设备点位的自动跟随:改序的直接后果,固定下来防回退。 */
+    @Test
+    void singleDevicePointFollowsDeviceOrder() {
+        assertEquals(CockpitMonitoringPointStatus.WORK_ORDER,
+                CockpitMonitoringPointStatusResolver.resolve(Collections.singletonList(
+                        device("D-WO", true, false, true, true))));
+    }
+
+    private static CockpitMonitoringPointDeviceSnapshot device(
+            boolean online, boolean alarm, boolean warning, boolean workOrderInProgress) {
+        return device("DEVICE-1", online, alarm, warning, workOrderInProgress);
+    }
+
+    private static CockpitMonitoringPointDeviceSnapshot device(String code, boolean online, boolean alarm,
+            boolean warning, boolean workOrderInProgress) {
+        return new CockpitMonitoringPointDeviceSnapshot(
+                code, code, "压力计",
+                online, alarm, warning, workOrderInProgress,
+                Collections.emptyList(), null);
+    }
+
+    /** 报警设备:报警标志为真,另带预警/工单与「全部未处理报警均被未结束工单承接」事实。 */
+    private static CockpitMonitoringPointDeviceSnapshot alarmDevice(
+            boolean warning, boolean workOrderInProgress, boolean alarmsCoveredByOpenWorkOrders) {
+        return new CockpitMonitoringPointDeviceSnapshot(
+                "DEVICE-1", "DEVICE-1", "压力计", null, null, null,
+                true, true, warning, workOrderInProgress,
+                Collections.emptyList(), null, true, alarmsCoveredByOpenWorkOrders);
+    }
+}

+ 5 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/monitoring/BusinessCockpitMonitoringSituationAdapter.java

@@ -177,6 +177,11 @@ public class BusinessCockpitMonitoringSituationAdapter implements CockpitMonitor
     /**
      * 指标态:缺状态记录 → UNKNOWN(不冒充 OFFLINE);离线 → OFFLINE;报警 → ALARM;预警中 → WARNING;其余 → NORMAL。
      * 现网无样本新鲜度依据,故不产生 EXPIRED。
+     *
+     * <p><b>刻意不跟随单设备业务序</b>(票 71 / 维护者 2026-10-22 裁定 Q3=A):本方法按设备事实判,
+     * 预警中即 WARNING、不因「有未结束工单」变成 WORK_ORDER,且没有 {@code WORK_ORDER} 桶。
+     * 所以同一台「预警未解决 + 工单未结束」的设备会出现「一张图/设备卡 = 工单处置、
+     * 运行监测异常清单 = 预警」——那是裁定结果,不是缺陷;「异常点」可见范围也因此不变。
      */
     private String metricStatus(String deviceCode, CockpitMonitoringPointDeviceSnapshot fact) {
         EquipmentStatus status = null;

+ 17 - 9
pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/point/CockpitMonitoringPointStatusResolver.java

@@ -62,14 +62,21 @@ public final class CockpitMonitoringPointStatusResolver {
     }
 
     /**
-     * 单设备业务状态(维护者 2026-10-17 裁定)。
+     * 单设备业务状态(维护者 2026-10-22 裁定:未转工单的报警 &gt; 工单处置 &gt; 预警)。
      *
-     * <p>报警**且**该设备全部未处理报警都已由未结束工单承接时 → {@code WORK_ORDER}
-     * (处置在进行,优先于报警/预警);否则维持现行序
-     * {@code alarm → warning → workOrderInProgress → online ? NORMAL : OFFLINE}。
+     * <p>判定序(**业务处置序**,不是枚举优先级序):
+     * <ol>
+     *   <li>报警标志为真:该设备全部未处理报警都已由未结束工单承接 → {@code WORK_ORDER}
+     *       (票 52 / 2026-10-17 裁定,保留);否则 {@code ALARM} —— 「查不到承接」不等于「已承接」;</li>
+     *   <li>否则存在未结束工单 → {@code WORK_ORDER}(票 71 / 2026-10-22 新增:压在预警之前);</li>
+     *   <li>否则存在未解决预警 → {@code WARNING};</li>
+     *   <li>否则 {@code online ? NORMAL : OFFLINE}。</li>
+     * </ol>
      *
-     * <p>该裁定只作用于**单设备**判定;点位聚合 {@code resolve()} 仍取
-     * {@code min(priority)},故枚举优先级与前端词表不变。
+     * <p>边界(2026-10-22 同日裁定):枚举 {@code priority} 与点位聚合 {@code min(priority)}
+     * <b>不变</b>(Q2);{@code monitoring/situation} 的 {@code metricStatus()} 按设备事实判、
+     * <b>不跟随</b>本序(Q3)—— 同一设备可能「地图 = 工单处置、运行监测异常清单 = 预警」,
+     * 那是裁定结果,不是缺陷。
      */
     public static CockpitMonitoringPointStatus deviceStatus(CockpitMonitoringPointDeviceSnapshot device) {
         if (device.isAlarm()) {
@@ -77,12 +84,13 @@ public final class CockpitMonitoringPointStatusResolver {
                     ? CockpitMonitoringPointStatus.WORK_ORDER
                     : CockpitMonitoringPointStatus.ALARM;
         }
-        if (device.isWarning()) {
-            return CockpitMonitoringPointStatus.WARNING;
-        }
+        // 「未转工单的报警」已在上面拦截;剩下的「有未结束工单」压过「有未解决预警」。
         if (device.isWorkOrderInProgress()) {
             return CockpitMonitoringPointStatus.WORK_ORDER;
         }
+        if (device.isWarning()) {
+            return CockpitMonitoringPointStatus.WARNING;
+        }
         return device.isOnline() ? CockpitMonitoringPointStatus.NORMAL : CockpitMonitoringPointStatus.OFFLINE;
     }
 

+ 0 - 395
pipe-network-service/zksy-system/src/test/java/com/zksy/base/service/impl/WaterSupplyDeviceServiceImplTest.java

@@ -1,395 +0,0 @@
-package com.zksy.base.service.impl;
-
-import com.baomidou.mybatisplus.core.MybatisConfiguration;
-import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
-import com.zksy.base.alarm.domain.AlarmData;
-import com.zksy.base.alarm.mapper.AlarmDataMapper;
-import com.zksy.base.domain.EquipmentBase;
-import com.zksy.base.domain.EquipmentMaintain;
-import com.zksy.base.domain.EquipmentStatus;
-import com.zksy.base.domain.EquipmentType;
-import com.zksy.ops.domain.WorkOrder;
-import com.zksy.base.domain.vo.AbnormalDeviceVO;
-import com.zksy.base.domain.vo.DeviceDetailVO;
-import com.zksy.base.domain.vo.DeviceGisVO;
-import com.zksy.base.domain.vo.DeviceTypeStatisticsVO;
-import com.zksy.base.mapper.EquipmentMaintainMapper;
-import com.zksy.base.mapper.EquipmentStatusMapper;
-import com.zksy.base.mapper.EquipmentTypeMapper;
-import com.zksy.ops.mapper.WorkOrderMapper;
-import com.zksy.base.service.EquipmentBaseService;
-import com.zksy.common.core.domain.entity.SysUser;
-import com.zksy.system.service.ISysUserService;
-import org.junit.jupiter.api.BeforeAll;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.apache.ibatis.builder.MapperBuilderAssistant;
-import org.mockito.Mockito;
-import org.springframework.test.util.ReflectionTestUtils;
-
-import java.math.BigDecimal;
-import java.time.LocalDateTime;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.stream.Collectors;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNull;
-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.when;
-
-class WaterSupplyDeviceServiceImplTest {
-
-    @BeforeAll
-    static void initializeMybatisPlusLambdaCache() {
-        MapperBuilderAssistant assistant = new MapperBuilderAssistant(new MybatisConfiguration(), "");
-        TableInfoHelper.initTableInfo(assistant, EquipmentBase.class);
-        TableInfoHelper.initTableInfo(assistant, EquipmentStatus.class);
-        TableInfoHelper.initTableInfo(assistant, AlarmData.class);
-        TableInfoHelper.initTableInfo(assistant, WorkOrder.class);
-    }
-
-    private WaterSupplyDeviceServiceImpl service;
-    private EquipmentBaseService equipmentBaseService;
-    private EquipmentTypeMapper equipmentTypeMapper;
-    private EquipmentStatusMapper equipmentStatusMapper;
-    private EquipmentMaintainMapper equipmentMaintainMapper;
-    private AlarmDataMapper alarmDataMapper;
-    private WorkOrderMapper workOrderMapper;
-    private ISysUserService sysUserService;
-
-    private EquipmentBase pressureDevice;
-    private EquipmentBase repairDevice;
-    private EquipmentBase gasDevice;
-
-    @BeforeEach
-    void setUp() {
-        service = Mockito.spy(new WaterSupplyDeviceServiceImpl());
-        equipmentBaseService = mock(EquipmentBaseService.class);
-        equipmentTypeMapper = mock(EquipmentTypeMapper.class);
-        equipmentStatusMapper = mock(EquipmentStatusMapper.class);
-        equipmentMaintainMapper = mock(EquipmentMaintainMapper.class);
-        alarmDataMapper = mock(AlarmDataMapper.class);
-        workOrderMapper = mock(WorkOrderMapper.class);
-        sysUserService = mock(ISysUserService.class);
-
-        ReflectionTestUtils.setField(service, "equipmentBaseService", equipmentBaseService);
-        ReflectionTestUtils.setField(service, "equipmentTypeMapper", equipmentTypeMapper);
-        ReflectionTestUtils.setField(service, "equipmentStatusMapper", equipmentStatusMapper);
-        ReflectionTestUtils.setField(service, "equipmentMaintainMapper", equipmentMaintainMapper);
-        ReflectionTestUtils.setField(service, "alarmDataMapper", alarmDataMapper);
-        ReflectionTestUtils.setField(service, "workOrderMapper", workOrderMapper);
-        ReflectionTestUtils.setField(service, "sysUserService", sysUserService);
-
-        pressureDevice = device("water-pressure-1", "WS-PRESSURE-001", "供水压力监测点", "12");
-        repairDevice = device("water-flow-1", "WS-FLOW-001", "供水流量监测点", "11");
-        gasDevice = device("gas-1", "GAS-001", "燃气监测点", "3");
-
-        when(equipmentBaseService.findByTopLevelType("供水")).thenReturn(Arrays.asList(
-                pressureDevice, repairDevice));
-        when(equipmentTypeMapper.selectList(any())).thenReturn(Arrays.asList(
-                type(1, "供水", 0),
-                type(12, "供水设备甲", 1),
-                type(11, "供水设备乙", 1),
-                type(3, "其他设备", 0)));
-        when(equipmentTypeMapper.selectBatchIds(any())).thenReturn(Arrays.asList(
-                type(12, "供水设备甲", 1),
-                type(11, "供水设备乙", 1),
-                type(3, "其他设备", 0)));
-        when(equipmentTypeMapper.selectById(any())).thenReturn(null);
-        when(equipmentStatusMapper.selectList(any())).thenReturn(Collections.emptyList());
-        when(equipmentStatusMapper.selectOne(any())).thenReturn(null);
-        when(alarmDataMapper.selectList(any())).thenReturn(Collections.emptyList());
-        when(workOrderMapper.selectList(any())).thenReturn(Collections.emptyList());
-        when(equipmentMaintainMapper.selectList(any())).thenReturn(Collections.emptyList());
-        when(equipmentMaintainMapper.selectOne(any())).thenReturn(null);
-        when(sysUserService.selectUserById(any(Long.class))).thenReturn(null);
-    }
-
-    @Test
-    void typeStatisticsCountOnlyTheWaterTypeSubtree() {
-        EquipmentBase secondPressure = device("water-pressure-2", "WS-PRESSURE-002",
-                "第二供水压力监测点", "12");
-        when(equipmentBaseService.findByTopLevelType("供水")).thenReturn(Arrays.asList(
-                pressureDevice, secondPressure, repairDevice));
-        List<DeviceTypeStatisticsVO> roots = service.getDeviceTypeStatistics();
-
-        assertEquals(1, roots.size());
-        assertEquals(Integer.valueOf(1), roots.get(0).getTypeId());
-        assertEquals(3, roots.get(0).getDeviceCount());
-        List<Integer> childTypeIds = roots.get(0).getChildren().stream()
-                .map(DeviceTypeStatisticsVO::getTypeId)
-                .sorted()
-                .collect(Collectors.toList());
-        assertEquals(Arrays.asList(11, 12), childTypeIds);
-    }
-
-    @Test
-    void typeStatisticsReturnsEmptyWhenNoWaterDevices() {
-        when(equipmentBaseService.findByTopLevelType("供水")).thenReturn(Collections.emptyList());
-        assertTrue(service.getDeviceTypeStatistics().isEmpty());
-    }
-
-    @Test
-    void gisPointsIncludeWaterOnlyWithCoordinatesAndStatuses() {
-        pressureDevice.setLongitude(new BigDecimal("113.28000000"));
-        pressureDevice.setLatitude(new BigDecimal("23.12000000"));
-        gasDevice.setLongitude(new BigDecimal("113.32000000"));
-        gasDevice.setLatitude(new BigDecimal("23.16000000"));
-        when(equipmentStatusMapper.selectList(any())).thenReturn(Collections.singletonList(
-                status(pressureDevice.getEquipmentId(), 1, 1, 1)));
-        when(equipmentTypeMapper.selectBatchIds(any())).thenReturn(Collections.singletonList(
-                type(12, "压力监测", 1)));
-
-        List<DeviceGisVO> points = service.getAllDeviceGisPoints(null);
-
-        assertEquals(Collections.singletonList("water-pressure-1"), points.stream()
-                .map(DeviceGisVO::getEquipmentId)
-                .collect(Collectors.toList()));
-        assertEquals("压力监测", points.get(0).getEquipmentTypeName());
-        assertEquals(1, points.get(0).getOnlineStatus());
-        assertEquals(1, points.get(0).getAlarmStatus());
-        assertEquals(1, points.get(0).getCurrentStatus());
-        assertEquals(new BigDecimal("113.28000000"), points.get(0).getLongitude());
-    }
-
-    @Test
-    void gisPointsExcludesDevicesWithoutCoordinates() {
-        pressureDevice.setLongitude(new BigDecimal("113.28000000"));
-        pressureDevice.setLatitude(new BigDecimal("23.12000000"));
-        // repairDevice 无坐标,应被过滤
-
-        List<DeviceGisVO> points = service.getAllDeviceGisPoints(null);
-
-        assertEquals(Collections.singletonList("water-pressure-1"), points.stream()
-                .map(DeviceGisVO::getEquipmentId)
-                .collect(Collectors.toList()));
-    }
-
-    @Test
-    void gisPointsFilterByEquipmentTypeId() {
-        pressureDevice.setLongitude(new BigDecimal("113.28000000"));
-        pressureDevice.setLatitude(new BigDecimal("23.12000000"));
-        repairDevice.setLongitude(new BigDecimal("113.29000000"));
-        repairDevice.setLatitude(new BigDecimal("23.13000000"));
-
-        List<DeviceGisVO> points = service.getAllDeviceGisPoints("12");
-
-        assertEquals(Collections.singletonList("water-pressure-1"), points.stream()
-                .map(DeviceGisVO::getEquipmentId)
-                .collect(Collectors.toList()));
-    }
-
-    @Test
-    void deviceDetailIncludesAlarmAndRelatedWorkOrderSummary() {
-        when(equipmentStatusMapper.selectOne(any())).thenReturn(status(pressureDevice.getEquipmentId(), 1, 1, 1));
-        when(alarmDataMapper.selectList(any())).thenReturn(Collections.singletonList(
-                alarm(pressureDevice.getEquipmentCode(), "供水压力超限", "1"))));
-        when(workOrderMapper.selectList(any())).thenReturn(Collections.singletonList(
-                workOrder(pressureDevice.getEquipmentId(), "WO-DETAIL", 2, 1L)));
-        when(sysUserService.selectUserById(1L)).thenReturn(sysUser(1L, "zhangsan", "张三"));
-
-        DeviceDetailVO detail = service.getDeviceDetail(pressureDevice.getEquipmentId());
-
-        assertEquals(1, detail.getAlertLevel());
-        assertEquals("设备报警", detail.getAbnormalReason());
-        assertEquals("WO-DETAIL", detail.getRelatedWorkOrderNo());
-        assertEquals(2, detail.getRelatedWorkOrderStatus());
-        assertEquals("张三", detail.getRelatedWorkOrderPerson());
-    }
-
-    @Test
-    void deviceDetailKeepsMaintenanceStateSeparateFromAlarmLevel() {
-        when(equipmentStatusMapper.selectOne(any())).thenReturn(
-                status(repairDevice.getEquipmentId(), 3, 0, 1));
-        when(alarmDataMapper.selectList(any())).thenReturn(Collections.emptyList());
-        when(workOrderMapper.selectList(any())).thenReturn(Collections.emptyList());
-
-        DeviceDetailVO detail = service.getDeviceDetail(repairDevice.getEquipmentId());
-
-        assertEquals(3, detail.getCurrentStatus());
-        assertEquals(0, detail.getAlarmStatus());
-        assertNull(detail.getAlertLevel());
-        assertEquals("设备维修中", detail.getAbnormalReason());
-    }
-
-    @Test
-    void deviceDetailIgnoresProcessedAlarmRecordWhenEquipmentIsNormal() {
-        when(equipmentStatusMapper.selectOne(any())).thenReturn(
-                status(repairDevice.getEquipmentId(), 1, 0, 1));
-        AlarmData processedAlarm = new AlarmData();
-        processedAlarm.setDeviceCode(repairDevice.getEquipmentCode());
-        processedAlarm.setAlarmStatus(0);
-        processedAlarm.setAlarmLevel("1");
-        when(alarmDataMapper.selectList(any())).thenReturn(Collections.singletonList(processedAlarm));
-        when(workOrderMapper.selectList(any())).thenReturn(Collections.emptyList());
-
-        DeviceDetailVO detail = service.getDeviceDetail(repairDevice.getEquipmentId());
-
-        assertEquals(0, detail.getAlarmStatus());
-        assertNull(detail.getAlertLevel());
-        assertNull(detail.getAbnormalReason());
-    }
-
-    @Test
-    void deviceDetailDoesNotExposeNonWaterDevices() {
-        // gasDevice 不属于供水设备,findWaterDeviceById 找不到,应返回 null
-        assertNull(service.getDeviceDetail(gasDevice.getEquipmentId()));
-    }
-
-    @Test
-    void deviceDetailIncludesLatestMaintainRecord() {
-        when(equipmentStatusMapper.selectOne(any())).thenReturn(
-                status(pressureDevice.getEquipmentId(), 1, 0, 1));
-        when(alarmDataMapper.selectList(any())).thenReturn(Collections.emptyList());
-        when(workOrderMapper.selectList(any())).thenReturn(Collections.emptyList());
-        EquipmentMaintain maintain = maintain("m1", pressureDevice.getEquipmentId());
-        when(equipmentMaintainMapper.selectOne(any())).thenReturn(maintain);
-
-        DeviceDetailVO detail = service.getDeviceDetail(pressureDevice.getEquipmentId());
-
-        assertTrue(detail.getLatestMaintain() != null);
-        assertEquals("m1", detail.getLatestMaintain().get("maintainId"));
-    }
-
-    @Test
-    void abnormalGisPointsReturnsWaterDevicesWithAlarmOrMaintenanceStatus() {
-        pressureDevice.setLongitude(new BigDecimal("113.28000000"));
-        pressureDevice.setLatitude(new BigDecimal("23.12000000"));
-        repairDevice.setLongitude(new BigDecimal("113.29000000"));
-        repairDevice.setLatitude(new BigDecimal("23.13000000"));
-        when(equipmentStatusMapper.selectList(any())).thenReturn(Arrays.asList(
-                status(pressureDevice.getEquipmentId(), 1, 1, 1),
-                status(repairDevice.getEquipmentId(), 3, 0, 1)));
-        when(alarmDataMapper.selectList(any())).thenReturn(Collections.singletonList(
-                alarm(pressureDevice.getEquipmentCode(), "供水压力超限", "1"))));
-        when(workOrderMapper.selectList(any())).thenReturn(Collections.singletonList(
-                workOrder(pressureDevice.getEquipmentId(), "WO-A", 2, 1L)));
-        when(equipmentMaintainMapper.selectList(any())).thenReturn(Collections.singletonList(
-                maintain("m1", pressureDevice.getEquipmentId())));
-        when(sysUserService.selectUserById(1L)).thenReturn(sysUser(1L, "zhangsan", "张三"));
-
-        List<AbnormalDeviceVO> result = service.getAbnormalDeviceGisPoints(null);
-
-        assertEquals(2, result.size());
-        AbnormalDeviceVO pressureRow = result.stream()
-                .filter(r -> "water-pressure-1".equals(r.getEquipmentId()))
-                .findFirst().orElseThrow();
-        assertEquals(1, pressureRow.getAlarmStatus());
-        assertEquals(1, pressureRow.getCurrentStatus());
-        assertEquals(1, pressureRow.getOnlineStatus());
-        assertEquals(1, pressureRow.getAlertLevel());
-        assertEquals("设备报警", pressureRow.getAbnormalReason());
-        assertEquals("WO-A", pressureRow.getRelatedWorkOrderNo());
-        assertEquals(2, pressureRow.getRelatedWorkOrderStatus());
-        assertEquals("张三", pressureRow.getRelatedWorkOrderPerson());
-        assertEquals("m1", pressureRow.getLatestMaintainId());
-
-        AbnormalDeviceVO repairRow = result.stream()
-                .filter(r -> "water-flow-1".equals(r.getEquipmentId()))
-                .findFirst().orElseThrow();
-        assertEquals(0, repairRow.getAlarmStatus());
-        assertEquals(3, repairRow.getCurrentStatus());
-        assertNull(repairRow.getAlertLevel());
-        assertEquals("设备维修中", repairRow.getAbnormalReason());
-        assertNull(repairRow.getLatestMaintainId());
-    }
-
-    @Test
-    void abnormalGisPointsExcludesNonWaterAndMissingCoordinates() {
-        // gasDevice 有坐标但非供水设备;repairDevice 供水但无坐标;仅 pressureDevice 符合
-        gasDevice.setLongitude(new BigDecimal("113.32000000"));
-        gasDevice.setLatitude(new BigDecimal("23.16000000"));
-        pressureDevice.setLongitude(new BigDecimal("113.28000000"));
-        pressureDevice.setLatitude(new BigDecimal("23.12000000"));
-        when(equipmentStatusMapper.selectList(any())).thenReturn(Arrays.asList(
-                status(pressureDevice.getEquipmentId(), 1, 1, 1),
-                status(gasDevice.getEquipmentId(), 1, 1, 1),
-                status(repairDevice.getEquipmentId(), 3, 0, 1)));
-
-        List<AbnormalDeviceVO> result = service.getAbnormalDeviceGisPoints(null);
-
-        assertEquals(Collections.singletonList("water-pressure-1"), result.stream()
-                .map(AbnormalDeviceVO::getEquipmentId)
-                .collect(Collectors.toList()));
-    }
-
-    /** 设备类型ID 现为整型;非数字按 null 处理。 */
-    private static Integer parseTypeId(String value) {
-        if (value == null) {
-            return null;
-        }
-        try {
-            return Integer.valueOf(value);
-        } catch (NumberFormatException e) {
-            return null;
-        }
-    }
-
-    private static EquipmentBase device(String id, String code, String name, String typeId) {
-        EquipmentBase device = new EquipmentBase();
-        device.setEquipmentId(id);
-        device.setEquipmentCode(code);
-        device.setEquipmentName(name);
-        device.setEquipmentTypeId(parseTypeId(typeId));
-        device.setEquipmentLocation("测试位置");
-        return device;
-    }
-
-    private static EquipmentType type(Integer id, String name, Integer parentId) {
-        EquipmentType type = new EquipmentType();
-        type.setId(id);
-        type.setTypeName(name);
-        type.setParentId(parentId);
-        return type;
-    }
-
-    private static EquipmentStatus status(String equipmentId, int currentStatus,
-                                          int alarmStatus, int onlineStatus) {
-        EquipmentStatus status = new EquipmentStatus();
-        status.setEquipmentId(equipmentId);
-        status.setCurrentStatus(currentStatus);
-        status.setAlarmStatus(alarmStatus);
-        status.setOnlineStatus(onlineStatus);
-        status.setStatusUpdateTime(LocalDateTime.of(2026, 9, 2, 10, 0));
-        return status;
-    }
-
-    private static AlarmData alarm(String deviceCode, String warningType, String level) {
-        AlarmData alarm = new AlarmData();
-        alarm.setDeviceCode(deviceCode);
-        alarm.setAlarmLevel(level);
-        alarm.setAlarmStatus(0);
-        return alarm;
-    }
-
-    private static WorkOrder workOrder(String deviceId, String orderNo, int orderStatus, Long receiveUser) {
-        WorkOrder order = new WorkOrder();
-        order.setDeviceId(deviceId);
-        order.setOrderNo(orderNo);
-        order.setOrderStatus(orderStatus);
-        order.setReceiveUser(receiveUser);
-        return order;
-    }
-
-    private static SysUser sysUser(Long userId, String userName, String nickName) {
-        SysUser user = new SysUser();
-        user.setUserId(userId);
-        user.setUserName(userName);
-        user.setNickName(nickName);
-        return user;
-    }
-
-    private static EquipmentMaintain maintain(String maintainId, String equipmentId) {
-        EquipmentMaintain record = new EquipmentMaintain();
-        record.setMaintainId(maintainId);
-        record.setEquipmentId(equipmentId);
-        record.setMaintainType("故障维修");
-        record.setMaintainPerson("张三");
-        return record;
-    }
-
-}