| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221 |
- 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;
- /**
- * 漏失监测告警服务
- * <p>
- * 设备每24小时上传正常或异常录音,单次达到RMS阈值且满足漏损频段即一级告警。
- * 同一录音重传由接收层去重;保留配置多次确认的能力,供其他采样周期使用:
- * <ul>
- * <li>rms 越限阈值(max_value)、恢复阈值(min_value) 均从 {@code warning_threshold} 表按设备编码获取,与系统现有逻辑一致</li>
- * <li>alarm-count=1:每份独立录音 rms≥max 且在漏水特征频段 → <b>一级告警及短信</b></li>
- * <li>alarm-count>1:窗口内累计 N 次确认,次数不足时预警</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: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<NoiseInfo>()
- .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);
- }
- }
|