当前位置: 代码网 > it编程>编程语言>Java > 基于SpringBoot和Vue的实时设备告警系统全链路实现方案

基于SpringBoot和Vue的实时设备告警系统全链路实现方案

2026年07月22日 Java 我要评论
本文系统梳理一个"实时监控告警"功能从数据采集 → 告警判定 → 持久化 → 接口 暴露 → 前端展示与提醒 → 闭环处理 &ra

本文系统梳理一个"实时监控告警"功能从数据采集 → 告警判定 → 持久化 → 接口 暴露 → 前端展示与提醒 → 闭环处理 → 兜底补偿的完整技术链路。涵盖各环节的知识点、设计要点与与具体业务无关的通用示例代码,可作为任意"iot/监控/工单"类系统的告警模块设计参考。

一、整体架构与数据流

一个典型的实时告警系统由五段构成:

┌──────────┐   采集    ┌──────────┐  判定/去重  ┌──────────┐
│ 数据源    │ ───────▶ │ 采集任务  │ ─────────▶ │  告警表   │
│(设备/api)│  周期轮询  │(定时/流) │  生成/闭环  │ (db)     │
└──────────┘           └──────────┘            └────┬─────┘
                                                     │ rest
                                              ┌──────▼─────┐   推送/轮询  ┌─────────┐
                                              │ 后端接口层  │ ──────────▶ │ 前端界面 │
                                              └────────────┘             │ +提醒    │
                                                                         └─────────┘

核心设计原则:

  • 采集与判定解耦:采集只负责拿到原始状态,判定逻辑独立,方便扩展新告警类型。
  • 状态化而非事件流:告警落库为"一条有生命周期的记录"(有 start/stop),而不是无穷无尽的日志流。这样才能做"当前未恢复告警"查询。
  • 前端只读 + 幂等操作:前端不参与告警产生,只做展示与"确认/处理"这类幂等状态流转。

二、告警生成层:边沿检测与状态机

2.1 电平 vs 边沿

采集到的原始状态通常是电平信号(布尔量:故障=true / 正常=false)。如果每个采集周期只要是 true 就插一条告警,2 秒一采集 会瞬间刷出成千上万条重复记录。

正确做法是边沿检测(edge detection):只在状态发生跳变时动作。

边沿上一次本次动作
上升沿falsetrue生成新告警(记录 start_time)
下降沿truefalse关闭告警(记录 stop_time、时长)
保持相同相同什么都不做

2.2 通用示例:边沿检测器

/**
 * 通用布尔量边沿检测器,线程安全
 * key 通常是 "设备id:信号名"
 */
public class edgedetector {

    private final map<string, boolean> laststate = new concurrenthashmap<>();

    public enum edge { rising, falling, none }

    public edge detect(string key, boolean current) {
        boolean prev = laststate.put(key, current);
        if (prev == null) {
            // 首次采集:把 true 视为上升沿,false 视为无事件
            return current ? edge.rising : edge.none;
        }
        if (!prev && current) return edge.rising;
        if (prev && !current) return edge.falling;
        return edge.none;
    }
}

2.3 在采集周期中使用

edgedetector detector = new edgedetector();

void onsample(string deviceid, string signal, boolean faultbit, date now) {
    string key = deviceid + ":" + signal;
    switch (detector.detect(key, faultbit)) {
        case rising:
            alarmservice.open(deviceid, signal, now);   // 生成告警
            break;
        case falling:
            alarmservice.close(deviceid, signal, now);  // 闭合告警
            break;
        default:
            // 状态未变,忽略
    }
}

状态存哪里? 单机可用内存 map;多实例/需重启不丢,可把"上一次状态"放 redis,或直接以数据库中"是否存在未闭合告警"作为判断依据(见第三节,最稳,无需额外状态存储)。

三、告警去重与生命周期

比内存边沿更可靠的方案:以数据库中"是否存在未闭合记录"作为状态源。这样天然幂等、重启不丢、多实例安全。

3.1 生命周期模型

一条告警记录有三个关键时间点与两个派生字段:

start_time ──────(持续中)──────▶ stop_time
                                  total_time = stop - start(秒)
handle_status: 待确认 → 已确认 → 已处理(或 超时未确认)

3.2 开告警(带去重)

public void open(long deviceid, string alarmname, date starttime) {
    // 去重:若已存在该设备该类型"未闭合"告警,则不再新建
    if (mapper.selectunfinished(deviceid, alarmname) != null) {
        return;
    }
    alarm a = new alarm();
    a.setdeviceid(deviceid);
    a.setalarmname(alarmname);
    a.setstarttime(starttime);
    a.setalarmlevel(resolvelevel(alarmname));   // 名称→等级映射
    a.sethandlestatus(status_pending);          // 默认待确认
    mapper.insert(a);
}

3.3 关告警(计算时长)

public boolean close(long deviceid, string alarmname, date stoptime) {
    alarm a = mapper.selectunfinished(deviceid, alarmname);
    if (a == null) return false;                // 没有未闭合记录,忽略

    long seconds = (stoptime.gettime() - a.getstarttime().gettime()) / 1000;
    return mapper.updatestop(a.getid(), stoptime, seconds) > 0;
}

3.4 名称 → 等级映射(可配置化)

把"哪个告警多严重"抽成映射表,未来加告警只改配置,不改逻辑:

public class alarmlevel {
    public static final int info = 1, warn = 2, serious = 3, fatal = 4;

    private static final map<string, integer> map = new hashmap<>();
    static {
        map.put("急停", fatal);
        map.put("高压报警", serious);
        map.put("低流量报警", warn);
        // ...
    }
    /** 未命中默认按严重处理,保证"漏配也不漏报" */
    public static int of(string name) {
        return map.getordefault(name, serious);
    }
}

更进一步可把映射表放数据库字典表(sys_dict_data)或独立阈值配置表,做到运营可视化配置。

四、数据库层设计

4.1 建表 ddl(通用示例)

create table device_alarm (
    id            bigint       not null auto_increment comment '主键',
    device_id     bigint       not null               comment '设备id',
    alarm_name    varchar(64)  not null               comment '告警名称/类型',
    alarm_level   tinyint      not null default 3      comment '等级 1提示2预警3严重4致命',
    start_time    datetime     not null               comment '开始时间',
    stop_time     datetime     null                   comment '结束时间(null=未恢复)',
    total_time    int          null                   comment '持续秒数',
    handle_status tinyint      not null default 0      comment '0待确认1已确认2超时3已处理',
    handle_user   varchar(64)  null                   comment '处理人',
    handle_time   datetime     null                   comment '处理时间',
    handle_remark varchar(500) null                   comment '处理备注',
    create_time   datetime     not null               comment '创建时间',
    primary key (id),
    key idx_device_unfinished (device_id, alarm_name, stop_time),
    key idx_stop_time (stop_time),
    key idx_create_time (create_time)
) engine=innodb default charset=utf8mb4 comment='设备告警表';

4.2 索引设计要点

查询场景索引说明
“该设备该类型是否有未闭合告警”(去重、闭合)(device_id, alarm_name, stop_time)最高频写路径,联合索引命中
“当前所有未恢复告警”(大屏轮询)idx_stop_timewhere stop_time is null
历史分页/导出idx_create_time按时间倒序翻页

stop_time is null 表示未恢复——用 null 而不是额外的 is_finished 布尔字段,语义清晰且能与时间字段复用索引。

4.3 关键 sql

-- 查未闭合(去重判断)
select * from device_alarm
where device_id = #{deviceid} and alarm_name = #{alarmname}
  and stop_time is null limit 1;
-- 闭合告警
update device_alarm
set stop_time = #{stoptime}, total_time = #{seconds}
where id = #{id} and stop_time is null;   -- 加 stop_time is null 防并发重复闭合
-- 当前未恢复列表
select a.*, d.device_name
from device_alarm a left join device d on a.device_id = d.id
where a.stop_time is null
order by a.alarm_level desc, a.start_time desc;

并发防护update ... where id=? and stop_time is null 利用行锁 + 条件,天然防止两个线程重复闭合同一告警(第二个 update 影响 0 行)。

五、后端接口层:rest api 设计

5.1 接口清单(restful + 语义动作)

方法路径用途
get/alarm/list分页历史查询
get/alarm/current当前未恢复告警(前端轮询)
get/alarm/{id}详情
post/alarm/confirm/{id}确认告警
post/alarm/batchconfirm批量确认
post/alarm/handle处理闭环(带备注)
post/alarm/export导出 excel

查询用 rest 名词 + get;"确认/处理"这类状态流转用动词子路径(/confirm/handle),比强行 put 整个对象更清晰、更安全(避免前端误改其他字段)。

5.2 通用控制器示例

@restcontroller
@requestmapping("/alarm")
public class alarmcontroller {

    @autowired private alarmservice service;

    /** 前端轮询:当前所有未恢复告警 */
    @getmapping("/current")
    public result<list<alarm>> current() {
        return result.ok(service.listunfinished());
    }

    /** 确认(幂等) */
    @postmapping("/confirm/{id}")
    public result<void> confirm(@pathvariable long id) {
        service.confirm(id, currentuser());
        return result.ok();
    }

    /** 处理闭环 */
    @postmapping("/handle")
    public result<void> handle(@requestparam long id,
                               @requestparam(required = false) string remark) {
        service.handle(id, remark, currentuser());
        return result.ok();
    }
}

5.3 权限与审计

  • 每个写接口加权限校验(如 spring security @preauthorize)。
  • "确认/处理"记录操作人 + 操作时间,形成审计链,责任可追溯。

六、前端展示层:三种实时推送方案对比

前端要"实时"感知新告警,有三条技术路线:

方案原理优点缺点适用
轮询 pollingsetinterval 定时请求实现最简单、无长连接、兼容强有延迟、有空请求开销秒级实时够用、部署简单
sseeventsource 服务端单向推原生断线重连、比 ws 轻单向、老 ie 不支持只需服务端→客户端推送
websocket全双工长连接真正实时、双向需维护连接/心跳/鉴权高频、双向交互

6.1 轮询(最常用,通用示例)

class alarmpoller {
  constructor(fetchfn, { interval = 5000, onnew } = {}) {
    this.fetchfn = fetchfn;      // 返回 promise<alarm[]>
    this.interval = interval;
    this.onnew = onnew;
    this.knownids = new set();
    this.timer = null;
  }

  start() {
    this.tick();                 // 立即拉一次
    this.timer = setinterval(() => this.tick(), this.interval);
  }

  async tick() {
    try {
      const list = await this.fetchfn();
      // 找出本轮"新出现"的告警
      const fresh = list.filter(a => !this.knownids.has(a.id));
      list.foreach(a => this.knownids.add(a.id));
      // 清理已恢复的 id,防止 set 无限膨胀
      const alive = new set(list.map(a => a.id));
      this.knownids = new set([...this.knownids].filter(id => alive.has(id)));
      if (fresh.length && this.onnew) this.onnew(fresh);
    } catch (e) {
      console.error('轮询告警失败', e);   // 失败不中断下一轮
    }
  }

  stop() { clearinterval(this.timer); this.timer = null; }
}

// 用法
const poller = new alarmpoller(() => api.getcurrentalarms(), {
  interval: 5000,
  onnew: (alarms) => alarms.foreach(a => alarmmanager.alarm({
    title: a.devicename, body: a.alarmname, level: maplevel(a.alarmlevel),
  })),
});
poller.start();

轮询的两个关键技巧:

  1. "新增"判定用 id 集合 diff,不要每次全量弹提醒,否则同一告警会反复响。
  2. 及时清理已恢复 id,避免 set 内存泄漏。

6.2 sse(服务端主动推)

// 前端
const es = new eventsource('/alarm/stream');
es.addeventlistener('alarm', (e) => {
  const alarm = json.parse(e.data);
  alarmmanager.alarm({ title: alarm.devicename, body: alarm.alarmname });
});
es.onerror = () => console.warn('sse 断开,浏览器将自动重连');
// 后端 spring mvc
@getmapping(value = "/alarm/stream", produces = mediatype.text_event_stream_value)
public sseemitter stream() {
    sseemitter emitter = new sseemitter(0l);       // 不超时
    emitterregistry.add(emitter);                  // 存起来,产生告警时广播
    emitter.oncompletion(() -> emitterregistry.remove(emitter));
    return emitter;
}
// 产生新告警时:emitter.send(sseemitter.event().name("alarm").data(alarm));

6.3 websocket(双向)

const ws = new websocket('wss://host/ws/alarm');
ws.onmessage = (e) => {
  const msg = json.parse(e.data);
  if (msg.type === 'alarm') alarmmanager.alarm(msg.payload);
};
// 心跳保活
setinterval(() => ws.readystate === 1 && ws.send('ping'), 30000);

选型建议:秒级监控大屏优先轮询(最稳、运维成本最低);需要毫秒级或服务端主动推且不想维护重连时用 sse;有双向指令交互(如页面下发控制命令)才上 websocket

七、前端提醒层

新告警到达后,触发"提醒三件套":

  • web audio 蜂鸣:按告警等级播放不同急促度的合成音,无需音频文件。
  • 浏览器 notification:仅在 document.visibilitystate !== 'visible'(用户切走)时弹系统通知,避免前台重复骚扰。
  • 标题 / favicon 红点角标:后台时用 document.title 计数 + canvas 绘制 favicon 角标,把用户"拉回来"。

统一入口示例:

poller.onnew = (alarms) => {
  alarms.foreach(a => alarmmanager.alarm({
    title: a.devicename,
    body: a.alarmname,
    level: maplevel(a.alarmlevel),
    tag: 'alarm-' + a.id,        // 去重,防刷屏
  }));
};

八、闭环处理:确认 / 处理 / 超时

告警不能只"响",必须能被人闭环,否则等于噪音。典型状态机:

        产生
待确认 ───────▶ (用户点确认) ──▶ 已确认 ──▶ (处理完+备注) ──▶ 已处理
   │
   └─(超过n分钟无人确认)──▶ 超时未确认   ← 用于考核值班响应

8.1 确认(幂等)

public void confirm(long id, string user) {
    alarm a = new alarm();
    a.setid(id);
    a.sethandlestatus(status_confirmed);
    a.sethandleuser(user);
    a.sethandletime(new date());
    mapper.updateselective(a);      // 只更新非空字段
}

8.2 前端确认交互

async function confirmalarm(id) {
  await api.confirm(id);
  this.$message.success('已确认');
  this.refresh();          // 刷新当前告警列表
  alarmmanager.clear();    // 清角标/停声音
}

幂等性:确认/处理接口要能被重复调用而不出错(重复点、网络重试)。用"设置目标状态"而非"状态+1"即天然幂等。

九、兜底补偿:僵尸告警与数据自愈

实时系统一定要考虑异常路径,否则数据会失真:

9.1 两类典型脏数据

问题成因后果
僵尸告警恢复边沿丢失(设备离线、缓存过期、进程重启)stop_time 永远 null,大屏一直红
超时未确认无人值守,告警长期挂"待确认"无法考核响应,告警堆积

9.2 兜底任务(定时扫描自愈)

/** 每分钟跑一次,修复未闭合告警 */
public void fallback() {
    date now = new date();
    for (alarm a : mapper.selectallunfinished()) {
        long elapsedmin = (now.gettime() - a.getstarttime().gettime()) / 60000;

        // 1) 超时未确认 → 标记
        if (a.gethandlestatus() == status_pending && elapsedmin >= timeout_min) {
            mapper.marktimeout(a.getid());
        }

        // 2) 僵尸告警 → 若数据源已恢复正常/持续离线超阈值,则强制关闭
        boolean live = readcurrentstate(a);           // 从缓存/实时源读当前状态
        if (boolean.false.equals(live)) {
            mapper.forceclose(a.getid(), now, "点位已恢复,兜底关闭");
        } else if (live == null && offlinetoolong(a)) {
            mapper.forceclose(a.getid(), now, "设备持续离线,兜底关闭");
        }
    }
}

9.3 防误关的两个细节

  • 离线要"持续超阈值"才关:用一个 map<key, 首次离线时间> 记录,避免瞬时网络抖动误关正常告警。
  • 区分"离线"与"恢复":数据源读不到(null)= 离线,读到且为正常 = 真恢复,两者处置不同。

十、关键设计要点汇总

要点
采集/判定用边沿检测而非电平,避免海量重复;判定逻辑与采集解耦
去重以"数据库是否存在未闭合记录"为状态源,幂等且重启不丢
生命周期stop_time is null 表示未恢复;闭合时算 total_time
并发闭合 update 带 and stop_time is null 防重复闭合
等级名称→等级映射表化/字典化,漏配默认从严
数据库联合索引 (device_id, alarm_name, stop_time) 覆盖写路径
接口查询用 rest 名词,状态流转用动词子路径;写接口鉴权 + 审计
推送选型秒级用轮询,单向实时用 sse,双向用 websocket
前端去重id 集合 diff 判断"新增",并清理已恢复 id 防内存泄漏
提醒声音 + 仅后台通知 + 角标;用 tag 去重防刷屏
闭环确认/处理幂等,记录操作人与时间
兜底定时扫描修复僵尸告警与超时未确认;防抖动误关
健壮性轮询/推送失败不中断循环;异常路径必须有兜底

以上即"实时设备告警"功能从数据源到界面的全链路技术详解。核心思想可提炼为三句话:采集判定用边沿、状态落库带生命周期、异常路径必有兜底;而前端则遵循只读展示 + 幂等闭环 + 恰到好处的提醒。这套模式可直接迁移到设备监控、服务器运维告警、业务风控预警等任意实时告警场景。

以上就是基于springboot和vue的实时设备告警系统全链路实现方案的详细内容,更多关于springboot vue实时设备告警系统的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com