ソースを参照

feat(audio): 添加漏失监测功能

- 在application-dev.yaml、application-druid.yaml、application-prod.yaml中添加漏失监测配置参数
- 修改AudioApplication.java,添加zk-api mapper扫描路径
- 扩展EarlyWarningAutoService接口,新增recordWarning方法用于主动记录预警
- 实现EarlyWarningAutoService的recordWarning方法,支持存在未完结预警时更新内容
- 添加NoiseLeakAlarmService服务类,实现漏失监测核心逻辑
- 在audio-service的MessageHandler中集成漏失监测评估功能
- 更新flammable-gas-service的MessageHandler,优化气体浓度阈值判断逻辑
- 修正water-level-service的MessageHandler中警告代码格式
- 添加zk-api-service依赖到audio-service的pom.xml
林仔 1 ヶ月 前
親
コミット
87f604bff7

+ 7 - 1
audio-service/pom.xml

@@ -1,4 +1,4 @@
-<?xml version="1.0" encoding="UTF-8"?>
+<?xml version="1.0" encoding="UTF-8"?>
 <project xmlns="http://maven.apache.org/POM/4.0.0"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
@@ -24,6 +24,12 @@
             <artifactId>zk-common</artifactId>
             <version>1.0.0</version>
         </dependency>
+        <!--zk-api 服务:提供告警(AlarmDataService)与预警(EarlyWarningAutoService)能力-->
+        <dependency>
+            <groupId>com.zksy</groupId>
+            <artifactId>zk-api-service</artifactId>
+            <version>1.0.0</version>
+        </dependency>
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-web</artifactId>

+ 1 - 1
audio-service/src/main/java/com/zksy/audio/AudioApplication.java

@@ -7,7 +7,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
 import org.springframework.scheduling.annotation.EnableScheduling;
 
 @EnableScheduling
-@MapperScan(basePackages = "com.zksy.audio.mapper")
+@MapperScan(basePackages = {"com.zksy.audio.mapper", "com.zksy.api.mapper"})
 @SpringBootApplication(scanBasePackages = {"com.zksy.audio","com.zksy.api"})
 public class AudioApplication {
     public static void main(String[] args) {

+ 196 - 0
audio-service/src/main/java/com/zksy/audio/service/NoiseLeakAlarmService.java

@@ -0,0 +1,196 @@
+package com.zksy.audio.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.zksy.api.domain.AlarmData;
+import com.zksy.api.domain.WarningThreshold;
+import com.zksy.api.service.AlarmDataService;
+import com.zksy.api.service.EarlyWarningAutoService;
+import com.zksy.api.service.WarningThresholdService;
+import com.zksy.audio.domain.NoiseInfo;
+import com.zksy.audio.mapper.NoiseInfoMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 漏失监测告警服务
+ * <p>
+ * 采用"时间持续性 + 漏水特征频段过滤"策略(漏失噪音是持续的,瞬时环境噪音不应触发):
+ * <ul>
+ *   <li>rms 越限阈值(max_value)、恢复阈值(min_value) 均从 {@code warning_threshold} 表按设备编码获取,与系统现有逻辑一致</li>
+ *   <li>出现单次 rms≥max 且在漏水特征频段 → <b>预警</b>(疑似漏失)</li>
+ *   <li>最近窗口内累计 N 次 rms≥max 且在漏水特征频段 → <b>告警</b>(确认漏失),并解除预警</li>
+ *   <li>rms 回落≤min(或不在特征频段)→ 解除预警,重置确认态</li>
+ * </ul>
+ */
+@Slf4j
+@Service
+public class NoiseLeakAlarmService {
+    //rms声音响度
+    private static final String LEAK_WARNING_CODE = "WARN-LEAK";
+    
+    private static final String LEAK_FREQ_CODE = "WARN-LEAK-FREQ";
+    private static final String LEAK_WARNING_TYPE = "漏失预警";
+
+    private static final DateTimeFormatter TS_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
+    /** 已确认漏失的设备编码,用于避免同一漏失事件内重复告警 */
+    private final Set<String> confirmedLeakEncode = ConcurrentHashMap.newKeySet();
+
+    @Value("${wlds.leak.freq-min:200.0}")
+    private double freqMin;
+
+    @Value("${wlds.leak.freq-max:2000.0}")
+    private double freqMax;
+
+    @Value("${wlds.leak.window-minutes:30}")
+    private int windowMinutes;
+
+    @Value("${wlds.leak.alarm-count:3}")
+    private int alarmCount;
+
+    @Autowired
+    private NoiseInfoMapper noiseInfoMapper;
+
+    @Autowired
+    private AlarmDataService alarmDataService;
+
+    @Autowired
+    private EarlyWarningAutoService earlyWarningAutoService;
+
+    @Autowired
+    private WarningThresholdService warningThresholdService;
+
+    /**
+     * 对一条新入库的噪音数据进行漏失预警/告警评估
+     *
+     * @param noise 当前噪音数据
+     */
+    public void evaluate(NoiseInfo noise) {
+        if (noise == null || noise.getEncode() == null || noise.getEncode().trim().isEmpty()) {
+            log.warn("漏失评估跳过:噪音数据缺少设备编码");
+            return;
+        }
+        String encode = noise.getEncode();
+
+        // 从阈值表获取该设备的漏失阈值(max=越限,min=恢复)
+        WarningThreshold threshold = queryThreshold(encode, LEAK_WARNING_CODE);
+        Double maxValue = threshold != null ? threshold.getMaxValue() : null;
+        Double minValue = threshold != null ? threshold.getMinValue() : null;
+        if (maxValue == null) {
+            log.warn("漏失评估跳过:设备未配置漏失阈值(max_value),encode={}", encode);
+            return;
+        }
+
+        // 特征频段:优先取阈值表(WARN-LEAK-FREQ min=下限 max=上限),未配则回退应用配置
+        WarningThreshold freqThreshold = queryThreshold(encode, LEAK_FREQ_CODE);
+        double lowerFreq = freqMin;
+        double upperFreq = freqMax;
+        if (freqThreshold != null && freqThreshold.getMinValue() != null && freqThreshold.getMaxValue() != null
+                && freqThreshold.getMinValue() < freqThreshold.getMaxValue()) {
+            lowerFreq = freqThreshold.getMinValue();
+            upperFreq = freqThreshold.getMaxValue();
+        }
+
+        Integer rms = noise.getRms();
+        if (rms == null) {
+            return;
+        }
+
+        boolean freqInBand = noise.getCenterFrequency() != null
+                && noise.getCenterFrequency() >= lowerFreq && noise.getCenterFrequency() <= upperFreq;
+
+        // 1) 特征频段消失:漏水特征不再成立,彻底解除
+        if (!freqInBand) {
+            confirmedLeakEncode.remove(encode);
+            earlyWarningAutoService.closeAutoWarning(encode, LEAK_WARNING_CODE, "漏失特征频段消失,解除预警");
+            return;
+        }
+
+        // 2) rms 回落至恢复阈值(≤min):解除预警并重置确认态
+        if (minValue != null && rms <= minValue) {
+            confirmedLeakEncode.remove(encode);
+            earlyWarningAutoService.closeAutoWarning(encode, LEAK_WARNING_CODE, "rms回落至恢复阈值,解除预警");
+            return;
+        }
+
+        // 3) 中间带(min < rms < max):保持现状,不做级别变更,避免边界抖动
+        if (rms < maxValue) {
+            return;
+        }
+
+        // 4) rms >= max:疑似→确认(时间持续性)
+        long count = countQualified(encode, maxValue, lowerFreq, upperFreq);
+        if (count >= alarmCount) {
+            if (confirmedLeakEncode.add(encode)) {
+                saveLeakAlarm(encode, rms, count, lowerFreq, upperFreq);
+                log.info("确认漏失告警: encode={}, 窗口累计{}次", encode, count);
+            }
+            earlyWarningAutoService.closeAutoWarning(encode, LEAK_WARNING_CODE, "已确认漏失,转告警处理");
+        } else {
+            confirmedLeakEncode.remove(encode);
+            earlyWarningAutoService.recordWarning(encode, LEAK_WARNING_TYPE, LEAK_WARNING_CODE, 3,
+                    buildWarningContent(encode, count, maxValue, lowerFreq, upperFreq));
+        }
+    }
+
+    /**
+     * 按设备编码与预警编码查询阈值规则
+     */
+    private WarningThreshold queryThreshold(String encode, String warningCode) {
+        try {
+            return warningThresholdService.getWarningThresholdByDeviceAndCode(encode, warningCode);
+        } catch (Exception e) {
+            log.error("查询阈值失败:encode={}, warningCode={}", encode, warningCode, e);
+            return null;
+        }
+    }
+
+    /**
+     * 统计指定设备在最近时间窗口内,rms 超过越限阈值且中心频率落在漏失频段的采样次数
+     */
+    private long countQualified(String encode, Double maxValue, double lowerFreq, double upperFreq) {
+        String start = LocalDateTime.now().minusMinutes(windowMinutes).format(TS_FORMATTER);
+        return noiseInfoMapper.selectCount(new LambdaQueryWrapper<NoiseInfo>()
+                .eq(NoiseInfo::getEncode, encode)
+                .ge(NoiseInfo::getTs, start)
+                .ge(NoiseInfo::getRms, maxValue)
+                .ge(NoiseInfo::getCenterFrequency, lowerFreq)
+                .le(NoiseInfo::getCenterFrequency, upperFreq));
+    }
+
+    private void saveLeakAlarm(String encode, Integer rms, long count, double lowerFreq, double upperFreq) {
+        AlarmData alarm = new AlarmData();
+        alarm.setDeviceCode(encode);
+        alarm.setWarningType("漏失报警");
+        alarm.setWarningCode(LEAK_WARNING_CODE);
+        if (rms != null) {
+            alarm.setActualValue(BigDecimal.valueOf(rms));
+        }
+        alarm.setAlarmLevel(1);
+        alarm.setAlarmStatus(0);
+        alarm.setAlarmTime(LocalDateTime.now());
+        alarm.setCreateTime(LocalDateTime.now());
+        alarm.setRemark(String.format("漏失监测确认漏失:最近%d分钟内%d次采样rms≥阈值且在漏失频段[%.0f,%.0f]Hz",
+                windowMinutes, count, lowerFreq, upperFreq));
+        try {
+            alarmDataService.saveAlarmData(alarm);
+            log.info("漏失告警入库成功: encode={}, count={}", encode, count);
+        } catch (Exception e) {
+            log.error("漏失告警入库失败: encode={}", encode, e);
+        }
+    }
+
+    private String buildWarningContent(String encode, long count, Double maxValue, double lowerFreq, double upperFreq) {
+        return String.format("【系统自动预警】设备[%s]疑似漏失:最近%d分钟内%d次采样rms≥%s且中心频率落在漏失频段[%.0f,%.0f]Hz,待确认",
+                encode, windowMinutes, count, Objects.toString(maxValue, "-"), lowerFreq, upperFreq);
+    }
+}

+ 5 - 0
audio-service/src/main/java/com/zksy/audio/utils/MessageHandler.java

@@ -10,6 +10,7 @@ import cn.hutool.core.util.StrUtil;
 import com.zksy.audio.domain.ChannelInfo;
 import com.zksy.audio.domain.NoiseInfo;
 import com.zksy.audio.service.NoiseInfoService;
+import com.zksy.audio.service.NoiseLeakAlarmService;
 import com.zksy.audio.utils.CrtHelper;
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.Unpooled;
@@ -54,6 +55,8 @@ public class MessageHandler extends ChannelInboundHandlerAdapter {
 	@Autowired
 	private DeviceOfflineCheckTask deviceOfflineCheckTask;
 	@Autowired
+	private NoiseLeakAlarmService noiseLeakAlarmService;
+	@Autowired
 	public MessageHandler(NoiseInfoService noiseInfoService) {
 		this.service = noiseInfoService;
 	}
@@ -290,6 +293,8 @@ public class MessageHandler extends ChannelInboundHandlerAdapter {
 							DatePattern.NORM_DATETIME_PATTERN));
 					//log.info(JsonUtils.toJsonString(noise));
 					service.save(noise);
+					// 漏失监测:时间持续性 + 漏水特征频段过滤,两阶段 预警/告警
+					noiseLeakAlarmService.evaluate(noise);
 					// 更新设备最后接收时间
 					DeviceOfflineCheckTask.deviceLastReceiveTimeMap.put(info.getDeviceNo(), new java.util.Date());
 					deviceOfflineCheckTask.markDeviceOnline(info.getDeviceNo());

+ 6 - 1
audio-service/src/main/resources/application-dev.yaml

@@ -16,4 +16,9 @@ spring:
       config:
         server-addr: 192.168.110.30:8848
 wlds:
-  fileUploadBaseDir: D:\home\wlds\uploads
+  fileUploadBaseDir: D:\home\wlds\uploads
+  leak:
+    freq-min: 200.0     # 特征频段下限 Hz
+    freq-max: 2000.0    # 特征频段上限 Hz
+    window-minutes: 30  # 时间窗口(分钟)
+    alarm-count: 3      # 确认漏失所需累计次数

+ 6 - 1
audio-service/src/main/resources/application-druid.yaml

@@ -16,4 +16,9 @@ spring:
       config:
         server-addr: 172.16.102.52:8848
 wlds:
-  fileUploadBaseDir: \home\wlds\uploads
+  fileUploadBaseDir: \home\wlds\uploads
+  leak:
+    freq-min: 200.0     # 特征频段下限 Hz
+    freq-max: 2000.0    # 特征频段上限 Hz
+    window-minutes: 30  # 时间窗口(分钟)
+    alarm-count: 3      # 确认漏失所需累计次数

+ 6 - 1
audio-service/src/main/resources/application-prod.yaml

@@ -10,4 +10,9 @@ spring:
     host: 47.107.107.47
     port: 6379
 wlds:
-  fileUploadBaseDir: \home\wlds\uploads
+  fileUploadBaseDir: \home\wlds\uploads
+  leak:
+    freq-min: 200.0     # 特征频段下限 Hz
+    freq-max: 2000.0    # 特征频段上限 Hz
+    window-minutes: 30  # 时间窗口(分钟)
+    alarm-count: 3      # 确认漏失所需累计次数

+ 16 - 196
flammable-gas-service/src/main/java/com/zksy/gas/utils/MessageHandler.java

@@ -218,196 +218,36 @@ public class MessageHandler extends ChannelInboundHandlerAdapter {
 	        String deviceId = resultData.getMacAddress();
 	        boolean hasAlarm = false;
 
-		//获取温度阈值
-		String temWarningCode = "WARN-GAS-TEMPERATURE";
-		WarningThreshold temThreshold = checkThreshold(deviceId, temWarningCode);
-		Double temMinValue = temThreshold != null ? temThreshold.getMinValue() : null;
-		Double temMaxValue = temThreshold != null ? temThreshold.getMaxValue() : null;
-		String temWarningType = temThreshold != null ? temThreshold.getWarningType() : "可燃气体温度预警";
-		String temRemark = temThreshold != null ? temThreshold.getRemark() : null;
-
-		//获取湿度阈值
-		String humWarningCode = "WARN-GAS-HUMIDITY";
-		WarningThreshold humThreshold = checkThreshold(deviceId, humWarningCode);
-		Double humMinValue = humThreshold != null ? humThreshold.getMinValue() : null;
-		Double humMaxValue = humThreshold != null ? humThreshold.getMaxValue() : null;
-		String humWarningType = humThreshold != null ? humThreshold.getWarningType() : "湿度预警";
-		String humRemark = humThreshold != null ? humThreshold.getRemark() : null;
-
-		//获取气体浓度阈值
+		//获取气体浓度阈值(按气体类型只监测浓度,当前仅CH4;且仅按上限判定)
 		String gasWarningCode = "WARN-GAS-CONCENTRATION";
 		WarningThreshold gasThreshold = checkThreshold(deviceId, gasWarningCode);
-		Double gasMinValue = gasThreshold != null ? gasThreshold.getMinValue() : null;
 		Double gasMaxValue = gasThreshold != null ? gasThreshold.getMaxValue() : null;
 		String gasWarningType = gasThreshold != null ? gasThreshold.getWarningType() : "气体浓度预警";
 		String gasRemark = gasThreshold != null ? gasThreshold.getRemark() : null;
 
-		//判断温度
-		float temperature = resultData.getTemperature();
-		boolean temperatureIsOverThreshold = false;
-		if (temMinValue != null && temperature <= temMinValue) {
-			temperatureIsOverThreshold = true;
-		}
-		if (temMaxValue != null && temperature >= temMaxValue) {
-			temperatureIsOverThreshold = true;
-		}
-		BigDecimal temMinBd = temMinValue != null ? BigDecimal.valueOf(temMinValue) : null;
-		BigDecimal temMaxBd = temMaxValue != null ? BigDecimal.valueOf(temMaxValue) : null;
-		BigDecimal temActualBd = BigDecimal.valueOf(temperature);
-		if (temMinValue != null || temMaxValue != null) {
-			if (temperatureIsOverThreshold) {
-					int alarmLevel = saveAlarmData(deviceId, temWarningType, temWarningCode,
-							temMinBd, temMaxBd, temActualBd, temRemark != null ? temRemark : "可燃气体温度报警");
-					earlyWarningAutoService.closeAutoWarning(deviceId, temWarningCode, "已达报警阈值,转报警处理");
-					hasAlarm = true;
-					if (alarmLevel == 1) {
-					sendMessage(deviceId, temWarningType, resultData.getLongitude(), resultData.getLatitude());
-				}
-			} else {
-				earlyWarningAutoService.checkAndGenerate(deviceId, temWarningType, temWarningCode,
-						temMinBd, temMaxBd, temActualBd, temRemark);
-			}
-		}
-
-		//判断湿度
-		float humidity = resultData.getHumidity();
-		boolean humidityIsOverThreshold = false;
-		if (humMinValue != null && humidity <= humMinValue) {
-			humidityIsOverThreshold = true;
-		}
-		if (humMaxValue != null && humidity >= humMaxValue) {
-			humidityIsOverThreshold = true;
-		}
-		BigDecimal humMinBd = humMinValue != null ? BigDecimal.valueOf(humMinValue) : null;
-		BigDecimal humMaxBd = humMaxValue != null ? BigDecimal.valueOf(humMaxValue) : null;
-		BigDecimal humActualBd = BigDecimal.valueOf(humidity);
-		if (humMinValue != null || humMaxValue != null) {
-			if (humidityIsOverThreshold) {
-					int alarmLevel = saveAlarmData(deviceId, humWarningType, humWarningCode,
-							humMinBd, humMaxBd, humActualBd, humRemark != null ? humRemark : "可燃气体湿度报警");
-					earlyWarningAutoService.closeAutoWarning(deviceId, humWarningCode, "已达报警阈值,转报警处理");
-					hasAlarm = true;
-					if (alarmLevel == 1) {
-					sendMessage(deviceId, humWarningType, resultData.getLongitude(), resultData.getLatitude());
-				}
-			} else {
-				earlyWarningAutoService.checkAndGenerate(deviceId, humWarningType, humWarningCode,
-						humMinBd, humMaxBd, humActualBd, humRemark);
-			}
-		}
-
-		//判断气体浓度
+		//判断气体浓度:仅当浓度达到或超过上限时视为越限;浓度低(接近0)属正常,不告警
 		BigDecimal gasConcentration = resultData.getGasConcentration();
-		float actualGasConcentration = gasConcentration != null ? gasConcentration.floatValue() : 0f;
-		boolean concentrationIsOverThreshold = false;
-		if (gasMinValue != null && actualGasConcentration <= gasMinValue) {
-			concentrationIsOverThreshold = true;
-		}
-		if (gasMaxValue != null && actualGasConcentration >= gasMaxValue) {
-			concentrationIsOverThreshold = true;
-		}
-		BigDecimal gasMinBd = gasMinValue != null ? BigDecimal.valueOf(gasMinValue) : null;
-		BigDecimal gasMaxBd = gasMaxValue != null ? BigDecimal.valueOf(gasMaxValue) : null;
-		if ((gasMinValue != null || gasMaxValue != null) && gasConcentration != null) {
-			if (concentrationIsOverThreshold) {
-					int alarmLevel = saveAlarmData(deviceId, gasWarningType, gasWarningCode,
-							gasMinBd, gasMaxBd, gasConcentration, gasRemark != null ? gasRemark : "可燃气体浓度报警");
-					earlyWarningAutoService.closeAutoWarning(deviceId, gasWarningCode, "已达报警阈值,转报警处理");
-					hasAlarm = true;
-					if (alarmLevel == 1) {
+		if (gasConcentration != null && gasMaxValue != null) {
+			BigDecimal gasMaxBd = BigDecimal.valueOf(gasMaxValue);
+			if (gasConcentration.floatValue() >= gasMaxValue) {
+				// 越限报警:等级计算以0为下限基线(气体浓度不会低于0),保证仅上限时也能正确分级
+				int alarmLevel = saveAlarmData(deviceId, gasWarningType, gasWarningCode,
+						BigDecimal.ZERO, gasMaxBd, gasConcentration,
+						gasRemark != null ? gasRemark : "可燃气体浓度报警");
+				earlyWarningAutoService.closeAutoWarning(deviceId, gasWarningCode, "已达报警阈值,转报警处理");
+				hasAlarm = true;
+				if (alarmLevel == 1) {
 					sendMessage(deviceId, gasWarningType, resultData.getLongitude(), resultData.getLatitude());
 				}
 			} else {
+				// 未越限:接近上限时生成自动预警(min传null,仅按上限评估预警区)
 				earlyWarningAutoService.checkAndGenerate(deviceId, gasWarningType, gasWarningCode,
-						gasMinBd, gasMaxBd, gasConcentration, gasRemark);
-			}
-		}
-
-		//获取到的数据逐一进行判断
-		Map<String, Boolean> alarmBits = resultData.parseAlarmBits();
-		if(alarmBits.get("unknownAlarm")){
-				saveAlarmDataLevelOne(deviceId, "未知报警", "WARN-UNKNOWN", null, "可燃气体未知报警");
-				sendMessage(deviceId,"未知报警",resultData.getLongitude(),resultData.getLatitude());
-				hasAlarm = true;
+						null, gasMaxBd, gasConcentration, gasRemark);
 			}
-			if(alarmBits.get("highAlarm")){
-				saveAlarmDataLevelOne(deviceId, "高报警", "WARN-HIGH", null, "可燃气体高报警");
-				sendMessage(deviceId,"高报警",resultData.getLongitude(),resultData.getLatitude());
-				hasAlarm = true;
-			}
-		if(alarmBits.get("overRange")){
-			saveAlarmDataLevelOne(deviceId, "超量程", "WARN-OVER-RANGE", null, "可燃气体超量程报警");
-			sendMessage(deviceId,"超量程",resultData.getLongitude(),resultData.getLatitude());
-			hasAlarm = true;
-		}
-		if(alarmBits.get("calibrationCycle")){
-			saveAlarmDataLevelOne(deviceId, "标定周期", "WARN-CALIBRATION-CYCLE", null, "可燃气体标定周期报警");
-			sendMessage(deviceId,"标定周期",resultData.getLongitude(),resultData.getLatitude());
-			hasAlarm = true;
-		}
-		if(alarmBits.get("overLife")){
-			saveAlarmDataLevelOne(deviceId, "超寿命", "WARN-OVER-LIFE", null, "可燃气体超寿命报警");
-			sendMessage(deviceId,"超寿命",resultData.getLongitude(),resultData.getLatitude());
-			hasAlarm = true;
-		}
-		if(alarmBits.get("fallAlarm")){
-			saveAlarmDataLevelOne(deviceId, "跌倒报警", "WARN-FALL", null, "可燃气体跌倒报警");
-			sendMessage(deviceId,"跌倒报警",resultData.getLongitude(),resultData.getLatitude());
-			hasAlarm = true;
-		}
-		if(alarmBits.get("undervoltage")){
-			saveAlarmDataLevelOne(deviceId, "欠压报警", "WARN-UNDERVOLTAGE", null, "可燃气体欠压报警");
-			sendMessage(deviceId,"欠压报警",resultData.getLongitude(),resultData.getLatitude());
-			hasAlarm = true;
 		}
-		if(alarmBits.get("rangeAlarm")){
-			saveAlarmDataLevelOne(deviceId, "区间报警", "WARN-RANGE", null, "可燃气体区间报警");
-			sendMessage(deviceId,"区间报警",resultData.getLongitude(),resultData.getLatitude());
-			hasAlarm = true;
-		}
-		if(alarmBits.get("keyAlarm")){
-			saveAlarmDataLevelOne(deviceId, "按键报警", "WARN-KEY", null, "可燃气体按键报警");
-			sendMessage(deviceId,"按键报警",resultData.getLongitude(),resultData.getLatitude());
-			hasAlarm = true;
-		}
-		if(alarmBits.get("vibrationAlarm")){
-			saveAlarmDataLevelOne(deviceId, "震动报警", "WARN-VIBRATION", null, "可燃气体震动报警");
-			sendMessage(deviceId,"震动报警",resultData.getLongitude(),resultData.getLatitude());
-			hasAlarm = true;
-		}
-		if(alarmBits.get("waterLevelAlarm")){
-			saveAlarmDataLevelOne(deviceId, "水位报警", "WARN-WATER-LEVEL", null, "可燃气体水位报警");
-			sendMessage(deviceId,"水位报警",resultData.getLongitude(),resultData.getLatitude());
-			hasAlarm = true;
-		}
-		if(alarmBits.get("powerOffAlarm")){
-			saveAlarmDataLevelOne(deviceId, "断电报警", "WARN-POWER-OFF", null, "可燃气体断电报警");
-			sendMessage(deviceId,"断电报警",resultData.getLongitude(),resultData.getLatitude());
-			hasAlarm = true;
-		}
-		if(alarmBits.get("sensorFault")){
-			saveAlarmDataLevelOne(deviceId, "传感器故障", "WARN-SENSOR-FAULT", null, "可燃气体传感器故障");
-			sendMessage(deviceId,"传感器故障",resultData.getLongitude(),resultData.getLatitude());
-			hasAlarm = true;
-		}
-		if(alarmBits.get("overHumidity")){
-			saveAlarmDataLevelOne(deviceId, "超湿报警", "WARN-OVER-HUMIDITY", null, "可燃气体超湿报警");
-			sendMessage(deviceId,"超湿报警",resultData.getLongitude(),resultData.getLatitude());
-			hasAlarm = true;
-		}
-		if(alarmBits.get("overTemperature")){
-			saveAlarmDataLevelOne(deviceId, "超温报警", "WARN-OVER-TEMPERATURE", null, "可燃气体超温报警");
-			sendMessage(deviceId,"超温报警",resultData.getLongitude(),resultData.getLatitude());
-			hasAlarm = true;
-		}
-		if(alarmBits.get("systemFault")){
-			saveAlarmDataLevelOne(deviceId, "系统故障", "WARN-SYSTEM-FAULT", null, "可燃气体系统故障");
-			sendMessage(deviceId,"系统故障",resultData.getLongitude(),resultData.getLatitude());
-			hasAlarm = true;
-			}
 
-			return hasAlarm;
-		}
+		return hasAlarm;
+	}
 
 		private WarningThreshold checkThreshold(String deviceCode, String warningCode) {
 		try {
@@ -452,26 +292,6 @@ public class MessageHandler extends ChannelInboundHandlerAdapter {
 		}
 		return alarmLevel;
 	}
-
-	private void saveAlarmDataLevelOne(String deviceCode, String warningType, String warningCode,
-									   BigDecimal actualValue, String remark) {
-		try {
-			AlarmData alarmData = new AlarmData();
-			alarmData.setDeviceCode(deviceCode);
-			alarmData.setWarningType(warningType);
-			alarmData.setWarningCode(warningCode);
-			alarmData.setActualValue(actualValue);
-			alarmData.setAlarmLevel(1);
-			alarmData.setAlarmStatus(0);
-			alarmData.setAlarmTime(LocalDateTime.now());
-			alarmData.setRemark(remark);
-			alarmData.setCreateTime(LocalDateTime.now());
-			alarmDataService.saveAlarmData(alarmData);
-			log.info("设备主动告警入库(1级): deviceCode={}, warningCode={}", deviceCode, warningCode);
-		} catch (Exception e) {
-			log.error("保存告警数据失败", e);
-		}
-	}
 	//构造发送短信参数&&发送短信
 	public void sendMessage(String deviceNo, String alarmType, BigDecimal lng, BigDecimal lat){
 		// 构造短信参数

+ 1 - 1
water-level-service/src/main/java/com/zksy/water/utils/MessageHandler.java

@@ -224,7 +224,7 @@ public class MessageHandler extends ChannelInboundHandlerAdapter {
 			try {
 				// 处理水位告警
 				if (resultData.getWaterLevel() != null) {
-					if (handleAlarm(deviceCode, "WARN_WATER_LEVEL", "水位", 
+					if (handleAlarm(deviceCode, "WARN-WATER-LEVEL", "水位",
 						resultData.getWaterLevel())) {
 						hasAlarm = true;
 					}

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

@@ -32,4 +32,17 @@ public interface EarlyWarningAutoService {
      * 关闭指定设备+预警编码下未完结的自动预警
      */
     void closeAutoWarning(String deviceCode, String warningCode, String reason);
+
+    /**
+     * 主动记录一条自动预警(用于非数值置信度类的预警,如漏失监测的"疑似漏失")。
+     * 存在未完结预警时更新其内容与级别,否则新建。
+     *
+     * @param deviceCode  设备编码
+     * @param warningType 预警类型名称
+     * @param warningCode 预警编码
+     * @param level       预警级别 1-4
+     * @param content     预警内容
+     */
+    void recordWarning(String deviceCode, String warningType, String warningCode,
+                       int level, String content);
 }

+ 34 - 0
zk-api-service/src/main/java/com/zksy/api/service/impl/EarlyWarningAutoServiceImpl.java

@@ -101,6 +101,40 @@ public class EarlyWarningAutoServiceImpl implements EarlyWarningAutoService {
         }
     }
 
+    @Override
+    public void recordWarning(String deviceCode, String warningType, String warningCode,
+                              int level, String content) {
+        if (deviceCode == null || deviceCode.isEmpty() || warningCode == null || warningCode.isEmpty()) {
+            log.warn("记录主动预警参数不完整,忽略: deviceCode={}, warningCode={}", deviceCode, warningCode);
+            return;
+        }
+        try {
+            EarlyWarning existing = findOpenAutoWarning(deviceCode, warningCode);
+            if (existing != null) {
+                existing.setWarningLevel(String.valueOf(level));
+                existing.setWarningContent(content);
+                if (warningType != null && !warningType.isEmpty()) {
+                    existing.setWarningType(warningType);
+                    existing.setWarningName(buildWarningName(warningType, level));
+                }
+                EquipmentBase equipment = findEquipment(deviceCode);
+                if (equipment != null && equipment.getEquipmentName() != null) {
+                    existing.setDeviceName(equipment.getEquipmentName());
+                }
+                existing.setUpdateBy(SYSTEM_USER);
+                existing.setUpdateTime(LocalDateTime.now());
+                earlyWarningApiMapper.updateById(existing);
+                log.info("更新主动预警: warningNo={}, deviceCode={}, warningCode={}, level={}级",
+                        existing.getWarningNo(), deviceCode, warningCode, level);
+            } else {
+                createNew(deviceCode, warningType, warningCode,
+                        new EarlyWarningLevelResult(level, null, "UPPER"), content, null, null, null);
+            }
+        } catch (Exception e) {
+            log.error("记录主动预警失败: deviceCode={}, warningCode={}", deviceCode, warningCode, e);
+        }
+    }
+
     private EarlyWarning findOpenAutoWarning(String deviceCode, String warningCode) {
         String marker = buildRemarkMarker(deviceCode, warningCode);
         return earlyWarningApiMapper.selectOne(new LambdaQueryWrapper<EarlyWarning>()