NoiseLeakAlarmService.java 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. package com.zksy.audio.service;
  2. import static com.zksy.common.utils.CollectionSideEffects.*;
  3. import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  4. import com.zksy.api.domain.AlarmData;
  5. import com.zksy.api.domain.WarningThreshold;
  6. import com.zksy.api.service.AlarmDataService;
  7. import com.zksy.api.service.EarlyWarningAutoService;
  8. import com.zksy.api.service.WarningThresholdService;
  9. import com.zksy.audio.domain.NoiseInfo;
  10. import com.zksy.audio.mapper.NoiseInfoMapper;
  11. import lombok.extern.slf4j.Slf4j;
  12. import org.springframework.beans.factory.annotation.Autowired;
  13. import org.springframework.beans.factory.annotation.Value;
  14. import org.springframework.stereotype.Service;
  15. import java.math.BigDecimal;
  16. import java.time.LocalDateTime;
  17. import java.time.format.DateTimeFormatter;
  18. import java.util.Objects;
  19. import java.util.Set;
  20. import java.util.concurrent.ConcurrentHashMap;
  21. /**
  22. * 漏失监测告警服务
  23. * <p>
  24. * 设备每24小时上传正常或异常录音,单次达到RMS阈值且满足漏损频段即一级告警。
  25. * 同一录音重传由接收层去重;保留配置多次确认的能力,供其他采样周期使用:
  26. * <ul>
  27. * <li>rms 越限阈值(max_value)、恢复阈值(min_value) 均从 {@code warning_threshold} 表按设备编码获取,与系统现有逻辑一致</li>
  28. * <li>alarm-count=1:每份独立录音 rms≥max 且在漏水特征频段 → <b>一级告警及短信</b></li>
  29. * <li>alarm-count>1:窗口内累计 N 次确认,次数不足时预警</li>
  30. * <li>rms 回落≤min(或不在特征频段)→ 解除预警,重置确认态</li>
  31. * </ul>
  32. */
  33. @Slf4j
  34. @Service
  35. public class NoiseLeakAlarmService {
  36. //rms声音响度
  37. private static final String LEAK_WARNING_CODE = "WARN-LEAK";
  38. private static final String LEAK_FREQ_CODE = "WARN-LEAK-FREQ";
  39. private static final String LEAK_WARNING_TYPE = "漏失预警";
  40. private static final DateTimeFormatter TS_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
  41. /** 已确认状态用于恢复与中间带保持;单次模式下每份独立异常录音均告警。 */
  42. private final Set<String> confirmedLeakEncode = ConcurrentHashMap.newKeySet();
  43. @Value("${wlds.leak.freq-min:200.0}")
  44. private double freqMin;
  45. @Value("${wlds.leak.freq-max:2000.0}")
  46. private double freqMax;
  47. @Value("${wlds.leak.window-minutes:30}")
  48. private int windowMinutes;
  49. @Value("${wlds.leak.alarm-count:1}")
  50. private int alarmCount = 1;
  51. @Autowired
  52. private NoiseInfoMapper noiseInfoMapper;
  53. @Autowired
  54. private AlarmDataService alarmDataService;
  55. @Autowired
  56. private EarlyWarningAutoService earlyWarningAutoService;
  57. @Autowired
  58. private WarningThresholdService warningThresholdService;
  59. @Autowired
  60. private com.zksy.api.utils.CollectionDeviceNotifier deviceNotifier;
  61. /**
  62. * 对一条新入库的噪音数据进行漏失预警/告警评估
  63. *
  64. * @param noise 当前噪音数据
  65. */
  66. public void evaluate(NoiseInfo noise) {
  67. if (noise == null || noise.getEncode() == null || noise.getEncode().trim().isEmpty()) {
  68. log.warn("漏失评估跳过:噪音数据缺少设备编码");
  69. return;
  70. }
  71. String encode = noise.getEncode();
  72. // 从阈值表获取该设备的漏失阈值(max=越限,min=恢复)
  73. WarningThreshold threshold = queryThreshold(encode, LEAK_WARNING_CODE);
  74. Double maxValue = threshold != null ? threshold.getMaxValue() : null;
  75. Double minValue = threshold != null ? threshold.getMinValue() : null;
  76. if (maxValue == null) {
  77. log.warn("漏失评估跳过:设备未配置漏失阈值(max_value),encode={}", encode);
  78. return;
  79. }
  80. // 特征频段:优先取阈值表(WARN-LEAK-FREQ min=下限 max=上限),未配则回退应用配置
  81. WarningThreshold freqThreshold = queryThreshold(encode, LEAK_FREQ_CODE);
  82. double lowerFreq = freqMin;
  83. double upperFreq = freqMax;
  84. if (freqThreshold != null && freqThreshold.getMinValue() != null && freqThreshold.getMaxValue() != null
  85. && freqThreshold.getMinValue() < freqThreshold.getMaxValue()) {
  86. lowerFreq = freqThreshold.getMinValue();
  87. upperFreq = freqThreshold.getMaxValue();
  88. }
  89. Integer rms = noise.getRms();
  90. if (rms == null) {
  91. return;
  92. }
  93. if (noise.getCenterFrequency() == null || !Double.isFinite(noise.getCenterFrequency())) {
  94. throw new IllegalArgumentException("缺少有效音频频率,保留原报警状态");
  95. }
  96. boolean freqInBand = noise.getCenterFrequency() != null
  97. && noise.getCenterFrequency() >= lowerFreq && noise.getCenterFrequency() <= upperFreq;
  98. // 1) 特征频段消失:漏水特征不再成立,彻底解除
  99. if (!freqInBand) {
  100. confirmedLeakEncode.remove(encode);
  101. run(encode, "告警恢复", () -> alarmDataService.recoverAlarm(encode, LEAK_WARNING_CODE));
  102. run(encode, "解除预警", () -> earlyWarningAutoService.closeAutoWarning(encode, LEAK_WARNING_CODE, "漏失特征频段消失,解除预警"));
  103. run(encode, "报警状态恢复", () -> deviceNotifier.updateAlarmStatus(encode, 0));
  104. return;
  105. }
  106. // 2) rms 回落至恢复阈值(≤min):解除预警并重置确认态
  107. if (minValue != null && rms <= minValue) {
  108. confirmedLeakEncode.remove(encode);
  109. run(encode, "告警恢复", () -> alarmDataService.recoverAlarm(encode, LEAK_WARNING_CODE));
  110. run(encode, "解除预警", () -> earlyWarningAutoService.closeAutoWarning(encode, LEAK_WARNING_CODE, "rms回落至恢复阈值,解除预警"));
  111. run(encode, "报警状态恢复", () -> deviceNotifier.updateAlarmStatus(encode, 0));
  112. return;
  113. }
  114. // 3) 中间带(min < rms < max):保持现状,不做级别变更,避免边界抖动
  115. if (rms < maxValue) {
  116. if (confirmedLeakEncode.contains(encode)) {
  117. run(encode, "报警状态同步", () -> deviceNotifier.updateAlarmStatus(encode, 1));
  118. }
  119. return;
  120. }
  121. // 单次模式只依赖本次已保存的录音,不再查询历史计数。
  122. long count = alarmCount == 1 ? 1 : countQualified(encode, maxValue, lowerFreq, upperFreq);
  123. if (count >= alarmCount) {
  124. boolean newlyConfirmed = confirmedLeakEncode.add(encode);
  125. if (alarmCount == 1 || newlyConfirmed) {
  126. run(encode, "一级漏失告警短信", () -> deviceNotifier.sendLevelOneSms(encode, "漏失报警", "RMS=" + rms));
  127. if (saveLeakAlarm(encode, rms, count, maxValue, lowerFreq, upperFreq)) {
  128. log.info("确认漏失告警: encode={}, 本次确认采用{}份独立采样", encode, count);
  129. } else {
  130. confirmedLeakEncode.remove(encode);
  131. }
  132. }
  133. run(encode, "解除预警", () -> earlyWarningAutoService.closeAutoWarning(encode, LEAK_WARNING_CODE, "已确认漏失,转告警处理"));
  134. run(encode, "报警状态同步", () -> deviceNotifier.updateAlarmStatus(encode, 1));
  135. } else {
  136. confirmedLeakEncode.remove(encode);
  137. final double lower = lowerFreq, upper = upperFreq;
  138. run(encode, "疑似漏失预警", () -> earlyWarningAutoService.recordWarning(encode, LEAK_WARNING_TYPE, LEAK_WARNING_CODE, 3,
  139. buildWarningContent(encode, count, maxValue, lower, upper)));
  140. }
  141. }
  142. /**
  143. * 按设备编码与预警编码查询阈值规则
  144. */
  145. private WarningThreshold queryThreshold(String encode, String warningCode) {
  146. return warningThresholdService.getWarningThresholdByDeviceAndCode(encode, warningCode);
  147. }
  148. /**
  149. * 统计指定设备在最近时间窗口内,rms 超过越限阈值且中心频率落在漏失频段的采样次数
  150. */
  151. private long countQualified(String encode, Double maxValue, double lowerFreq, double upperFreq) {
  152. String start = LocalDateTime.now().minusMinutes(windowMinutes).format(TS_FORMATTER);
  153. return noiseInfoMapper.selectCount(new LambdaQueryWrapper<NoiseInfo>()
  154. .eq(NoiseInfo::getEncode, encode)
  155. .ge(NoiseInfo::getTs, start)
  156. .ge(NoiseInfo::getRms, maxValue)
  157. .ge(NoiseInfo::getCenterFrequency, lowerFreq)
  158. .le(NoiseInfo::getCenterFrequency, upperFreq));
  159. }
  160. private boolean saveLeakAlarm(String encode, Integer rms, long count, Double maxValue,
  161. double lowerFreq, double upperFreq) {
  162. AlarmData alarm = new AlarmData();
  163. alarm.setDeviceCode(encode);
  164. alarm.setWarningType("漏失报警");
  165. alarm.setWarningCode(LEAK_WARNING_CODE);
  166. // 漏失告警只在 alarm_data 中保存 RMS 的越限上限;最小值仅用于恢复判断,不入库。
  167. if (maxValue != null) {
  168. alarm.setMaxValue(BigDecimal.valueOf(maxValue));
  169. }
  170. if (rms != null) {
  171. alarm.setActualValue(BigDecimal.valueOf(rms));
  172. }
  173. alarm.setAlarmLevel(1);
  174. alarm.setAlarmStatus(0);
  175. alarm.setCreateTime(LocalDateTime.now());
  176. alarm.setRemark(alarmCount == 1
  177. ? String.format("漏失监测单次独立采样确认:rms≥阈值且在漏失频段[%.0f,%.0f]Hz", lowerFreq, upperFreq)
  178. : String.format("漏失监测确认漏失:最近%d分钟内%d次采样rms≥阈值且在漏失频段[%.0f,%.0f]Hz",
  179. windowMinutes, count, lowerFreq, upperFreq));
  180. try {
  181. requireSaved(alarmDataService.saveAlarmData(alarm));
  182. log.info("漏失告警入库成功: encode={}, count={}", encode, count);
  183. return true;
  184. } catch (Exception e) {
  185. log.error("漏失告警入库失败: encode={}", encode, e);
  186. return false;
  187. }
  188. }
  189. private String buildWarningContent(String encode, long count, Double maxValue, double lowerFreq, double upperFreq) {
  190. return String.format("【系统自动预警】设备[%s]疑似漏失:最近%d分钟内%d次采样rms≥%s且中心频率落在漏失频段[%.0f,%.0f]Hz,待确认",
  191. encode, windowMinutes, count, Objects.toString(maxValue, "-"), lowerFreq, upperFreq);
  192. }
  193. }