소스 검색

feat(cockpit): monitoring/overview 回出逐设备台账 devices[](票 62 追加需求)

- 运行概况改为展示**设备台账**(逐台一行),不再是异常设备清单:
  overview 的 devices[] 与 counts.equipmentCount 同源同口径(同一遍历产出),
  行数恒等于该计数;筛选生效时两者同步收窄
- 每行:deviceCode / deviceName / deviceType{code,name} / industry /
  monitoringState / monitoringPointCode / sampledAt / createdAt;缺值一律 null
- 快照 SourceDevice 补 deviceName(equipment_base.equipment_name),业务适配器填充
- 契约测试:行数==equipmentCount、逐行字段非空、筛选后仍相等
- cockpit 全量 429/429 通过
Kazerin 9 시간 전
부모
커밋
3575e5ff48

+ 2 - 1
docs/cockpit/README.md

@@ -37,7 +37,7 @@
 | 基础设施 | `GET /api/cockpit/v1/infrastructure/operating-information` | `industry` `range` `from` `to` |
 | 基础设施 | `GET /api/cockpit/v1/infrastructure/management-information` | `industry` `district` |
 | 基础设施 | `GET /api/cockpit/v1/infrastructure/emergency-support` | `category` `industry` `area` |
-| 运行监测 | `GET /api/cockpit/v1/monitoring/overview` | `industry` `deviceType` |
+| 运行监测 | `GET /api/cockpit/v1/monitoring/overview` | `industry` `deviceType`;响应含逐设备台账 `devices[]` |
 | 运行监测 | `GET /api/cockpit/v1/monitoring/distribution` | `industry` `deviceType` `pointStatus` `district` `page` `pageSize` `snapshotAt` |
 | 运行监测 | `GET /api/cockpit/v1/monitoring/situation` | `industry` `deviceType` `startTime` `endTime` `page` `pageSize` |
 | 预警处置 | `GET /api/cockpit/v1/events/current` | `mode`(必填) `level` `industry` `status` `page` `pageSize` |
@@ -96,6 +96,7 @@
 - `overview/warning-settings` 的趋势桶是**业务时区日期**(`yyyy-MM-dd`),声明窗口时按「桶日期落在窗口起止日期之间(含端点)」筛选,**桶顺序沿用适配器顺序**;桶标签无法解析时保留该桶,不静默丢数据。
 - 未知类型、非智能设备、非正式行业在**按行业/类型筛选时**显式排除,不做静默归类;`map-snapshot` 不带筛选时仍返回全部对象(`industry`/`typeName` 为 `null`),保证「全城底图」不因类型数据缺失而丢对象。
 - 事件行(`overview`/`warningResponse` 的 `events/current` 的 `EventItem`)携带 `coordinateValidation`,与地图/监测切片**同一校验实现**(`CockpitCoordinateValidator`);坐标结构非法、越界或落在主场景区外时 `renderable = false` 且给出 `reason`。
+- `monitoring/overview` 的 `devices[]` 是**逐设备台账行**(2026-10-22 追加):每行 = `deviceCode` / `deviceName` / `deviceType{code,name}` / `industry` / `monitoringState` / `monitoringPointCode` / `sampledAt` / `createdAt`,顺序为后端遍历顺序(稳定,调用方不重排)。**行数恒等于 `counts.equipmentCount`**(同一遍历产出,不是第二个口径),筛选(`industry`/`deviceType`)生效时两者同步收窄。它与 `monitoring/situation` 的 `abnormalEquipment[]` **不是一回事**:后者只装异常四态(WARNING/ALARM/EXPIRED/OFFLINE)设备,是「异常清单」;前者是**全部**智能设备的台账。缺值一律 `null`(`deviceName`/`monitoringPointCode`/`createdAt`),不拿编码冒充、不填 0。
 - 事件坐标按同一自动准入判定(不再依赖 `coordinateVerified`):区内且数值合法 → `VALID` / `renderable=true`。报警族在数据可得时另带 `thresholdMinValue`/`thresholdMaxValue`(预警族为 `null`)。
 - `events/history-analysis` 同时返回两块内容:`rows[]` 是**小时桶聚合行**(趋势与行级处置率口径,不携带逐事件坐标/阈值);`events[]` 是**逐事件行**(票 61 新增,形状与 `events/current` 的 `EventItem` 同构),按 `eventTime` 倒序、同时间按 `eventNo` 升序,由 `page`/`pageSize`(默认 1/100,上限 1000)分页,元数据在 `pagination` 中,`pagination.total` 即窗口内事件总数。逐事件坐标仍只在 `events/current` 提供,历史逐事件行的 `coordinateValidation` 为 `null`。
 

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

@@ -81,12 +81,42 @@ class CockpitMonitoringOverviewControllerTest {
                 .andExpect(jsonPath("$.data.typeDistribution", hasSize(4)))
                 .andExpect(jsonPath("$.data.typeDistribution[0].deviceCount").value(2))
                 .andExpect(jsonPath("$.data.typeDistribution[0].onlineCount").value(1))
+                // 执行票 62:运行概况要展示**设备台账**(逐台一行),所以 overview 回出 devices[]。
+                // 与 counts.equipmentCount 同源:行数必须恒等于那个计数(不能有第二个口径)。
+                .andExpect(jsonPath("$.data.devices", hasSize(6)))
+                .andExpect(jsonPath("$.data.devices[0].deviceCode").isNotEmpty())
+                .andExpect(jsonPath("$.data.devices[0].monitoringState").isNotEmpty())
+                .andExpect(jsonPath("$.data.devices[0].deviceType.code").isNotEmpty())
                 .andExpect(jsonPath("$.data.mapFilter.deviceCodes", hasSize(6)))
                 .andExpect(jsonPath("$.data.excluded.unknownTypeCount").value(1))
                 .andExpect(jsonPath("$.data.excluded.typeKindMismatchCount").value(1))
                 .andExpect(jsonPath("$.data.excluded.nonFormalIndustryCount").value(0));
     }
 
+    @Test
+    void deviceLedgerRowsMatchEquipmentCountAndCarryNameAndPoint() throws Exception {
+        // 执行票 62:设备台账 = 逐设备一行,行数**恒等于** counts.equipmentCount;
+        // 每行带设备名(不拿编码冒充)、类型、行业、状态、所属点位、数据时间。
+        when(adapter.load()).thenReturn(fixture());
+
+        mockMvc.perform(get(ENDPOINT))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.data.counts.equipmentCount").value(6))
+                .andExpect(jsonPath("$.data.devices", hasSize(6)))
+                .andExpect(jsonPath("$.data.devices[0].deviceCode").isNotEmpty())
+                .andExpect(jsonPath("$.data.devices[0].deviceName").isNotEmpty())
+                .andExpect(jsonPath("$.data.devices[0].deviceType.code").isNotEmpty())
+                .andExpect(jsonPath("$.data.devices[0].deviceType.name").isNotEmpty())
+                .andExpect(jsonPath("$.data.devices[0].industry").isNotEmpty())
+                .andExpect(jsonPath("$.data.devices[0].monitoringState").isNotEmpty());
+
+        // 筛选后台账跟着收窄:行数仍等于 equipmentCount(同一个筛选口径)。
+        mockMvc.perform(get(ENDPOINT).queryParam("industry", "GAS"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.data.counts.equipmentCount").value(2))
+                .andExpect(jsonPath("$.data.devices", hasSize(2)));
+    }
+
     @Test
     void industryFilterRestrictsOverview() throws Exception {
         when(adapter.load()).thenReturn(fixture());
@@ -327,8 +357,9 @@ class CockpitMonitoringOverviewControllerTest {
 
     private static CockpitMonitoringOverviewSnapshot.SourceDevice device(
             String code, String typeCode, String state, String pointCode) {
+        // 执行票 62:设备台账要显示名称 —— 夹具带上真实形状的名称(`设备-<编码>`)。
         return new CockpitMonitoringOverviewSnapshot.SourceDevice(
-                code, typeCode, state, "2026-09-02T09:59:00+08:00", pointCode);
+                code, "设备-" + code, typeCode, state, "2026-09-02T09:59:00+08:00", pointCode, null);
     }
 
     private static CockpitMonitoringOverviewSnapshot.SourceDevice deviceAt(

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

@@ -123,6 +123,8 @@ public class BusinessCockpitMonitoringOverviewAdapter implements CockpitMonitori
             }
             devices.add(new CockpitMonitoringOverviewSnapshot.SourceDevice(
                     deviceCode,
+                    // 执行票 62:设备台账要显示名称(缺名如实为 null,不拿编码冒充)。
+                    isBlank(device.getEquipmentName()) ? null : device.getEquipmentName().trim(),
                     type == null || type.getId() == null ? null : String.valueOf(type.getId()),
                     monitoringState(status),
                     status == null || status.getStatusUpdateTime() == null

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

@@ -22,6 +22,12 @@ public class CockpitMonitoringOverviewResponse {
     private final Status status;
     private final OnlineRate onlineRate;
     private final List<TypeDistribution> typeDistribution;
+    /**
+     * 逐设备台账(执行票 62 追加需求):与 {@code counts.equipmentCount} 同源同口径
+     * (筛选后的智能设备全集,逐台一行),供运行概况的「设备台账」清单直接渲染。
+     * 顺序=后端遍历顺序(稳定),前端不重排。
+     */
+    private final List<Device> devices;
     private final MapFilter mapFilter;
     private final Excluded excluded;
 
@@ -39,6 +45,7 @@ public class CockpitMonitoringOverviewResponse {
             Status status,
             OnlineRate onlineRate,
             List<TypeDistribution> typeDistribution,
+            List<Device> devices,
             MapFilter mapFilter,
             Excluded excluded) {
         this.requestId = requestId;
@@ -54,6 +61,7 @@ public class CockpitMonitoringOverviewResponse {
         this.status = status;
         this.onlineRate = onlineRate;
         this.typeDistribution = typeDistribution;
+        this.devices = devices;
         this.mapFilter = mapFilter;
         this.excluded = excluded;
     }
@@ -71,6 +79,7 @@ public class CockpitMonitoringOverviewResponse {
     public Status getStatus() { return status; }
     public OnlineRate getOnlineRate() { return onlineRate; }
     public List<TypeDistribution> getTypeDistribution() { return typeDistribution; }
+    public List<Device> getDevices() { return devices; }
     public MapFilter getMapFilter() { return mapFilter; }
     public Excluded getExcluded() { return excluded; }
 
@@ -164,6 +173,56 @@ public class CockpitMonitoringOverviewResponse {
         public String getReason() { return reason; }
     }
 
+    /**
+     * 逐设备台账行(执行票 62 追加需求)。
+     *
+     * <p>字段只取「运行概况的设备台账」这一屏真正要显示的:设备编码/名称、类型、行业、
+     * 监测状态、所属监测点、采样时间、数据时间(记录创建时间)。**不在这里加设备坐标**
+     * ——那是 `map-snapshot` 的职责;也不加阈值 ——那是 `monitoring/situation` 的异常口径。
+     * 缺值一律 {@code null},不填 0、不填空串冒充。
+     */
+    public static class Device {
+        private final String deviceCode;
+        private final String deviceName;
+        private final DeviceType deviceType;
+        private final String industry;
+        /** 设备监测状态(后端 `MONITORING_STATE` 五态之一);缺失为 {@code null}。 */
+        private final String monitoringState;
+        /** 所属监测点编码(未关联设备为 {@code null})。 */
+        private final String monitoringPointCode;
+        private final String sampledAt;
+        /** 数据时间=记录创建时间({@code equipment_base.create_time})。 */
+        private final String createdAt;
+
+        public Device(
+                String deviceCode,
+                String deviceName,
+                DeviceType deviceType,
+                String industry,
+                String monitoringState,
+                String monitoringPointCode,
+                String sampledAt,
+                String createdAt) {
+            this.deviceCode = deviceCode;
+            this.deviceName = deviceName;
+            this.deviceType = deviceType;
+            this.industry = industry;
+            this.monitoringState = monitoringState;
+            this.monitoringPointCode = monitoringPointCode;
+            this.sampledAt = sampledAt;
+            this.createdAt = createdAt;
+        }
+
+        public String getDeviceCode() { return deviceCode; }
+        public String getDeviceName() { return deviceName; }
+        public DeviceType getDeviceType() { return deviceType; }
+        public String getIndustry() { return industry; }
+        public String getMonitoringState() { return monitoringState; }
+        public String getMonitoringPointCode() { return monitoringPointCode; }
+        public String getSampledAt() { return sampledAt; }
+        public String getCreatedAt() { return createdAt; }
+    }
+
     public static class TypeDistribution {
         private final DeviceType deviceType;
         private final String industry;

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

@@ -216,6 +216,34 @@ public class CockpitMonitoringOverviewService {
         List<String> pointCodes = new ArrayList<>(selectedPointCodes);
         List<String> deviceCodes = new ArrayList<>(selectedDeviceCodes);
 
+        // 执行票 62 追加需求:运行概况要展示**设备台账**(逐台一行),不是只有聚合计数。
+        // 逐设备行与 `counts.equipmentCount` 同源(同一次遍历产出的 `selectedDevices`),
+        // 所以两处读数结构性一致——台账行数就是 equipmentCount,不会出现第二个口径。
+        List<CockpitMonitoringOverviewResponse.Device> devices = new ArrayList<>();
+        for (DeviceRuntime runtime : state.devices.values()) {
+            String typeCode = runtime.typeCode;
+            CockpitMonitoringOverviewSnapshot.SourceType type = isBlank(typeCode) ? null : types.get(typeCode);
+            // 与上面的筛选分支**同一套判据**:只把真正计入本响应的设备放进台账。
+            if (type == null
+                    || !SMART_DEVICE.equals(type.getObjectKind())
+                    || type.getIndustry() == null
+                    || !INDUSTRIES.contains(type.getIndustry())
+                    || (selectedIndustry != null && !selectedIndustry.equals(type.getIndustry()))
+                    || (!isBlank(deviceType) && !deviceType.trim().equals(typeCode))) {
+                continue;
+            }
+            CockpitMonitoringOverviewSnapshot.SourceDevice source = snapshotDevice(snapshot, runtime.deviceCode);
+            devices.add(new CockpitMonitoringOverviewResponse.Device(
+                    runtime.deviceCode,
+                    source == null ? null : source.getDeviceName(),
+                    new CockpitMonitoringOverviewResponse.DeviceType(type.getCode(), type.getName()),
+                    type.getIndustry(),
+                    runtime.monitoringState,
+                    isBlank(runtime.pointCode) ? null : runtime.pointCode,
+                    runtime.sampledAt,
+                    source == null ? null : source.getCreatedAt()));
+        }
+
         return new CockpitMonitoringOverviewResponse(
                 requestId,
                 "v1",
@@ -237,6 +265,7 @@ public class CockpitMonitoringOverviewService {
                 new CockpitMonitoringOverviewResponse.OnlineRate(
                         rate, online, denominator, rateState, rateReason),
                 typeDistribution,
+                devices,
                 new CockpitMonitoringOverviewResponse.MapFilter(
                         selectedIndustry,
                         isBlank(deviceType) ? null : deviceType.trim(),

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

@@ -45,6 +45,8 @@ public class CockpitMonitoringOverviewSnapshot {
 
     public static class SourceDevice {
         private final String deviceCode;
+        /** 设备名称({@code equipment_base.equipment_name});缺值为 {@code null}(不拿编码冒充)。 */
+        private final String deviceName;
         private final String typeCode;
         private final String monitoringState;
         private final String sampledAt;
@@ -58,7 +60,7 @@ public class CockpitMonitoringOverviewSnapshot {
                 String monitoringState,
                 String sampledAt,
                 String pointCode) {
-            this(deviceCode, typeCode, monitoringState, sampledAt, pointCode, null);
+            this(deviceCode, null, typeCode, monitoringState, sampledAt, pointCode, null);
         }
 
         public SourceDevice(
@@ -68,7 +70,20 @@ public class CockpitMonitoringOverviewSnapshot {
                 String sampledAt,
                 String pointCode,
                 String createdAt) {
+            this(deviceCode, null, typeCode, monitoringState, sampledAt, pointCode, createdAt);
+        }
+
+        /** 执行票 62 追加:设备台账要显示名称,只有编码不可读。 */
+        public SourceDevice(
+                String deviceCode,
+                String deviceName,
+                String typeCode,
+                String monitoringState,
+                String sampledAt,
+                String pointCode,
+                String createdAt) {
             this.deviceCode = deviceCode;
+            this.deviceName = deviceName;
             this.typeCode = typeCode;
             this.monitoringState = monitoringState;
             this.sampledAt = sampledAt;
@@ -77,6 +92,7 @@ public class CockpitMonitoringOverviewSnapshot {
         }
 
         public String getDeviceCode() { return deviceCode; }
+        public String getDeviceName() { return deviceName; }
         public String getTypeCode() { return typeCode; }
         public String getMonitoringState() { return monitoringState; }
         public String getSampledAt() { return sampledAt; }