package com.zksy.audio.service; import static com.zksy.common.utils.CollectionSideEffects.*; 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; /** * 漏失监测告警服务 *

* 设备每24小时上传正常或异常录音,单次达到RMS阈值且满足漏损频段即一级告警。 * 同一录音重传由接收层去重;保留配置多次确认的能力,供其他采样周期使用: *

*/ @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 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:1}") private int alarmCount = 1; @Autowired private NoiseInfoMapper noiseInfoMapper; @Autowired private AlarmDataService alarmDataService; @Autowired private EarlyWarningAutoService earlyWarningAutoService; @Autowired private WarningThresholdService warningThresholdService; @Autowired private com.zksy.api.utils.CollectionDeviceNotifier deviceNotifier; /** * 对一条新入库的噪音数据进行漏失预警/告警评估 * * @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; } if (noise.getCenterFrequency() == null || !Double.isFinite(noise.getCenterFrequency())) { throw new IllegalArgumentException("缺少有效音频频率,保留原报警状态"); } boolean freqInBand = noise.getCenterFrequency() != null && noise.getCenterFrequency() >= lowerFreq && noise.getCenterFrequency() <= upperFreq; // 1) 特征频段消失:漏水特征不再成立,彻底解除 if (!freqInBand) { confirmedLeakEncode.remove(encode); run(encode, "告警恢复", () -> alarmDataService.recoverAlarm(encode, LEAK_WARNING_CODE)); run(encode, "解除预警", () -> earlyWarningAutoService.closeAutoWarning(encode, LEAK_WARNING_CODE, "漏失特征频段消失,解除预警")); run(encode, "报警状态恢复", () -> deviceNotifier.updateAlarmStatus(encode, 0)); return; } // 2) rms 回落至恢复阈值(≤min):解除预警并重置确认态 if (minValue != null && rms <= minValue) { confirmedLeakEncode.remove(encode); run(encode, "告警恢复", () -> alarmDataService.recoverAlarm(encode, LEAK_WARNING_CODE)); run(encode, "解除预警", () -> earlyWarningAutoService.closeAutoWarning(encode, LEAK_WARNING_CODE, "rms回落至恢复阈值,解除预警")); run(encode, "报警状态恢复", () -> deviceNotifier.updateAlarmStatus(encode, 0)); return; } // 3) 中间带(min < rms < max):保持现状,不做级别变更,避免边界抖动 if (rms < maxValue) { if (confirmedLeakEncode.contains(encode)) { run(encode, "报警状态同步", () -> deviceNotifier.updateAlarmStatus(encode, 1)); } return; } // 单次模式只依赖本次已保存的录音,不再查询历史计数。 long count = alarmCount == 1 ? 1 : countQualified(encode, maxValue, lowerFreq, upperFreq); if (count >= alarmCount) { boolean newlyConfirmed = confirmedLeakEncode.add(encode); if (alarmCount == 1 || newlyConfirmed) { run(encode, "一级漏失告警短信", () -> deviceNotifier.sendLevelOneSms(encode, "漏失报警", "RMS=" + rms)); if (saveLeakAlarm(encode, rms, count, maxValue, lowerFreq, upperFreq)) { log.info("确认漏失告警: encode={}, 本次确认采用{}份独立采样", encode, count); } else { confirmedLeakEncode.remove(encode); } } run(encode, "解除预警", () -> earlyWarningAutoService.closeAutoWarning(encode, LEAK_WARNING_CODE, "已确认漏失,转告警处理")); run(encode, "报警状态同步", () -> deviceNotifier.updateAlarmStatus(encode, 1)); } else { confirmedLeakEncode.remove(encode); final double lower = lowerFreq, upper = upperFreq; run(encode, "疑似漏失预警", () -> earlyWarningAutoService.recordWarning(encode, LEAK_WARNING_TYPE, LEAK_WARNING_CODE, 3, buildWarningContent(encode, count, maxValue, lower, upper))); } } /** * 按设备编码与预警编码查询阈值规则 */ private WarningThreshold queryThreshold(String encode, String warningCode) { return warningThresholdService.getWarningThresholdByDeviceAndCode(encode, warningCode); } /** * 统计指定设备在最近时间窗口内,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() .eq(NoiseInfo::getEncode, encode) .ge(NoiseInfo::getTs, start) .ge(NoiseInfo::getRms, maxValue) .ge(NoiseInfo::getCenterFrequency, lowerFreq) .le(NoiseInfo::getCenterFrequency, upperFreq)); } private boolean saveLeakAlarm(String encode, Integer rms, long count, Double maxValue, double lowerFreq, double upperFreq) { AlarmData alarm = new AlarmData(); alarm.setDeviceCode(encode); alarm.setWarningType("漏失报警"); alarm.setWarningCode(LEAK_WARNING_CODE); // 漏失告警只在 alarm_data 中保存 RMS 的越限上限;最小值仅用于恢复判断,不入库。 if (maxValue != null) { alarm.setMaxValue(BigDecimal.valueOf(maxValue)); } if (rms != null) { alarm.setActualValue(BigDecimal.valueOf(rms)); } alarm.setAlarmLevel(1); alarm.setAlarmStatus(0); alarm.setCreateTime(LocalDateTime.now()); alarm.setRemark(alarmCount == 1 ? String.format("漏失监测单次独立采样确认:rms≥阈值且在漏失频段[%.0f,%.0f]Hz", lowerFreq, upperFreq) : String.format("漏失监测确认漏失:最近%d分钟内%d次采样rms≥阈值且在漏失频段[%.0f,%.0f]Hz", windowMinutes, count, lowerFreq, upperFreq)); try { requireSaved(alarmDataService.saveAlarmData(alarm)); log.info("漏失告警入库成功: encode={}, count={}", encode, count); return true; } catch (Exception e) { log.error("漏失告警入库失败: encode={}", encode, e); return false; } } 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); } }