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