Ver Fonte

feat(cockpit): 补齐点位关联设备坐标校验

Kazerin há 3 semanas atrás
pai
commit
8006fb43d7

+ 14 - 0
docs/cockpit/README.md

@@ -85,6 +85,20 @@
 - 适配器未给出 `coordinateVerified` 时按**未验证**处理(`state=UNVERIFIED`、`reason=UNVERIFIED_COORDINATE`、`renderable=false`)——字典要求「未验证禁止渲染」,服务端不臆造验证状态。报警族在数据可得时另带 `thresholdMinValue`/`thresholdMaxValue`(预警族为 `null`)。
 - `events/history-analysis` 的 `rows[]` 是**小时桶聚合行**,不存在逐事件行,因此不携带 `coordinateValidation` 与阈值字段(逐事件坐标只在 `events/current` 提供)。
 
+### 监测点详情设备坐标
+
+`GET /api/cockpit/v1/monitoring-points/{monitoringPointCode}` 的 `data.devices[]` 保留点位下的全部关联设备(包括离线设备),以 `deviceCode` 作为唯一关联键,并返回设备自身坐标与状态:
+
+| 字段 | 语义 |
+| --- | --- |
+| `longitude` / `latitude` | 来自 `equipment_base` 的设备坐标;不会复制所属监测点坐标,也不会人工偏移。缺失时字段为 `null`。 |
+| `coordinateValidation.state` | `VALID`、`INVALID` 或 `UNVERIFIED`。 |
+| `coordinateValidation.reason` | 不可上图时的非空原因,例如 `MISSING_LONGITUDE`、`LONGITUDE_OUT_OF_RANGE`、`ZERO_COORDINATE` 或 `UNVERIFIED_COORDINATE`。 |
+| `coordinateValidation.renderable` | 只有可信校核来源存在且坐标有效时为 `true`;前端仅在此为 `true` 时绘制设备点位。 |
+| `coordinateValidation.crs` | 当前统一为 `WGS84`。 |
+
+当前 `equipment_base` 没有可信坐标校核字段,因此真实适配器透传设备坐标但按 `UNVERIFIED_COORDINATE` 降级,`renderable=false`;设备仍保留在详情清单中。点位详情与 `map-snapshot` 都从同一设备关联关系和设备编码集合构造,均不因离线状态或坐标不可渲染而静默删除设备。
+
 ## 5. WebSocket 实时增量
 
 端点:`/ws/deviceStatus`。

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

@@ -17,6 +17,7 @@ import com.zksy.ops.service.WorkOrderService;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
+import java.math.BigDecimal;
 import java.time.LocalDateTime;
 import java.util.Collections;
 import java.util.List;
@@ -66,6 +67,11 @@ class BusinessCockpitDeviceFactResolverTest {
         assertTrue(orderQuery.getValue().getSqlSegment().contains("device_id"), "工单必须按设备主键关联");
         assertTrue(orderQuery.getValue().getParamNameValuePairs().containsValue("ID-DEV-1"));
         assertEquals("压力计", fact.getDeviceTypeName());
+        assertEquals(new BigDecimal("110.4025"), fact.getLongitude());
+        assertEquals(new BigDecimal("28.4500"), fact.getLatitude());
+        assertEquals("UNVERIFIED", fact.getCoordinateValidation().getState());
+        assertEquals("UNVERIFIED_COORDINATE", fact.getCoordinateValidation().getReason());
+        assertFalse(fact.getCoordinateValidation().isRenderable());
         assertEquals(CockpitMonitoringPointStatus.ALARM,
                 CockpitMonitoringPointStatusResolver.deviceStatus(fact));
     }
@@ -129,6 +135,8 @@ class BusinessCockpitDeviceFactResolverTest {
         device.setEquipmentId("ID-" + code);
         device.setEquipmentCode(code);
         device.setEquipmentName(code);
+        device.setLongitude(new BigDecimal("110.4025"));
+        device.setLatitude(new BigDecimal("28.4500"));
         return device;
     }
 

+ 16 - 2
pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/BusinessCockpitMapSnapshotAdapterTest.java

@@ -68,7 +68,8 @@ class BusinessCockpitMapSnapshotAdapterTest {
         EquipmentBase bound = device("D-1", "Z-TEST-RQXL01", "Z-测试燃气泄漏01", "22",
                 new BigDecimal("110.4024"), new BigDecimal("28.4499"));
         stubDevices(Collections.singletonList(bound), Collections.singletonList(bound));
-        when(deviceFactResolver.resolve(any(), anyString())).thenReturn(fact("Z-TEST-RQXL01"));
+        when(deviceFactResolver.resolve(any(), anyString())).thenReturn(
+                fact("Z-TEST-RQXL01", new BigDecimal("110.4025"), new BigDecimal("28.4500"), false));
 
         CockpitMapSnapshot snapshot = adapter.load(null);
 
@@ -78,6 +79,9 @@ class BusinessCockpitMapSnapshotAdapterTest {
         assertEquals("22", point.getTypeCode(), "关联设备类型唯一时点位类型可推导");
         assertEquals(1, point.getDevices().size());
         assertEquals("Z-TEST-RQXL01", point.getDevices().get(0).getDeviceCode());
+        assertEquals(new BigDecimal("110.4025"), point.getDevices().get(0).getLongitude());
+        assertEquals(new BigDecimal("28.4500"), point.getDevices().get(0).getLatitude());
+        assertFalse(point.getDevices().get(0).getCoordinateValidation().isRenderable());
 
         assertTrue(snapshot.getTypes().stream().anyMatch(
                 type -> "22".equals(type.getCode()) && "GAS".equals(type.getIndustry())),
@@ -102,6 +106,10 @@ class BusinessCockpitMapSnapshotAdapterTest {
 
         CockpitMapSnapshot snapshot = adapter.load(null);
         assertEquals(2, snapshot.getSmartDevices().size(), "适配器上报全量设备");
+        assertEquals(1, snapshot.getMonitoringPoints().get(0).getDevices().size());
+        assertEquals("BOUND-1", snapshot.getMonitoringPoints().get(0).getDevices().get(0).getDeviceCode());
+        assertFalse(snapshot.getMonitoringPoints().get(0).getDevices().get(0).isOnline(),
+                "离线设备仍属于点位关联集合");
 
         CockpitMapSnapshotService service = new CockpitMapSnapshotService(
                 adapter, Clock.fixed(Instant.parse("2026-09-02T02:00:00Z"), ZoneOffset.UTC));
@@ -157,8 +165,14 @@ class BusinessCockpitMapSnapshotAdapterTest {
     // ---- fixtures ----
 
     private static CockpitMonitoringPointDeviceSnapshot fact(String code) {
+        return fact(code, null, null, false);
+    }
+
+    private static CockpitMonitoringPointDeviceSnapshot fact(
+            String code, BigDecimal longitude, BigDecimal latitude, boolean online) {
         return new CockpitMonitoringPointDeviceSnapshot(
-                code, code, "类型名", true, false, false, false, Collections.emptyList(), null);
+                code, code, "类型名", longitude, latitude, false,
+                online, false, false, false, Collections.emptyList(), null);
     }
 
     private static MonitorPoint point(String id, String code, String name, BigDecimal lon, BigDecimal lat) {

+ 14 - 1
pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/BusinessCockpitMonitoringPointDetailAdapterTest.java

@@ -47,13 +47,15 @@ class BusinessCockpitMonitoringPointDetailAdapterTest {
     void loadsPointAndResolvesBoundDeviceTypeAndRuntimeFacts() {
         MonitorPoint point = point("P-1", "MP-001", "供水干管监测点", "110.4024", "28.4499");
         EquipmentBase device = device("D-1", "DEV-001", "压力计-001", 11);
+        device.setLongitude(new BigDecimal("110.4025"));
+        device.setLatitude(new BigDecimal("28.4500"));
         EquipmentType type = type(11, "超声波流量计");
         when(monitorPointService.list()).thenReturn(Collections.singletonList(point));
         when(equipmentTypeService.list()).thenReturn(Collections.singletonList(type));
         when(equipmentBaseService.getEquipmentByPoint("P-1"))
                 .thenReturn(Collections.singletonList(device));
         when(deviceFactResolver.resolve(device, "超声波流量计"))
-                .thenReturn(fact("DEV-001"));
+                .thenReturn(fact("DEV-001", new BigDecimal("110.4025"), new BigDecimal("28.4500")));
 
         CockpitMonitoringPointDetail detail = adapter.load(" MP-001 ");
 
@@ -62,6 +64,11 @@ class BusinessCockpitMonitoringPointDetailAdapterTest {
         assertEquals(new BigDecimal("110.4024"), detail.getLongitude());
         assertEquals(new BigDecimal("28.4499"), detail.getLatitude());
         assertFalse(detail.isCoordinateVerified(), "现网没有可信坐标校核字段");
+        assertEquals(new BigDecimal("110.4025"), detail.getDevices().get(0).getLongitude());
+        assertEquals(new BigDecimal("28.4500"), detail.getDevices().get(0).getLatitude());
+        assertEquals("UNVERIFIED", detail.getDevices().get(0).getCoordinateValidation().getState());
+        assertEquals("UNVERIFIED_COORDINATE", detail.getDevices().get(0).getCoordinateValidation().getReason());
+        assertFalse(detail.getDevices().get(0).getCoordinateValidation().isRenderable());
         assertEquals(1, detail.getDevices().size());
         assertEquals("DEV-001", detail.getDevices().get(0).getDeviceCode());
         assertEquals("压力计", detail.getDevices().get(0).getDeviceTypeName());
@@ -103,6 +110,12 @@ class BusinessCockpitMonitoringPointDetailAdapterTest {
                 Collections.emptyList(), null);
     }
 
+    private static CockpitMonitoringPointDeviceSnapshot fact(String code, BigDecimal longitude, BigDecimal latitude) {
+        return new CockpitMonitoringPointDeviceSnapshot(
+                code, code, "压力计", longitude, latitude, false,
+                true, false, false, false, Collections.emptyList(), null);
+    }
+
     private static MonitorPoint point(String id, String code, String name, String longitude, String latitude) {
         MonitorPoint point = new MonitorPoint();
         point.setPointId(id);

+ 9 - 1
pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/CockpitMonitoringPointDetailControllerTest.java

@@ -79,7 +79,12 @@ class CockpitMonitoringPointDetailControllerTest {
                 .andExpect(jsonPath("$.data.deviceStatistics.offlineDeviceCount").value(1))
                 .andExpect(jsonPath("$.data.devices", hasSize(5)))
                 .andExpect(jsonPath("$.data.devices[0].deviceCode").value("EQ-001"))
-                .andExpect(jsonPath("$.data.devices[0].deviceStatus").value("ALARM"))
+                .andExpect(jsonPath("$.data.devices[0].longitude").value(110.142))
+                 .andExpect(jsonPath("$.data.devices[0].latitude").value(28.453))
+                 .andExpect(jsonPath("$.data.devices[0].coordinateValidation.state").value("UNVERIFIED"))
+                 .andExpect(jsonPath("$.data.devices[0].coordinateValidation.reason").value("UNVERIFIED_COORDINATE"))
+                 .andExpect(jsonPath("$.data.devices[0].coordinateValidation.renderable").value(false))
+                 .andExpect(jsonPath("$.data.devices[0].deviceStatus").value("ALARM"))
                 .andExpect(jsonPath("$.data.devices[0].statusLabel").value("报警"))
                 .andExpect(jsonPath("$.data.devices[1].deviceCode").value("EQ-003"))
                 .andExpect(jsonPath("$.data.devices[2].deviceCode").value("EQ-005"))
@@ -251,6 +256,9 @@ class CockpitMonitoringPointDetailControllerTest {
                 deviceCode,
                 deviceCode + "设备",
                 "压力计",
+                new BigDecimal("110.142"),
+                new BigDecimal("28.453"),
+                false,
                 online,
                 alarm,
                 warning,

+ 7 - 1
pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/monitoring/CockpitMonitoringDistributionService.java

@@ -344,7 +344,10 @@ public class CockpitMonitoringDistributionService {
                         device.getDeviceCode().trim(),
                         device.getDeviceName(),
                         type.getName(),
-                        device.isOnline(),
+                        device.getLongitude(),
+                         device.getLatitude(),
+                         validation,
+                         device.isOnline(),
                         device.isAlarm(),
                         device.isWarning(),
                         device.isWorkOrderInProgress(),
@@ -474,6 +477,9 @@ public class CockpitMonitoringDistributionService {
                 source.getDeviceCode(),
                 source.getDeviceName(),
                 source.getDeviceTypeName(),
+                 source.getLongitude(),
+                 source.getLatitude(),
+                 source.getCoordinateValidation(),
                 online,
                 source.isAlarm(),
                 source.isWarning(),

+ 3 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/point/BusinessCockpitDeviceFactResolver.java

@@ -75,6 +75,9 @@ public class BusinessCockpitDeviceFactResolver {
                 deviceCode,
                 device.getEquipmentName(),
                 deviceTypeName,
+                 device.getLongitude(),
+                 device.getLatitude(),
+                 false,
                 online,
                 alarm,
                 hasOpenWarning(deviceCode),

+ 22 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/point/CockpitMonitoringPointDeviceCard.java

@@ -1,11 +1,18 @@
 package com.zksy.cockpit.point;
 
+import com.zksy.cockpit.foundation.CockpitCoordinateValidation;
+import com.zksy.cockpit.foundation.CockpitCoordinateValidator;
+
+import java.math.BigDecimal;
 import java.util.List;
 
 public class CockpitMonitoringPointDeviceCard {
     private final String deviceCode;
     private final String deviceName;
     private final String deviceTypeName;
+    private final BigDecimal longitude;
+    private final BigDecimal latitude;
+    private final CockpitCoordinateValidation coordinateValidation;
     private final boolean online;
     private final boolean alarm;
     private final boolean warning;
@@ -18,9 +25,21 @@ public class CockpitMonitoringPointDeviceCard {
     public CockpitMonitoringPointDeviceCard(String deviceCode, String deviceName, String deviceTypeName,
             boolean online, boolean alarm, boolean warning, boolean workOrderInProgress,
             List<String> statusReasons, String statusTime, CockpitMonitoringPointStatus deviceStatus) {
+        this(deviceCode, deviceName, deviceTypeName, null, null,
+                CockpitCoordinateValidator.validate(null, null, false),
+                online, alarm, warning, workOrderInProgress, statusReasons, statusTime, deviceStatus);
+    }
+
+    public CockpitMonitoringPointDeviceCard(String deviceCode, String deviceName, String deviceTypeName,
+            BigDecimal longitude, BigDecimal latitude, CockpitCoordinateValidation coordinateValidation,
+            boolean online, boolean alarm, boolean warning, boolean workOrderInProgress,
+            List<String> statusReasons, String statusTime, CockpitMonitoringPointStatus deviceStatus) {
         this.deviceCode = deviceCode;
         this.deviceName = deviceName;
         this.deviceTypeName = deviceTypeName;
+        this.longitude = longitude;
+        this.latitude = latitude;
+        this.coordinateValidation = coordinateValidation;
         this.online = online;
         this.alarm = alarm;
         this.warning = warning;
@@ -34,6 +53,9 @@ public class CockpitMonitoringPointDeviceCard {
     public String getDeviceCode() { return deviceCode; }
     public String getDeviceName() { return deviceName; }
     public String getDeviceTypeName() { return deviceTypeName; }
+    public BigDecimal getLongitude() { return longitude; }
+    public BigDecimal getLatitude() { return latitude; }
+    public CockpitCoordinateValidation getCoordinateValidation() { return coordinateValidation; }
     public boolean isOnline() { return online; }
     public boolean isAlarm() { return alarm; }
     public boolean isWarning() { return warning; }

+ 30 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/point/CockpitMonitoringPointDeviceSnapshot.java

@@ -1,11 +1,18 @@
 package com.zksy.cockpit.point;
 
+import com.zksy.cockpit.foundation.CockpitCoordinateValidation;
+import com.zksy.cockpit.foundation.CockpitCoordinateValidator;
+
+import java.math.BigDecimal;
 import java.util.List;
 
 public class CockpitMonitoringPointDeviceSnapshot {
     private final String deviceCode;
     private final String deviceName;
     private final String deviceTypeName;
+    private final BigDecimal longitude;
+    private final BigDecimal latitude;
+    private final CockpitCoordinateValidation coordinateValidation;
     private final boolean online;
     private final boolean alarm;
     private final boolean warning;
@@ -16,9 +23,29 @@ public class CockpitMonitoringPointDeviceSnapshot {
     public CockpitMonitoringPointDeviceSnapshot(String deviceCode, String deviceName, String deviceTypeName,
             boolean online, boolean alarm, boolean warning, boolean workOrderInProgress,
             List<String> statusReasons, String statusTime) {
+        this(deviceCode, deviceName, deviceTypeName, null, null, false,
+                online, alarm, warning, workOrderInProgress, statusReasons, statusTime);
+    }
+
+    public CockpitMonitoringPointDeviceSnapshot(String deviceCode, String deviceName, String deviceTypeName,
+            BigDecimal longitude, BigDecimal latitude, boolean coordinateVerified,
+            boolean online, boolean alarm, boolean warning, boolean workOrderInProgress,
+            List<String> statusReasons, String statusTime) {
+        this(deviceCode, deviceName, deviceTypeName, longitude, latitude,
+                CockpitCoordinateValidator.validate(longitude, latitude, coordinateVerified),
+                online, alarm, warning, workOrderInProgress, statusReasons, statusTime);
+    }
+
+    public CockpitMonitoringPointDeviceSnapshot(String deviceCode, String deviceName, String deviceTypeName,
+            BigDecimal longitude, BigDecimal latitude, CockpitCoordinateValidation coordinateValidation,
+            boolean online, boolean alarm, boolean warning, boolean workOrderInProgress,
+            List<String> statusReasons, String statusTime) {
         this.deviceCode = deviceCode;
         this.deviceName = deviceName;
         this.deviceTypeName = deviceTypeName;
+        this.longitude = longitude;
+        this.latitude = latitude;
+        this.coordinateValidation = coordinateValidation;
         this.online = online;
         this.alarm = alarm;
         this.warning = warning;
@@ -30,6 +57,9 @@ public class CockpitMonitoringPointDeviceSnapshot {
     public String getDeviceCode() { return deviceCode; }
     public String getDeviceName() { return deviceName; }
     public String getDeviceTypeName() { return deviceTypeName; }
+    public BigDecimal getLongitude() { return longitude; }
+    public BigDecimal getLatitude() { return latitude; }
+    public CockpitCoordinateValidation getCoordinateValidation() { return coordinateValidation; }
     public boolean isOnline() { return online; }
     public boolean isAlarm() { return alarm; }
     public boolean isWarning() { return warning; }

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

@@ -37,7 +37,10 @@ public final class CockpitMonitoringPointStatusResolver {
                         device.getDeviceCode(),
                         device.getDeviceName(),
                         device.getDeviceTypeName(),
-                        device.isOnline(),
+                        device.getLongitude(),
+                         device.getLatitude(),
+                         device.getCoordinateValidation(),
+                         device.isOnline(),
                         device.isAlarm(),
                         device.isWarning(),
                         device.isWorkOrderInProgress(),