Ver código fonte

refactor(alarm): 移除告警处理时间相关字段并优化处理逻辑

- 从数据库表结构中移除 handle_time、handle_user 和 handle_remark 字段
- 从 AlarmData 实体类中移除对应的属性定义
- 修改告警处理接口,不再接收处理人和备注参数,也不设置处理时间
- 更新 XML 映射文件中的列选择和更新语句
- 将设备名称和类型改为非数据库字段,通过设备台账动态补充
- 新增 CollectionAlarmWriter 服务类处理采集告警统一入库规则
- 重构平均响应时间统计逻辑,基于操作日志而非告警表字段计算
- 调整告警统计数据查询和展示逻辑,移除对已删除字段的引用
- 更新数据库触发器中告警处理日志的记录方式
- 优化设备信息填充逻辑,完善设备类型名称获取
- 重构告警统计查询,将设备名称和类型改为子查询获取
- 更新测试数据脚本以匹配新的数据库结构
林仔 3 semanas atrás
pai
commit
c5a5b87d34
23 arquivos alterados com 211 adições e 218 exclusões
  1. 0 3
      alarm_data_table.sql
  2. 1 2
      pipe-network-service/sql/migration_alarm_review.sql
  3. 10 45
      pipe-network-service/sql/water_supply_alarm_test_data.sql
  4. 17 51
      pipe-network-service/sql/water_supply_monitoring_alarm_rebuild_test_data.sql
  5. 1 6
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/alarm/AlarmController.java
  6. 0 1
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/base/alarm/AlarmDataController.java
  7. 3 8
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/gasbasic/GasAlarmController.java
  8. 4 6
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/gasbasic/PressureAlarmController.java
  9. 1 1
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/dto/out/AlarmStatsOutDTO.java
  10. 0 10
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/dto/out/WaterAlarmOutDTO.java
  11. 19 17
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/service/impl/WaterSupplyAlarmServiceImpl.java
  12. 0 12
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/export/WaterAlarmLedgerExportDTO.java
  13. 4 17
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/domain/AlarmData.java
  14. 4 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/mapper/AlarmDataMapper.java
  15. 20 2
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/service/impl/AlarmDataServiceImpl.java
  16. 16 0
      pipe-network-service/zksy-system/src/main/resources/mapper/base/AlarmDataMapper.xml
  17. 5 2
      pipe-network-service/zksy-system/src/main/resources/mapper/warning/AlarmWarningRelMapper.xml
  18. 1 1
      pipe-network-service/zksy-system/src/test/java/com/zksy/ops/service/impl/WorkOrderServiceImplTest.java
  19. 0 13
      zk-api-service/src/main/java/com/zksy/api/domain/AlarmData.java
  20. 1 2
      zk-api-service/src/main/java/com/zksy/api/mapper/AlarmDataMapper.java
  21. 93 0
      zk-api-service/src/main/java/com/zksy/api/service/CollectionAlarmWriter.java
  22. 7 9
      zk-api-service/src/main/resources/mapper/AlarmDataMapper.xml
  23. 4 10
      报警功能测试说明.md

+ 0 - 3
alarm_data_table.sql

@@ -9,9 +9,6 @@ CREATE TABLE IF NOT EXISTS `alarm_data` (
   `actual_value` decimal(10,2) DEFAULT NULL COMMENT '实际值',
   `alarm_status` int(1) DEFAULT 0 COMMENT '报警状态 0-未处理 1-已处理',
   `alarm_time` datetime DEFAULT NULL COMMENT '报警时间',
-  `handle_time` datetime DEFAULT NULL COMMENT '处理时间',
-  `handle_user` varchar(255) DEFAULT NULL COMMENT '处理人',
-  `handle_remark` text COMMENT '处理备注',
   `remark` text COMMENT '备注',
   `create_time` datetime DEFAULT NULL COMMENT '创建时间',
   `update_time` datetime DEFAULT NULL COMMENT '更新时间',

+ 1 - 2
pipe-network-service/sql/migration_alarm_review.sql

@@ -54,8 +54,7 @@ BEGIN
                 '监测系统', coalesce(NEW.alarm_time, NEW.create_time, CURRENT_TIMESTAMP));
     ELSIF NEW.alarm_status IS DISTINCT FROM OLD.alarm_status AND NEW.alarm_status = 1 THEN
         INSERT INTO app_user.alarm_log(alarm_id, oper_type, oper_desc, oper_user_name, oper_time)
-        VALUES (NEW.id, 3, coalesce(NEW.handle_remark, '报警已处理'), coalesce(NEW.handle_user, '监测系统'),
-                coalesce(NEW.handle_time, clock_timestamp()));
+        VALUES (NEW.id, 3, '报警已处理', '系统', clock_timestamp());
     END IF;
     IF (TG_OP = 'INSERT' AND NEW.order_id IS NOT NULL)
        OR (TG_OP = 'UPDATE' AND NEW.order_id IS DISTINCT FROM OLD.order_id AND NEW.order_id IS NOT NULL) THEN

+ 10 - 45
pipe-network-service/sql/water_supply_alarm_test_data.sql

@@ -82,54 +82,19 @@ END $$;
 
 -- ------------------------------------------------------------
 -- 2. 报警数据(alarm_data)
---    预期统计:实时报警3个设备 / 今日报警6条 / 平均响应25.00分钟 / 设备异常率75%
+--    预期统计:实时报警3个设备 / 今日报警6条 / 平均响应时间由alarm_log处理日志统计 / 设备异常率75%
 -- ------------------------------------------------------------
 INSERT INTO app_user.alarm_data
-    (id, device_code, device_name, device_type, warning_type, warning_code,
-     actual_value, min_value, max_value, alarm_status, alarm_level, deviation_ratio,
-     alarm_time, handle_time, handle_user, handle_remark, remark, create_time, update_time)
+    (id, device_code, warning_type, warning_code, actual_value, min_value, max_value, alarm_status, alarm_level, deviation_ratio, alarm_time, remark, create_time, update_time)
 VALUES
-    -- 未处理:水位1级(今日)
-    ('test-alarm-001', 'TEST-WL-001', '测试水位监测点1', '水位监测', '水位预警', 'WARN-WATER-LEVEL-TEST',
-     0.20, 0.50, 3.00, 0, 1, 40.00,
-     now() - interval '180 minutes', NULL, NULL, NULL, '供水监测报警测试数据',
-     now() - interval '180 minutes', now() - interval '180 minutes'),
-    -- 未处理:水位2级(今日,同设备第二条)
-    ('test-alarm-002', 'TEST-WL-001', '测试水位监测点1', '水位监测', '水位预警', 'WARN-WATER-LEVEL-TEST',
-     0.30, 0.50, 3.00, 0, 2, 35.00,
-     now() - interval '150 minutes', NULL, NULL, NULL, '供水监测报警测试数据',
-     now() - interval '150 minutes', now() - interval '150 minutes'),
-    -- 已处理:水位3级(今日,响应35分钟)→ 关联测试工单
-    ('test-alarm-003', 'TEST-WL-002', '测试水位监测点2', '水位监测', '水位预警', 'WARN-WATER-LEVEL-TEST',
-     3.80, 0.50, 3.00, 1, 3, 25.00,
-     now() - interval '120 minutes', now() - interval '85 minutes', '测试用户', '现场处理完成',
-     '供水监测报警测试数据', now() - interval '120 minutes', now() - interval '85 minutes'),
-    -- 未处理:压力1级(今日,一张图最高级点位)
-    ('test-alarm-004', 'TEST-PR-001', '测试压力监测点1', '压力监测', '压力预警', 'WARN-PRESSURE-TEST',
-     0.95, 0.40, 0.80, 0, 1, 30.00,
-     now() - interval '100 minutes', NULL, NULL, NULL, '供水监测报警测试数据',
-     now() - interval '100 minutes', now() - interval '100 minutes'),
-    -- 已处理:压力2级(今日,响应30分钟)
-    ('test-alarm-005', 'TEST-PR-001', '测试压力监测点1', '压力监测', '压力预警', 'WARN-PRESSURE-TEST',
-     0.90, 0.40, 0.80, 1, 2, 28.00,
-     now() - interval '90 minutes', now() - interval '60 minutes', '测试用户', '压力调整',
-     '供水监测报警测试数据', now() - interval '90 minutes', now() - interval '60 minutes'),
-    -- 已处理:流量4级(昨日,不计入今日统计)
-    ('test-alarm-006', 'TEST-FL-001', '测试流量监测点1', '流量监测', '流量预警', 'WARN-FLOW-TEST',
-     120.00, 10.00, 100.00, 1, 4, 20.00,
-     now() - interval '1 day' - interval '60 minutes', now() - interval '1 day',
-     '测试用户', '流量恢复正常', '供水监测报警测试数据',
-     now() - interval '1 day' - interval '60 minutes', now() - interval '1 day'),
-    -- 未处理:流量3级(昨日,用于实时列表/历史过滤/解除测试)
-    ('test-alarm-007', 'TEST-FL-001', '测试流量监测点1', '流量监测', '流量预警', 'WARN-FLOW-TEST',
-     115.00, 10.00, 100.00, 0, 3, 15.00,
-     now() - interval '1 day' - interval '30 minutes', NULL, NULL, NULL, '供水监测报警测试数据',
-     now() - interval '1 day' - interval '30 minutes', now() - interval '1 day' - interval '30 minutes'),
-    -- 已处理:水位2级(今日,响应10分钟)
-    ('test-alarm-008', 'TEST-WL-002', '测试水位监测点2', '水位监测', '水位预警', 'WARN-WATER-LEVEL-TEST',
-     3.50, 0.50, 3.00, 1, 2, 18.00,
-     now() - interval '300 minutes', now() - interval '290 minutes', '测试用户', '水位回落',
-     '供水监测报警测试数据', now() - interval '300 minutes', now() - interval '290 minutes');
+    ('test-alarm-001', 'TEST-WL-001', '水位预警', 'WARN-WATER-LEVEL-TEST', 0.20, 0.50, 3.00, 0, 1, 40.00, now() - interval '180 minutes', '供水监测报警测试数据', now() - interval '180 minutes', now() - interval '180 minutes'),
+    ('test-alarm-002', 'TEST-WL-001', '水位预警', 'WARN-WATER-LEVEL-TEST', 0.30, 0.50, 3.00, 0, 2, 35.00, now() - interval '150 minutes', '供水监测报警测试数据', now() - interval '150 minutes', now() - interval '150 minutes'),
+    ('test-alarm-003', 'TEST-WL-002', '水位预警', 'WARN-WATER-LEVEL-TEST', 3.80, 0.50, 3.00, 1, 3, 25.00, now() - interval '120 minutes', '供水监测报警测试数据', now() - interval '120 minutes', now() - interval '85 minutes'),
+    ('test-alarm-004', 'TEST-PR-001', '压力预警', 'WARN-PRESSURE-TEST', 0.95, 0.40, 0.80, 0, 1, 30.00, now() - interval '100 minutes', '供水监测报警测试数据', now() - interval '100 minutes', now() - interval '100 minutes'),
+    ('test-alarm-005', 'TEST-PR-001', '压力预警', 'WARN-PRESSURE-TEST', 0.90, 0.40, 0.80, 1, 2, 28.00, now() - interval '90 minutes', '供水监测报警测试数据', now() - interval '90 minutes', now() - interval '60 minutes'),
+    ('test-alarm-006', 'TEST-FL-001', '流量预警', 'WARN-FLOW-TEST', 120.00, 10.00, 100.00, 1, 4, 20.00, now() - interval '1 day' - interval '60 minutes', '供水监测报警测试数据', now() - interval '1 day' - interval '60 minutes', now() - interval '1 day'),
+    ('test-alarm-007', 'TEST-FL-001', '流量预警', 'WARN-FLOW-TEST', 115.00, 10.00, 100.00, 0, 3, 15.00, now() - interval '1 day' - interval '30 minutes', '供水监测报警测试数据', now() - interval '1 day' - interval '30 minutes', now() - interval '1 day' - interval '30 minutes'),
+    ('test-alarm-008', 'TEST-WL-002', '水位预警', 'WARN-WATER-LEVEL-TEST', 3.50, 0.50, 3.00, 1, 2, 18.00, now() - interval '300 minutes', '供水监测报警测试数据', now() - interval '300 minutes', now() - interval '290 minutes');
 
 -- ------------------------------------------------------------
 -- 3. 阈值数据(warning_threshold,含分时阈值)

+ 17 - 51
pipe-network-service/sql/water_supply_monitoring_alarm_rebuild_test_data.sql

@@ -326,58 +326,24 @@ VALUES
 -- 9. 报警流水(16 条:一级4/二级5/三级3/四级4;待审核8/已转工单6/已解除2)
 -- ------------------------------------------------------------
 INSERT INTO app_user.alarm_data
-    (id, device_name, device_code, device_type, warning_type, warning_code,
-     actual_value, min_value, max_value, alarm_status, alarm_level, deviation_ratio,
-     alarm_time, handle_time, handle_user, handle_remark, remark, create_time, update_time)
+    (id, device_code, warning_type, warning_code, actual_value, min_value, max_value, alarm_status, alarm_level, deviation_ratio, alarm_time, remark, create_time, update_time)
 VALUES
-    ('wrb-alarm-01', '城南主轴流量监测点', 'CN-GS-202609160201', '流量监测', '流量预警', 'WRB-WARN-FLOW',
-     132.00, 10.00, 100.00, 0, 1, 32.00, now() - interval '35 minutes', NULL, NULL, NULL,
-     '#56已转工单-WO-01 待派单', now(), now()),
-    ('wrb-alarm-02', '城北出厂流量监测点', 'CB-GS-202609160202', '流量监测', '流量预警', 'WRB-WARN-FLOW',
-     128.00, 8.00, 120.00, 0, 2, 6.67, now() - interval '3 hours', NULL, NULL, NULL,
-     '#56已转工单-WO-02 待接单', now(), now()),
-    ('wrb-alarm-03', '太常厂区流量监测点', 'TC-GS-202609160203', '流量监测', '流量预警', 'WRB-WARN-FLOW',
-     145.00, 10.00, 100.00, 0, 3, 45.00, now() - interval '1 day 2 hours', NULL, NULL, NULL,
-     '#56已转工单-WO-03 已接单', now(), now()),
-    ('wrb-alarm-04', '城南支线流量监测点', 'CN-GS-202609160204', '流量监测', '流量预警', 'WRB-WARN-FLOW',
-     118.00, 10.00, 100.00, 0, 4, 18.00, now() - interval '2 days', NULL, NULL, NULL,
-     '#56已转工单-WO-04 处理中', now(), now()),
-    ('wrb-alarm-05', '城南加压站压力监测点', 'CN-GS-202609160211', '压力监测', '压力预警', 'WRB-WARN-PRESSURE',
-     0.95, 0.40, 0.80, 0, 1, 18.75, now() - interval '3 days', NULL, NULL, NULL,
-     '#56已转工单-WO-05 待验收', now(), now()),
-    ('wrb-alarm-06', '城北末梢压力监测点', 'CB-GS-202609160212', '压力监测', '压力预警', 'WRB-WARN-PRESSURE',
-     0.30, 0.35, 0.85, 0, 2, 14.29, now() - interval '4 days', NULL, NULL, NULL,
-     '#56已转工单(WO-06 已结案);终态不占用唯一性名额,可再次转工单', now(), now()),
-    ('wrb-alarm-07', '城南漏失声波监测点', 'CN-GS-202609160221', '漏失监测', '漏失预警', 'WRB-WARN-LEAK',
-     7.20, 0.00, 5.00, 0, 3, 44.00, now() - interval '5 days', NULL, NULL, NULL,
-     '#56已转工单-WO-07 延期中', now(), now()),
-    ('wrb-alarm-08', '城北漏失声波监测点', 'CB-GS-202609160222', '漏失监测', '漏失预警', 'WRB-WARN-LEAK',
-     6.10, 0.00, 6.00, 0, 4, 1.67, now() - interval '6 days', NULL, NULL, NULL,
-     '#56待审核(WO-08 已取消,口径回落到报警事实);终态不占用唯一性名额', now(), now()),
-    ('wrb-alarm-09', '太常主管压力监测点', 'TC-GS-202609160213', '压力监测', '压力预警', 'WRB-WARN-PRESSURE',
-     0.28, 0.40, 0.80, 0, 1, 30.00, now() - interval '50 minutes', NULL, NULL, NULL,
-     '#56待审核-无工单,可直接转工单', now(), now()),
-    ('wrb-alarm-10', '城南小区入口压力监测点', 'CN-GS-202609160214', '压力监测', '压力预警', 'WRB-WARN-PRESSURE',
-     0.22, 0.40, 0.80, 0, 3, 45.00, now() - interval '5 hours', NULL, NULL, NULL,
-     '#56待审核-无工单,可直接转工单', now(), now()),
-    ('wrb-alarm-11', '太常漏失声波监测点', 'TC-GS-202609160223', '漏失监测', '漏失预警', 'WRB-WARN-LEAK',
-     9.40, 0.00, 5.00, 0, 2, 88.00, now() - interval '1 day 6 hours', NULL, NULL, NULL,
-     '#56待审核-无工单,可直接转工单', now(), now()),
-    ('wrb-alarm-12', '城北支线流量监测点', 'CB-GS-202609160205', '流量监测', '流量预警', 'WRB-WARN-FLOW',
-     155.00, 8.00, 120.00, 0, 4, 29.17, now() - interval '2 days 4 hours', NULL, NULL, NULL,
-     '#56待审核-无工单,可直接转工单', now(), now()),
-    ('wrb-alarm-13', '太常支线流量监测点', 'TC-GS-202609160206', '流量监测', '流量预警', 'WRB-WARN-FLOW',
-     112.00, 10.00, 100.00, 0, 2, 12.00, now() - interval '3 days 3 hours', NULL, NULL, NULL,
-     '#56待审核-设备无状态记录,报警状态走回退分支', now(), now()),
-    ('wrb-alarm-14', '城北高位水池压力监测点', 'CB-GS-202609160215', '压力监测', '压力预警', 'WRB-WARN-PRESSURE',
-     0.20, 0.40, 0.80, 0, 4, 50.00, now() - interval '4 days 5 hours', NULL, NULL, NULL,
-     '#56已解除-误报(审核结论=2,不产生工单)', now(), now()),
-    ('wrb-alarm-15', '城南老城区漏失监测点', 'CN-GS-202609160224', '漏失监测', '漏失预警', 'WRB-WARN-LEAK',
-     8.10, 0.00, 5.00, 1, 1, 62.00, now() - interval '6 days 2 hours',
-     now() - interval '6 days 1 hour', '刘昊林', '#56已解除-现场确认后人工处置', '#56已解除-报警记录已处理', now(), now()),
-    ('wrb-alarm-16', '城南主轴流量监测点', 'CN-GS-202609160201', '流量监测', '流量预警', 'WRB-WARN-FLOW',
-     124.00, 10.00, 100.00, 0, 2, 24.00, now() - interval '90 minutes', NULL, NULL, NULL,
-     '#56待审核-同设备同类型已有 WO-01 未办结,转工单应被阻止', now(), now());
+    ('wrb-alarm-01', 'CN-GS-202609160201', '流量预警', 'WRB-WARN-FLOW', 132.00, 10.00, 100.00, 0, 1, 32.00, now() - interval '35 minutes', '#56已转工单-WO-01 待派单', now(), now()),
+    ('wrb-alarm-02', 'CB-GS-202609160202', '流量预警', 'WRB-WARN-FLOW', 128.00, 8.00, 120.00, 0, 2, 6.67, now() - interval '3 hours', '#56已转工单-WO-02 待接单', now(), now()),
+    ('wrb-alarm-03', 'TC-GS-202609160203', '流量预警', 'WRB-WARN-FLOW', 145.00, 10.00, 100.00, 0, 3, 45.00, now() - interval '1 day 2 hours', '#56已转工单-WO-03 已接单', now(), now()),
+    ('wrb-alarm-04', 'CN-GS-202609160204', '流量预警', 'WRB-WARN-FLOW', 118.00, 10.00, 100.00, 0, 4, 18.00, now() - interval '2 days', '#56已转工单-WO-04 处理中', now(), now()),
+    ('wrb-alarm-05', 'CN-GS-202609160211', '压力预警', 'WRB-WARN-PRESSURE', 0.95, 0.40, 0.80, 0, 1, 18.75, now() - interval '3 days', '#56已转工单-WO-05 待验收', now(), now()),
+    ('wrb-alarm-06', 'CB-GS-202609160212', '压力预警', 'WRB-WARN-PRESSURE', 0.30, 0.35, 0.85, 0, 2, 14.29, now() - interval '4 days', '#56已转工单(WO-06 已结案);终态不占用唯一性名额,可再次转工单', now(), now()),
+    ('wrb-alarm-07', 'CN-GS-202609160221', '漏失预警', 'WRB-WARN-LEAK', 7.20, 0.00, 5.00, 0, 3, 44.00, now() - interval '5 days', '#56已转工单-WO-07 延期中', now(), now()),
+    ('wrb-alarm-08', 'CB-GS-202609160222', '漏失预警', 'WRB-WARN-LEAK', 6.10, 0.00, 6.00, 0, 4, 1.67, now() - interval '6 days', '#56待审核(WO-08 已取消,口径回落到报警事实);终态不占用唯一性名额', now(), now()),
+    ('wrb-alarm-09', 'TC-GS-202609160213', '压力预警', 'WRB-WARN-PRESSURE', 0.28, 0.40, 0.80, 0, 1, 30.00, now() - interval '50 minutes', '#56待审核-无工单,可直接转工单', now(), now()),
+    ('wrb-alarm-10', 'CN-GS-202609160214', '压力预警', 'WRB-WARN-PRESSURE', 0.22, 0.40, 0.80, 0, 3, 45.00, now() - interval '5 hours', '#56待审核-无工单,可直接转工单', now(), now()),
+    ('wrb-alarm-11', 'TC-GS-202609160223', '漏失预警', 'WRB-WARN-LEAK', 9.40, 0.00, 5.00, 0, 2, 88.00, now() - interval '1 day 6 hours', '#56待审核-无工单,可直接转工单', now(), now()),
+    ('wrb-alarm-12', 'CB-GS-202609160205', '流量预警', 'WRB-WARN-FLOW', 155.00, 8.00, 120.00, 0, 4, 29.17, now() - interval '2 days 4 hours', '#56待审核-无工单,可直接转工单', now(), now()),
+    ('wrb-alarm-13', 'TC-GS-202609160206', '流量预警', 'WRB-WARN-FLOW', 112.00, 10.00, 100.00, 0, 2, 12.00, now() - interval '3 days 3 hours', '#56待审核-设备无状态记录,报警状态走回退分支', now(), now()),
+    ('wrb-alarm-14', 'CB-GS-202609160215', '压力预警', 'WRB-WARN-PRESSURE', 0.20, 0.40, 0.80, 0, 4, 50.00, now() - interval '4 days 5 hours', '#56已解除-误报(审核结论=2,不产生工单)', now(), now()),
+    ('wrb-alarm-15', 'CN-GS-202609160224', '漏失预警', 'WRB-WARN-LEAK', 8.10, 0.00, 5.00, 1, 1, 62.00, now() - interval '6 days 2 hours', '#56已解除-报警记录已处理', now(), now()),
+    ('wrb-alarm-16', 'CN-GS-202609160201', '流量预警', 'WRB-WARN-FLOW', 124.00, 10.00, 100.00, 0, 2, 24.00, now() - interval '90 minutes', '#56待审核-同设备同类型已有 WO-01 未办结,转工单应被阻止', now(), now());
 
 -- ------------------------------------------------------------
 -- 10. 工单(0..7 各一张;device_id 必须是供水设备 equipment_id,否则审核计数条取不到)

+ 1 - 6
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/alarm/AlarmController.java

@@ -114,17 +114,12 @@ public class AlarmController {
     @PutMapping("/handle/{alarmId}")
     @ApiOperation("处理报警")
     @Log(title = "处理报警", businessType = BusinessType.UPDATE)
-    public AjaxResult handle(@PathVariable("alarmId") String alarmId,
-                              @RequestParam("handleUser") String handleUser,
-                              @RequestParam(value = "handleRemark", required = false) String handleRemark) {
+    public AjaxResult handle(@PathVariable("alarmId") String alarmId) {
         AlarmData entity = service.getById(alarmId);
         if (entity == null) {
             return AjaxResult.error("报警信息不存在");
         }
         entity.setAlarmStatus(AlarmRecordStatusEnum.HANDLED.getCode());
-        entity.setHandleTime(LocalDateTime.now());
-        entity.setHandleUser(handleUser);
-        entity.setHandleRemark(handleRemark);
         entity.setUpdateTime(LocalDateTime.now());
         boolean updated = service.updateById(entity);
         return updated ? AjaxResult.success("处理成功") : AjaxResult.error("处理失败");

+ 0 - 1
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/base/alarm/AlarmDataController.java

@@ -67,7 +67,6 @@ public class AlarmDataController {
     @ApiOperation(value = "处理报警")
     public AjaxResult handle(@RequestBody AlarmData entity) {
         entity.setAlarmStatus(1);
-        entity.setHandleTime(LocalDateTime.now());
         entity.setUpdateTime(LocalDateTime.now());
         return service.updateById(entity) ? AjaxResult.success(entity) : AjaxResult.error("处理失败");
     }

+ 3 - 8
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/gasbasic/GasAlarmController.java

@@ -77,9 +77,7 @@ public class GasAlarmController {
         m.put("alarmTime", a.getAlarmTime() != null ? a.getAlarmTime().format(FMT) : "");
         m.put("alarmStatus", a.getAlarmStatus());
         m.put("statusText", a.getAlarmStatus() != null && a.getAlarmStatus() == 0 ? "待处理" : "已处理");
-        m.put("resolveTime", a.getHandleTime() != null ? a.getHandleTime().format(FMT) : null);
-        m.put("handler", a.getHandleUser());
-        m.put("remark", a.getHandleRemark());
+        m.put("remark", a.getRemark());
         m.put("hasWorkOrder", order != null);
         m.put("workOrderId", order == null ? null : order.getOrderId());
         m.put("workOrderStatus", order == null ? null : order.getOrderStatus());
@@ -206,14 +204,11 @@ public class GasAlarmController {
     @PutMapping("/resolve/{id}")
     @ApiOperation("标记已处理")
     @Anonymous
-    public AjaxResult resolve(@PathVariable String id,
-                              @RequestParam(required = false) String handleRemark) {
+    public AjaxResult resolve(@PathVariable String id) {
         AlarmData e = alarmDataService.getById(id);
         if (e == null) return AjaxResult.error("记录不存在");
         e.setAlarmStatus(1);
-        e.setHandleTime(java.time.LocalDateTime.now());
-        e.setHandleUser("当前用户");
-        e.setHandleRemark(handleRemark == null || handleRemark.trim().isEmpty() ? "标记已处理" : handleRemark.trim());
+        e.setUpdateTime(java.time.LocalDateTime.now());
         alarmDataService.updateById(e);
         return AjaxResult.success("已处理");
     }

+ 4 - 6
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/gasbasic/PressureAlarmController.java

@@ -73,8 +73,7 @@ public class PressureAlarmController {
         m.put("alarmTime", a.getAlarmTime() != null ? a.getAlarmTime().format(FMT) : "");
         m.put("alarmStatus", a.getAlarmStatus());
         m.put("statusText", a.getAlarmStatus() != null && a.getAlarmStatus() == 0 ? "待处理" : "已处理");
-        m.put("resolveTime", a.getHandleTime() != null ? a.getHandleTime().format(FMT) : null);
-        m.put("handler", a.getHandleUser()); m.put("remark", a.getHandleRemark());
+        m.put("remark", a.getRemark());
         m.put("hasWorkOrder", order != null);
         m.put("workOrderId", order == null ? null : order.getOrderId());
         m.put("workOrderStatus", order == null ? null : order.getOrderStatus());
@@ -183,12 +182,11 @@ public class PressureAlarmController {
     }
 
     @PutMapping("/resolve/{id}") @ApiOperation("标记已处理") @Anonymous
-    public AjaxResult resolve(@PathVariable String id,
-                              @RequestParam(required = false) String handleRemark) {
+    public AjaxResult resolve(@PathVariable String id) {
         AlarmData e = alarmDataService.getById(id);
         if (e == null) return AjaxResult.error("记录不存在");
-        e.setAlarmStatus(1); e.setHandleTime(java.time.LocalDateTime.now()); e.setHandleUser("当前用户");
-        e.setHandleRemark(handleRemark == null || handleRemark.trim().isEmpty() ? "标记已处理" : handleRemark.trim());
+        e.setAlarmStatus(1);
+        e.setUpdateTime(java.time.LocalDateTime.now());
         alarmDataService.updateById(e);
         return AjaxResult.success("已处理");
     }

+ 1 - 1
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/dto/out/AlarmStatsOutDTO.java

@@ -30,6 +30,6 @@ public class AlarmStatsOutDTO implements Serializable {
     @ApiModelProperty(value = "设备异常率(百分比,保留两位小数)", example = "2.50")
     private String deviceAbnormalRate;
 
-    @ApiModelProperty(value = "平均响应时间(分钟,今日已处理报警 处理时间-报警时间 均值)", example = "35.20")
+    @ApiModelProperty(value = "平均响应时间(分钟,取今日首次处理日志时间与报警时间之差;无有效日志为null)", example = "35.20")
     private String avgResponseMinutes;
 }

+ 0 - 10
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/dto/out/WaterAlarmOutDTO.java

@@ -88,16 +88,6 @@ public class WaterAlarmOutDTO implements Serializable {
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
     private LocalDateTime alarmTime;
 
-    @ApiModelProperty(value = "处理时间")
-    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
-    private LocalDateTime handleTime;
-
-    @ApiModelProperty(value = "处理人")
-    private String handleUser;
-
-    @ApiModelProperty(value = "处理备注")
-    private String handleRemark;
-
     @ApiModelProperty(value = "备注")
     private String remark;
 

+ 19 - 17
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/service/impl/WaterSupplyAlarmServiceImpl.java

@@ -55,6 +55,9 @@ public class WaterSupplyAlarmServiceImpl implements IWaterSupplyAlarmService {
     @Autowired
     private AlarmDataService alarmDataService;
 
+    @Autowired
+    private com.zksy.base.alarm.mapper.AlarmDataMapper alarmDataMapper;
+
 
     @Autowired
     private WaterSupplyQueryService waterSupplyQueryService;
@@ -93,6 +96,14 @@ public class WaterSupplyAlarmServiceImpl implements IWaterSupplyAlarmService {
         return waterSupplyQueryService.equipmentByCodes(deviceCodes);
     }
 
+    private Map<Integer, String> loadDeviceTypeNames(Collection<EquipmentBase> devices) {
+        Set<Integer> typeIds = devices.stream().map(EquipmentBase::getEquipmentTypeId)
+                .filter(Objects::nonNull).collect(Collectors.toSet());
+        return typeIds.isEmpty() ? Collections.emptyMap()
+                : equipmentTypeMapper.selectBatchIds(typeIds).stream().collect(Collectors.toMap(
+                        EquipmentType::getId, EquipmentType::getTypeName, (a, b) -> a));
+    }
+
     /**
      * 报警ID -> 工单映射(通过 alarm_data.order_id 关联)
      */
@@ -111,7 +122,7 @@ public class WaterSupplyAlarmServiceImpl implements IWaterSupplyAlarmService {
         stats.setTotalDeviceCount(waterCodes.size());
         if (waterCodes.isEmpty()) {
             stats.setDeviceAbnormalRate("0.00");
-            stats.setAvgResponseMinutes("0.00");
+            stats.setAvgResponseMinutes(null);
             return stats;
         }
 
@@ -135,22 +146,9 @@ public class WaterSupplyAlarmServiceImpl implements IWaterSupplyAlarmService {
                 .ge(AlarmData::getAlarmTime, todayStart));
         stats.setTodayAlarmCount(todayCount);
 
-        // 平均响应时间:今日已处理报警(处理时间-报警时间)均值,单位分钟
-        List<AlarmData> handledToday = alarmDataService.list(new LambdaQueryWrapper<AlarmData>()
-                .in(AlarmData::getDeviceCode, waterCodes)
-                .eq(AlarmData::getAlarmStatus, 1)
-                .ge(AlarmData::getHandleTime, todayStart));
-        double totalSeconds = 0;
-        int count = 0;
-        for (AlarmData alarm : handledToday) {
-            if (alarm.getAlarmTime() == null || alarm.getHandleTime() == null) {
-                continue;
-            }
-            totalSeconds += alarm.getHandleTime().toEpochSecond(java.time.ZoneOffset.ofHours(8))
-                    - alarm.getAlarmTime().toEpochSecond(java.time.ZoneOffset.ofHours(8));
-            count++;
-        }
-        stats.setAvgResponseMinutes(count == 0 ? "0.00" : String.format("%.2f", totalSeconds / 60.0d / count));
+        // 已移除告警表处理时间,响应时长以实际处理日志为准。
+        java.math.BigDecimal average = alarmDataMapper.selectAverageResponseMinutes(waterCodes, todayStart);
+        stats.setAvgResponseMinutes(average == null ? null : average.setScale(2, java.math.RoundingMode.HALF_UP).toPlainString());
         return stats;
     }
 
@@ -286,12 +284,14 @@ public class WaterSupplyAlarmServiceImpl implements IWaterSupplyAlarmService {
                 .collect(Collectors.toSet()));
         Map<String, WorkOrder> orderMap = loadWorkOrderMapByAlarmIds(alarmIds);
         Map<String, EquipmentStatus> statusMap = loadStatusMap(eqMap.values());
+        Map<Integer, String> typeNames = loadDeviceTypeNames(eqMap.values());
         List<WaterAlarmOutDTO> vos = new ArrayList<>();
         for (AlarmData alarm : alarms) {
             WaterAlarmOutDTO vo = BeanUtil.copyProperties(alarm, WaterAlarmOutDTO.class);
             EquipmentBase eq = alarm.getDeviceCode() == null ? null : eqMap.get(alarm.getDeviceCode());
             if (eq != null) {
                 vo.setEquipmentId(eq.getEquipmentId());
+                vo.setDeviceType(typeNames.get(eq.getEquipmentTypeId()));
                 if (StrUtil.isBlank(vo.getDeviceName())) {
                     vo.setDeviceName(eq.getEquipmentName());
                 }
@@ -523,6 +523,7 @@ public class WaterSupplyAlarmServiceImpl implements IWaterSupplyAlarmService {
         }
         Map<String, EquipmentBase> eqMap = loadEquipmentCodeMap(alarms.stream()
                 .map(AlarmData::getDeviceCode).collect(Collectors.toSet()));
+        Map<Integer, String> typeNames = loadDeviceTypeNames(eqMap.values());
         List<String> alarmIds = alarms.stream().map(AlarmData::getId).collect(Collectors.toList());
         Map<String, WorkOrder> orderMap = loadWorkOrderMapByAlarmIds(alarmIds);
         List<WaterAlarmOutDTO> vos = new ArrayList<>();
@@ -531,6 +532,7 @@ public class WaterSupplyAlarmServiceImpl implements IWaterSupplyAlarmService {
             EquipmentBase eq = alarm.getDeviceCode() == null ? null : eqMap.get(alarm.getDeviceCode());
             if (eq != null) {
                 vo.setEquipmentId(eq.getEquipmentId());
+                vo.setDeviceType(typeNames.get(eq.getEquipmentTypeId()));
                 if (StrUtil.isBlank(vo.getDeviceName())) {
                     vo.setDeviceName(eq.getEquipmentName());
                 }

+ 0 - 12
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/export/WaterAlarmLedgerExportDTO.java

@@ -58,15 +58,6 @@ public class WaterAlarmLedgerExportDTO {
     @Excel(name = "报警时间", dateFormat = "yyyy-MM-dd HH:mm:ss")
     private LocalDateTime alarmTime;
 
-    @Excel(name = "处理时间", dateFormat = "yyyy-MM-dd HH:mm:ss")
-    private LocalDateTime handleTime;
-
-    @Excel(name = "处理人")
-    private String handleUser;
-
-    @Excel(name = "处理备注")
-    private String handleRemark;
-
     public static WaterAlarmLedgerExportDTO from(WaterAlarmOutDTO value) {
         WaterAlarmLedgerExportDTO dto = new WaterAlarmLedgerExportDTO();
         dto.warningCode = value.getWarningCode();
@@ -83,9 +74,6 @@ public class WaterAlarmLedgerExportDTO {
         dto.deviationRatio = value.getDeviationRatio();
         dto.equipmentLocation = value.getEquipmentLocation();
         dto.alarmTime = value.getAlarmTime();
-        dto.handleTime = value.getHandleTime();
-        dto.handleUser = value.getHandleUser();
-        dto.handleRemark = value.getHandleRemark();
         return dto;
     }
 }

+ 4 - 17
pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/domain/AlarmData.java

@@ -30,12 +30,12 @@ public class AlarmData implements Serializable {
     @ApiModelProperty(value = "关联工单ID")
     private Long orderId;
 
-    @TableField(value = "device_name")
-    @ApiModelProperty(value = "设备名称")
+    @TableField(exist = false)
+    @ApiModelProperty(value = "设备名称,由设备台账查询补充")
     private String deviceName;
 
-    @TableField(value = "device_type")
-    @ApiModelProperty(value = "设备类型")
+    @TableField(exist = false)
+    @ApiModelProperty(value = "设备类型,由设备台账查询补充")
     private String deviceType;
 
     @TableField(value = "warning_type")
@@ -75,19 +75,6 @@ public class AlarmData implements Serializable {
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
     private LocalDateTime alarmTime;
 
-    @TableField(value = "handle_time")
-    @ApiModelProperty(value = "处理时间")
-    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
-    private LocalDateTime handleTime;
-
-    @TableField(value = "handle_user")
-    @ApiModelProperty(value = "处理人")
-    private String handleUser;
-
-    @TableField(value = "handle_remark")
-    @ApiModelProperty(value = "处理备注")
-    private String handleRemark;
-
     @TableField(value = "remark")
     @ApiModelProperty(value = "备注")
     private String remark;

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

@@ -12,6 +12,10 @@ import java.util.List;
 @Mapper
 public interface AlarmDataMapper extends BaseMapper<AlarmData> {
 
+    /** 平均响应时间取已处理操作日志,不依赖告警表已移除的处理时间字段。 */
+    java.math.BigDecimal selectAverageResponseMinutes(@Param("deviceCodes") java.util.Collection<String> deviceCodes,
+                                                      @Param("startTime") java.time.LocalDateTime startTime);
+
     IPage<AlarmData> selectLatestPage(Page<AlarmData> page,
                                       @Param("deviceCodes") List<String> deviceCodes,
                                       @Param("deviceCode") String deviceCode,

+ 20 - 2
pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/service/impl/AlarmDataServiceImpl.java

@@ -17,6 +17,7 @@ import com.zksy.ops.domain.WorkOrder;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
+import java.io.Serializable;
 import java.time.LocalDateTime;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -67,6 +68,15 @@ public class AlarmDataServiceImpl extends ServiceImpl<AlarmDataMapper, AlarmData
         return result;
     }
 
+    @Override
+    public AlarmData getById(Serializable id) {
+        AlarmData alarm = super.getById(id);
+        if (alarm != null) {
+            fillDeviceInfo(Collections.singletonList(alarm));
+        }
+        return alarm;
+    }
+
     private List<String> deviceCodesForQuery(String equipmentType) {
         if (!"drainage".equals(equipmentType)) {
             return null;
@@ -188,9 +198,14 @@ public class AlarmDataServiceImpl extends ServiceImpl<AlarmDataMapper, AlarmData
         // 批量查询 equipment_base
         LambdaQueryWrapper<EquipmentBase> eqWrapper = new LambdaQueryWrapper<>();
         eqWrapper.in(EquipmentBase::getEquipmentCode, codes)
-                 .select(EquipmentBase::getEquipmentCode, EquipmentBase::getEquipmentName,
+                 .select(EquipmentBase::getEquipmentCode, EquipmentBase::getEquipmentName, EquipmentBase::getEquipmentTypeId,
                          EquipmentBase::getLongitude, EquipmentBase::getLatitude);
         List<EquipmentBase> equipmentList = equipmentBaseMapper.selectList(eqWrapper);
+        Set<Integer> typeIds = equipmentList.stream().map(EquipmentBase::getEquipmentTypeId)
+                .filter(Objects::nonNull).collect(Collectors.toSet());
+        Map<Integer, String> typeNames = typeIds.isEmpty() ? Collections.emptyMap()
+                : equipmentTypeMapper.selectBatchIds(typeIds).stream().collect(Collectors.toMap(
+                        EquipmentType::getId, EquipmentType::getTypeName, (a, b) -> a));
         // 构建 code -> EquipmentBase 映射
         java.util.Map<String, EquipmentBase> codeEquipMap = equipmentList.stream()
                 .filter(e -> e.getEquipmentCode() != null)
@@ -200,6 +215,7 @@ public class AlarmDataServiceImpl extends ServiceImpl<AlarmDataMapper, AlarmData
             if (record.getDeviceCode() != null) {
                 EquipmentBase eq = codeEquipMap.get(record.getDeviceCode());
                 if (eq != null) {
+                    record.setDeviceType(typeNames.get(eq.getEquipmentTypeId()));
                     if (eq.getEquipmentName() != null) {
                         record.setDeviceName(eq.getEquipmentName());
                     }
@@ -224,7 +240,9 @@ public class AlarmDataServiceImpl extends ServiceImpl<AlarmDataMapper, AlarmData
                 .like(warningCode != null, AlarmData::getWarningCode, warningCode)
                 .eq(alarmStatus != null, AlarmData::getAlarmStatus, alarmStatus);
         queryWrapper.orderByDesc(AlarmData::getAlarmTime);
-        return this.list(queryWrapper);
+        List<AlarmData> records = this.list(queryWrapper);
+        fillDeviceInfo(records);
+        return records;
     }
 
     @Override

+ 16 - 0
pipe-network-service/zksy-system/src/main/resources/mapper/base/AlarmDataMapper.xml

@@ -2,6 +2,22 @@
 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <mapper namespace="com.zksy.base.alarm.mapper.AlarmDataMapper">
 
+    <select id="selectAverageResponseMinutes" resultType="java.math.BigDecimal">
+        SELECT AVG((EXTRACT(EPOCH FROM l.oper_time) - EXTRACT(EPOCH FROM a.alarm_time)) / 60.0)
+        FROM app_user.alarm_data a
+        JOIN (SELECT alarm_id, MIN(oper_time) AS oper_time FROM app_user.alarm_log
+              WHERE oper_type = 3 GROUP BY alarm_id) l ON l.alarm_id = a.id
+        WHERE a.alarm_status = 1 AND l.oper_time &gt;= #{startTime}
+          AND l.oper_time &gt;= a.alarm_time
+          <choose>
+              <when test="deviceCodes != null and deviceCodes.size() > 0">
+                  AND a.device_code IN
+                  <foreach collection="deviceCodes" item="code" open="(" separator="," close=")">#{code}</foreach>
+              </when>
+              <otherwise>AND 1 = 0</otherwise>
+          </choose>
+    </select>
+
     <select id="selectLatestPage" resultType="com.zksy.base.alarm.domain.AlarmData">
         SELECT a.*
         FROM alarm_data a

+ 5 - 2
pipe-network-service/zksy-system/src/main/resources/mapper/warning/AlarmWarningRelMapper.xml

@@ -5,9 +5,12 @@
     <select id="selectAlarmListByWarningId" resultType="java.util.Map">
         SELECT
             r.*,
-            a.device_name,
+            (SELECT e.equipment_name FROM app_user.equipment_base e
+             WHERE e.equipment_code = a.device_code ORDER BY e.equipment_id LIMIT 1) AS device_name,
             a.device_code,
-            a.device_type,
+            (SELECT t.type_name FROM app_user.equipment_base e
+             JOIN app_user.equipment_type t ON t.id::text = e.equipment_type_id::text
+             WHERE e.equipment_code = a.device_code ORDER BY e.equipment_id LIMIT 1) AS device_type,
             a.warning_type,
             a.warning_code,
             a.actual_value,

+ 1 - 1
pipe-network-service/zksy-system/src/test/java/com/zksy/ops/service/impl/WorkOrderServiceImplTest.java

@@ -132,7 +132,7 @@ class WorkOrderServiceImplTest {
         verify(alarmDataService).update(relation.capture(), any());
         assertEquals(99L, relation.getValue().getOrderId());
         assertNull(relation.getValue().getAlarmStatus());
-        assertNull(relation.getValue().getHandleTime());
+        assertNull(relation.getValue().getUpdateTime());
         assertNull(relation.getValue().getAuditStatus());
         org.mockito.Mockito.verifyNoInteractions(alarmClearService);
         verify(caseInfoMapper).update(isNull(), any());

+ 0 - 13
zk-api-service/src/main/java/com/zksy/api/domain/AlarmData.java

@@ -71,19 +71,6 @@ public class AlarmData implements Serializable {
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
     private LocalDateTime alarmTime;
 
-    @TableField(value = "handle_time")
-    @ApiModelProperty(value = "处理时间")
-    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
-    private LocalDateTime handleTime;
-
-    @TableField(value = "handle_user")
-    @ApiModelProperty(value = "处理人")
-    private String handleUser;
-
-    @TableField(value = "handle_remark")
-    @ApiModelProperty(value = "处理备注")
-    private String handleRemark;
-
     @TableField(value = "remark")
     @ApiModelProperty(value = "备注")
     private String remark;

+ 1 - 2
zk-api-service/src/main/java/com/zksy/api/mapper/AlarmDataMapper.java

@@ -28,8 +28,7 @@ public interface AlarmDataMapper extends BaseMapper<AlarmData> {
     int updateUnhandledAlarmLevel(@Param("id") String id, @Param("sample") AlarmData sample);
 
     /** 按指标恢复全部未处理记录(包括历史重复);保留告警发生时的等级、数值和工单关联。 */
-    @Update("UPDATE alarm_data SET alarm_status=1, handle_time=CURRENT_TIMESTAMP, handle_user='系统自动', "
-            + "handle_remark='采集数据已解除告警条件,系统自动处理', update_time=CURRENT_TIMESTAMP "
+    @Update("UPDATE alarm_data SET alarm_status=1, update_time=CURRENT_TIMESTAMP "
             + "WHERE device_code=#{deviceCode} AND warning_code=#{warningCode} AND alarm_status=0")
     @Options(timeout = 5)
     int markUnhandledAlarmsRecovered(@Param("deviceCode") String deviceCode, @Param("warningCode") String warningCode);

+ 93 - 0
zk-api-service/src/main/java/com/zksy/api/service/CollectionAlarmWriter.java

@@ -0,0 +1,93 @@
+package com.zksy.api.service;
+
+import com.zksy.api.domain.AlarmData;
+import com.zksy.api.mapper.AlarmDataMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.PlatformTransactionManager;
+import org.springframework.transaction.TransactionDefinition;
+import org.springframework.transaction.support.TransactionTemplate;
+
+import java.time.LocalDateTime;
+import java.util.Objects;
+
+/** 采集告警及离线告警的统一入库规则;告警处理状态与设备实时报警状态互相独立。 */
+@Slf4j
+@Service
+public class CollectionAlarmWriter {
+    private final AlarmDataMapper mapper;
+    private final TransactionTemplate transaction;
+
+    public CollectionAlarmWriter(AlarmDataMapper mapper, PlatformTransactionManager transactionManager) {
+        this.mapper = mapper;
+        transaction = new TransactionTemplate(transactionManager);
+        transaction.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
+        transaction.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
+        transaction.setTimeout(10);
+    }
+
+    /**
+     * 已有未处理告警时,同级跳过,等级升降则更新原记录及对应采集值。
+     * @return true表示新增,false表示复用原记录(更新或跳过);真实入库失败抛出异常。
+     */
+    public boolean saveOrUpdateUnhandled(AlarmData alarm) {
+        if (alarm == null) throw new IllegalArgumentException("告警不能为空");
+        validateKey(alarm.getDeviceCode(), alarm.getWarningCode());
+        alarm.setWarningType(alarmLabel(alarm.getWarningType()));
+        alarm.setRemark(alarmLabel(alarm.getRemark()));
+        if (alarm.getAlarmTime() == null) alarm.setAlarmTime(LocalDateTime.now());
+        if (alarm.getCreateTime() == null) alarm.setCreateTime(LocalDateTime.now());
+        if (alarm.getAlarmStatus() == null) alarm.setAlarmStatus(0);
+
+        return Boolean.TRUE.equals(transaction.execute(status -> {
+            lockKey(alarm.getDeviceCode(), alarm.getWarningCode());
+            AlarmData existing = mapper.selectUnhandledAlarmForUpdate(alarm.getDeviceCode(), alarm.getWarningCode());
+            if (existing != null) {
+                // 缺失等级的报文不能清除已有有效等级;真实采集端提供1-4级。
+                if (alarm.getAlarmLevel() != null && !Objects.equals(existing.getAlarmLevel(), alarm.getAlarmLevel())) {
+                    if (mapper.updateUnhandledAlarmLevel(existing.getId(), alarm) != 1) {
+                        throw new IllegalStateException("未处理告警等级更新未成功");
+                    }
+                    log.info("更新未处理告警等级: deviceCode={}, warningCode={}, alarmId={}, level={}->{}",
+                            alarm.getDeviceCode(), alarm.getWarningCode(), existing.getId(), existing.getAlarmLevel(), alarm.getAlarmLevel());
+                } else {
+                    log.debug("跳过同级未处理告警: deviceCode={}, warningCode={}, existingAlarmId={}",
+                            alarm.getDeviceCode(), alarm.getWarningCode(), existing.getId());
+                }
+                return false;
+            }
+            if (mapper.insert(alarm) != 1) throw new IllegalStateException("告警入库未成功");
+            return true;
+        }));
+    }
+
+    private String alarmLabel(String value) {
+        return value == null ? null : value.replace("预警", "告警");
+    }
+
+    /** 有效恢复数据才调用;0行表示无待处理告警,重复正常上报不会改写历史处理信息。 */
+    public int markRecovered(String deviceCode, String warningCode) {
+        validateKey(deviceCode, warningCode);
+        return transaction.execute(status -> {
+            lockKey(deviceCode, warningCode);
+            int count = mapper.markUnhandledAlarmsRecovered(deviceCode, warningCode);
+            if (count < 0) throw new IllegalStateException("告警恢复更新未成功");
+            if (count > 0) {
+                log.info("正常采集数据自动处理告警: deviceCode={}, warningCode={}, count={}", deviceCode, warningCode, count);
+            }
+            return count;
+        });
+    }
+
+    private void lockKey(String deviceCode, String warningCode) {
+        if (!Integer.valueOf(1).equals(mapper.lockAlarmKey(deviceCode, warningCode))) {
+            throw new IllegalStateException("未取得告警入库事务锁");
+        }
+    }
+
+    private void validateKey(String deviceCode, String warningCode) {
+        if (deviceCode == null || deviceCode.trim().isEmpty() || warningCode == null || warningCode.trim().isEmpty()) {
+            throw new IllegalArgumentException("告警必须包含device_code和warning_code");
+        }
+    }
+}

+ 7 - 9
zk-api-service/src/main/resources/mapper/AlarmDataMapper.xml

@@ -7,28 +7,26 @@
     <resultMap id="BaseResultMap" type="com.zksy.api.domain.AlarmData">
             <id property="id" column="id" jdbcType="VARCHAR"/>
             <result property="orderId" column="order_id" jdbcType="BIGINT"/>
-            <result property="deviceName" column="device_name" jdbcType="VARCHAR"/>
             <result property="deviceCode" column="device_code" jdbcType="VARCHAR"/>
-            <result property="deviceType" column="device_type" jdbcType="VARCHAR"/>
             <result property="warningType" column="warning_type" jdbcType="VARCHAR"/>
             <result property="warningCode" column="warning_code" jdbcType="VARCHAR"/>
-            <result property="warningValue" column="warning_value" jdbcType="DECIMAL"/>
+            <result property="minValue" column="min_value" jdbcType="DECIMAL"/>
+            <result property="maxValue" column="max_value" jdbcType="DECIMAL"/>
+            <result property="alarmLevel" column="alarm_level" jdbcType="INTEGER"/>
+            <result property="deviationRatio" column="deviation_ratio" jdbcType="DECIMAL"/>
+            <result property="auditStatus" column="audit_status" jdbcType="INTEGER"/>
             <result property="actualValue" column="actual_value" jdbcType="DECIMAL"/>
             <result property="alarmStatus" column="alarm_status" jdbcType="INTEGER"/>
             <result property="alarmTime" column="alarm_time" jdbcType="TIMESTAMP"/>
-            <result property="handleTime" column="handle_time" jdbcType="TIMESTAMP"/>
-            <result property="handleUser" column="handle_user" jdbcType="VARCHAR"/>
-            <result property="handleRemark" column="handle_remark" jdbcType="VARCHAR"/>
             <result property="remark" column="remark" jdbcType="VARCHAR"/>
             <result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
             <result property="updateTime" column="update_time" jdbcType="TIMESTAMP"/>
     </resultMap>
 
     <sql id="Base_Column_List">
-        id,order_id,device_name,device_code,device_type,
-        warning_type,warning_code,warning_value,
+        id,order_id,device_code,
+        warning_type,warning_code,min_value,max_value,alarm_level,deviation_ratio,audit_status,
         actual_value,alarm_status,alarm_time,
-        handle_time,handle_user,handle_remark,
         remark,create_time,update_time
     </sql>
 </mapper>

+ 4 - 10
报警功能测试说明.md

@@ -1,5 +1,8 @@
 # 报警功能测试说明
 
+> 历史测试说明。2026-09-24 已删除告警表的设备名称、设备类型及处理时间/人/备注列;
+> 当前迁移、验证与回滚见 `docs/alarm-column-removal-2026-09-24.md`。设备展示信息从台账关联获取。
+
 ## 一、数据库准备
 
 首先在数据库中执行 `alarm_data_table.sql` 文件,创建报警数据表:
@@ -8,18 +11,13 @@
 -- 报警数据表
 CREATE TABLE IF NOT EXISTS `alarm_data` (
   `id` varchar(64) NOT NULL COMMENT '主键',
-  `device_name` varchar(255) DEFAULT NULL COMMENT '设备名称',
   `device_code` varchar(255) DEFAULT NULL COMMENT '设备编码',
-  `device_type` varchar(100) DEFAULT NULL COMMENT '设备类型',
   `warning_type` varchar(100) DEFAULT NULL COMMENT '预警类型',
   `warning_code` varchar(100) DEFAULT NULL COMMENT '预警编码',
   `warning_value` decimal(10,2) DEFAULT NULL COMMENT '预警阈值',
   `actual_value` decimal(10,2) DEFAULT NULL COMMENT '实际值',
   `alarm_status` int(1) DEFAULT 0 COMMENT '报警状态 0-未处理 1-已处理',
   `alarm_time` datetime DEFAULT NULL COMMENT '报警时间',
-  `handle_time` datetime DEFAULT NULL COMMENT '处理时间',
-  `handle_user` varchar(255) DEFAULT NULL COMMENT '处理人',
-  `handle_remark` text COMMENT '处理备注',
   `remark` text COMMENT '备注',
   `create_time` datetime DEFAULT NULL COMMENT '创建时间',
   `update_time` datetime DEFAULT NULL COMMENT '更新时间',
@@ -67,9 +65,7 @@ POST /alarmTest/customAlarm
 Content-Type: application/json
 
 {
-  "deviceName": "自定义设备",
   "deviceCode": "CUSTOM-001",
-  "deviceType": "custom",
   "warningType": "自定义预警",
   "warningCode": "WARN-CUSTOM",
   "warningValue": 100,
@@ -93,9 +89,7 @@ POST /alarmData/handle
 Content-Type: application/json
 
 {
-  "id": "报警记录ID",
-  "handleUser": "处理人",
-  "handleRemark": "处理备注"
+  "id": "报警记录ID"
 }
 ```