Browse Source

补齐统一报警列表的供水展示状态与设备状态契约 (#66)

按报警记录、审核结论和关联工单推导展示状态,设备当前报警状态独立取自设备状态表;支持服务端状态筛选,并以 service/mapper 测试锁定两种状态口径。不改表和菜单。
Kazerin 3 weeks ago
parent
commit
bf69ede5cd

+ 1 - 1
pipe-network-service/CONTEXT.md

@@ -51,7 +51,7 @@ _Avoid_:把供水子系统等同于单一的报警页面,或把监测设备
 
 ### 4. 监测报警
 
-4.1 **报警管理**:供水报警管理与排水报警清单统一到 `subSystem/alarm/common/list`,展示报警单号、设备信息、监测/审核/工单状态与责任人,按菜单 `rootTypeId` 校验授权并过滤设备类型树。清单只承载查询与详情,审核、派发和处置仍在相应页面。排水另有 `remind` 提醒、`drainage/analysis` 研判、`drainage/alarmGIS` GIS(无周边环境页签)。新清单替代旧管理页的态势/占比/导出分支;供水原有报警一张图继续保留。不同报警类型可形成不同工单;同一设备同一报警类型同时只能存在一个未结束的同类报警工单(2026-09-24)。
+4.1 **报警管理**:供水报警管理是供水专属的只读报警流水、四级趋势和态势台账;排水保留公共报警清单。报警展示状态由后端结合报警记录及关联工单推导,设备报警状态来自设备状态表,两者与工单处置状态分别展示;按菜单 `rootTypeId` 校验授权并过滤设备类型树。审核、派发和处置仍在相应页面。排水另有 `remind` 提醒、`drainage/analysis` 研判、`drainage/alarmGIS` GIS(无周边环境页签)。供水专属页面恢复态势、分类占比和导出能力;供水监测报警一张图继续保留。不同报警类型可形成不同工单;同一设备同一报警类型同时只能存在一个未结束的同类报警工单(2026-09-24)。
 
 4.2 **报警审核**:四个子系统统一使用 `AlarmController` 的审核接口和 `subSystem/alarm/common/audit` 页面,列表仅展示 `alarm_status=0` 的事件。`audit_status` 独立记录未审核、确认报警、误报警;确认报警时创建待派单工单,误报警只记录结论,两种审核都不直接修改设备状态或报警处理状态。完整派单、接单、处理、验收由统一工单页承载。旧供水审核、直接派单和停用的解除接口已删除。报警生成、审核及处理记录在 `alarm_log`,确认记录直接包含“已转工单(实际工单号)”及选填意见;关联工单、工单操作不再单独写入报警日志。人工操作使用 `sys_user.nick_name`,历史按时间和日志 ID 倒序展示,不回填或删除历史数据。运行采集系统在收到有效恢复数据时,可以自动处理同设备、同报警编码的未处理告警;数值回到预警区即可处理,处理记录注明“系统自动”,不自动完成关联工单(见仓库docs/collection-alarm-recovery-2026-09-23.md)。
 

+ 4 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/dto/AlarmQuery.java

@@ -9,6 +9,10 @@ import javax.validation.constraints.Size;
 @EqualsAndHashCode(callSuper = true)
 public class AlarmQuery extends AlarmReviewQuery {
     private Integer alarmStatus;
+    /** 用户可见的报警展示状态:1待审核、2已转工单、3已解除、4误报警。 */
+    private Integer displayStatus;
+    /** 设备状态表中的报警状态:0正常、1报警。 */
+    private Integer deviceAlarmStatus;
     /** 研判候选事件仅保留有触发实际值的报警,零值仍属于有效值。 */
     @ApiModelProperty("只查询 actual_value 非空的报警,默认 false,实际值 0 保留")
     private boolean actualValueOnly;

+ 3 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/dto/AlarmVO.java

@@ -9,6 +9,9 @@ import java.time.LocalDateTime;
 @EqualsAndHashCode(callSuper = true)
 public class AlarmVO extends AlarmReviewVO {
     private Integer equipmentTypeId;
+    private Integer deviceAlarmStatus;
+    private Integer displayStatus;
+    private String displayStatusText;
     private String equipmentImage;
     private String orderNo;
     private Integer orderStatus;

+ 5 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/service/AlarmOperationsService.java

@@ -163,6 +163,8 @@ public class AlarmOperationsService {
         if (query.getAlarmLevel() != null && AlarmLevelEnum.fromCode(query.getAlarmLevel()) == null) throw new ServiceException("报警等级无效");
         if (query.getAuditStatus() != null && AlarmAuditStatusEnum.fromCode(query.getAuditStatus()) == null) throw new ServiceException("审核结果无效");
         if (query.getAlarmStatus() != null && AlarmRecordStatusEnum.fromCode(query.getAlarmStatus()) == null) throw new ServiceException("监测状态无效");
+        if (query.getDisplayStatus() != null && (query.getDisplayStatus() < 1 || query.getDisplayStatus() > 4)) throw new ServiceException("报警展示状态无效");
+        if (query.getDeviceAlarmStatus() != null && query.getDeviceAlarmStatus() != 0 && query.getDeviceAlarmStatus() != 1) throw new ServiceException("设备报警状态无效");
         if (query.getStartTime() != null && query.getEndTime() != null && query.getStartTime().isAfter(query.getEndTime())) throw new ServiceException("开始时间不能晚于结束时间");
     }
     private void labels(AlarmVO alarm) {
@@ -173,5 +175,8 @@ public class AlarmOperationsService {
         alarm.setMonitorStatusName(Objects.equals(alarm.getAlarmStatus(), AlarmRecordStatusEnum.UNHANDLED.getCode()) ? "异常"
                 : Objects.equals(alarm.getAlarmStatus(), AlarmRecordStatusEnum.HANDLED.getCode()) ? "已恢复" : "未知");
         alarm.setOrderStatusName(alarm.getWorkOrderId() == null ? "未转工单" : WorkOrderStatusEnum.getDescByCode(alarm.getOrderStatus()));
+        int displayStatus = Objects.equals(alarm.getAuditStatus(), 2) ? 4 : Objects.equals(alarm.getAlarmStatus(), AlarmRecordStatusEnum.HANDLED.getCode()) ? 3 : alarm.getWorkOrderId() != null ? 2 : 1;
+        alarm.setDisplayStatus(displayStatus);
+        alarm.setDisplayStatusText(displayStatus == 4 ? "误报警" : displayStatus == 3 ? "已解除" : displayStatus == 2 ? "已转工单" : "待审核");
     }
 }

+ 4 - 1
pipe-network-service/zksy-system/src/main/resources/mapper/base/AlarmOperationsMapper.xml

@@ -5,12 +5,13 @@
         FROM app_user.alarm_data a
         LEFT JOIN LATERAL (SELECT b.* FROM app_user.equipment_base b WHERE b.equipment_code = a.device_code
             ORDER BY b.equipment_id LIMIT 1) e ON true
+        LEFT JOIN LATERAL (SELECT s.alarm_status FROM app_user.equipment_status s WHERE s.equipment_id = e.equipment_id AND s.del_flag = '0' ORDER BY s.status_update_time DESC NULLS LAST, s.status_id DESC LIMIT 1) st ON true
         LEFT JOIN app_user.work_order w ON w.order_id = a.work_order_id
         LEFT JOIN app_user.sys_user u ON u.user_id = w.receive_user
     </sql>
     <sql id="columns">
         a.*, e.equipment_id, e.equipment_name AS current_device_name, e.equipment_location,
-        e.equipment_type_id, e.equipment_image, e.longitude, e.latitude,
+        e.equipment_type_id, e.equipment_image, e.longitude, e.latitude, st.alarm_status AS device_alarm_status,
         w.order_no, w.order_status, w.plan_finish_time, COALESCE(u.nick_name,u.user_name) AS receive_user_name,
         (SELECT wt.warning_unit FROM app_user.equipment_warning_type wt
             WHERE wt.equipment_type_id = e.equipment_type_id AND wt.warning_code = a.warning_code LIMIT 1) AS warning_unit
@@ -28,6 +29,8 @@
         <if test="query.alarmNo != null and query.alarmNo != ''">AND a.alarm_no ILIKE CONCAT('%',#{query.alarmNo},'%')</if>
         <if test="query.deviceCode != null and query.deviceCode != ''">AND a.device_code = #{query.deviceCode}</if>
         <if test="query.alarmStatus != null">AND a.alarm_status = #{query.alarmStatus}</if>
+        <if test="query.deviceAlarmStatus != null">AND st.alarm_status = #{query.deviceAlarmStatus}</if>
+        <if test="query.displayStatus != null">AND (CASE WHEN a.audit_status = 2 THEN 4 WHEN a.alarm_status = 1 THEN 3 WHEN a.work_order_id IS NOT NULL THEN 2 ELSE 1 END) = #{query.displayStatus}</if>
         <if test="query.actualValueOnly">AND a.actual_value IS NOT NULL</if>
         <if test="query.auditStatus != null">AND a.audit_status = #{query.auditStatus}</if>
         <if test="query.alarmLevel != null">AND a.alarm_level = #{query.alarmLevel}</if>

+ 17 - 0
pipe-network-service/zksy-system/src/test/java/com/zksy/base/alarm/mapper/AlarmOperationsMapperTest.java

@@ -95,4 +95,21 @@ class AlarmOperationsMapperTest {
     private String sql(MybatisConfiguration config,String name,Map<String,Object> params) {
         return config.getMappedStatement(AlarmOperationsMapper.class.getName()+"."+name).getBoundSql(params).getSql().replaceAll("\\s+"," ").trim();
     }
+    @Test void ledgerFiltersDisplayAndDeviceAlarmStatesWithinAuthorizedScope() throws Exception {
+        MybatisConfiguration config = new MybatisConfiguration();
+        for (String name : Arrays.asList("AlarmReviewMapper", "AlarmOperationsMapper")) {
+            try (InputStream xml = getClass().getResourceAsStream("/mapper/base/" + name + ".xml")) {
+                assertNotNull(xml); new XMLMapperBuilder(xml, config, name, config.getSqlFragments()).parse();
+            }
+        }
+        AlarmQuery query = new AlarmQuery(); query.setRootTypeId(1);
+        query.setDisplayStatus(2); query.setDeviceAlarmStatus(1);
+        Map<String, Object> params = new HashMap<>();
+        params.put("query", query); params.put("typeIds", Arrays.asList(1, 11, 12, 13, 14));
+        String sql = sql(config, "selectPage", params);
+        assertTrue(sql.contains("e.equipment_type_id IN"));
+        assertTrue(sql.contains("st.alarm_status = ?"));
+        assertTrue(sql.contains("CASE WHEN a.audit_status = 2 THEN 4 WHEN a.alarm_status = 1 THEN 3"));
+        assertTrue(sql.contains("st.alarm_status AS device_alarm_status"));
+    }
 }

+ 19 - 0
pipe-network-service/zksy-system/src/test/java/com/zksy/base/alarm/service/AlarmOperationsServiceTest.java

@@ -41,6 +41,25 @@ class AlarmOperationsServiceTest {
         assertEquals("确认报警",alarm.getAuditStatusName()); assertEquals("处理中",alarm.getOrderStatusName());
         verify(scope).resolveOperations(query);
     }
+    @Test void ledgerExposesIndependentDisplayAndDeviceAlarmStates() {
+        AlarmVO pending = new AlarmVO(); pending.setAlarmStatus(0); pending.setAuditStatus(0); pending.setDeviceAlarmStatus(0);
+        AlarmVO transferred = new AlarmVO(); transferred.setAlarmStatus(0); transferred.setAuditStatus(1);
+        transferred.setWorkOrderId(9L); transferred.setOrderStatus(3); transferred.setDeviceAlarmStatus(0);
+        AlarmVO released = new AlarmVO(); released.setAlarmStatus(1); released.setAuditStatus(1); released.setDeviceAlarmStatus(1);
+        AlarmVO falseAlarm = new AlarmVO(); falseAlarm.setAlarmStatus(0); falseAlarm.setAuditStatus(2); falseAlarm.setDeviceAlarmStatus(1);
+        Page<AlarmVO> page = new Page<>(1, 10, 3);
+        page.setRecords(Arrays.asList(pending, transferred, released, falseAlarm));
+        when(mapper.selectPage(any(), eq(query), anyList())).thenReturn(page);
+        service.page(query);
+        assertEquals("待审核", pending.getDisplayStatusText());
+        assertEquals(0, pending.getDeviceAlarmStatus());
+        assertEquals("已转工单", transferred.getDisplayStatusText());
+        assertEquals(0, transferred.getDeviceAlarmStatus());
+        assertEquals("已解除", released.getDisplayStatusText());
+        assertEquals("误报警", falseAlarm.getDisplayStatusText());
+        query.setDisplayStatus(5);
+        assertThrows(ServiceException.class, () -> service.page(query));
+    }
     @Test void statisticsKeepsFiltersAndResolvesAuthorizedScopeBeforeAggregating() {
         query.setPageNum(3); query.setPageSize(10); query.setKeyword("设备");
         query.setAlarmStatus(AlarmRecordStatusEnum.HANDLED.getCode());