
近期新闻里连续出现了多起与公共安全相关的突发事件既有轨道交通工具上的暴力行为也有飞行器内部的人员冲突还包括极端天气引发的次生灾害。这些事件虽然具体场景各不相同但暴露出的问题却很一致从异常发生到相关人员收到通知中间的信息传递链路往往太长。处置人员早一秒收到准确的事件位置、类型和现场状态就可能完全改变后续结果。本文不讨论具体事件本身而是从这些新闻背后的技术需求切入带大家从零搭建一套“公共安全异常事件预警与应急通知系统”。这套系统会覆盖事件接入、规则判断、分级告警、实时推送的完整链路。无论你是后端开发者、物联网方向的学生还是准备做应急管理相关项目的工程师都能从中拿到可运行的代码和可落地的设计思路。1. 背景与核心概念1.1 公共安全应急系统要解决什么问题公共安全领域的突发事件有几个共性特点突发性强、时间窗口短、涉及系统多。比如一栋楼里安装了烟雾传感器、门禁设备、摄像头、求助按钮正常情况下这些设备各管各的数据互不相通。一旦某处出现异常值班人员需要在多个后台系统之间来回切换才能拼凑出现场情况。这显然不符合应急场景的需要。更合理的做法是建立一个统一的事件接入层让不同来源的异常信号都变成标准格式的事件对象再交给规则引擎判断“这个事件要不要告警、属于几级告警、需要通知谁”最后通过短信、邮件、WebSocket、大屏等多种渠道触达相关人员。这套思路听起来不复杂但真正落地时会遇到协议不统一、事件量突增、通知重复、权限混乱等一系列问题。本文要实现的系统就是把这些问题逐个拆开、逐个解决。1.2 核心概念说明在进入编码之前需要先明确几个基础概念。事件Event事件是系统中流转的最小信息单元。它描述的是“在什么时间、什么地点、发生了什么类型的事情”。事件与告警是两回事事件是事实告警是对事件的判断结果。同一个事件不同规则可能判断出不同级别的告警。事件源Event Source产生事件的设备或服务。常见事件源包括安防摄像头、烟雾传感器、一键报警按钮、气象监测站、GPS定位终端等。本文中我们会为事件源设计一个统一的抽象接口后续新增事件源时不需要改动核心代码。规则引擎Rule Engine一段负责把事件翻译成告警的代码逻辑。最简单的方式是“阈值判断条件组合”例如事件类型为“入侵报警”且次数在5分钟内超过3次则升级为高级别告警。生产级系统通常使用 Drools、Easy Rules 等框架但本文为了便于理解会先实现一个轻量级规则引擎。通知渠道Notification Channel把告警信息送达用户的途径。常见渠道有邮件、短信、App Push、WebSocket实时消息、电话语音。不同级别告警应该使用不同渠道例如低级告警发邮件即可高级告警需要短信电话。1.3 为什么后端开发要关注这套系统有人会觉得应急预警是安防公司的业务和后端没太大关系。但实际上几乎所有带“实时”属性的系统都在做类似的事情日志系统里“错误率超过阈值就告警”电商系统里“风控识别异常订单就拦截”物联网平台里“设备离线就通知运维”。公共安全事件预警系统只是这些场景里最有代表性的一种。学会了事件模型设计、规则判断、多级通知这套方法论你在其他业务场景里也可以复用。这篇文章的价值不在于那一段代码而在于一套可迁移的系统设计思路。2. 环境准备与版本说明2.1 技术选型为了兼顾入门门槛和工程实用性本文选择 Spring Boot 搭建系统骨架理由有三点Spring Boot 提供了 Web、WebSocket、邮件发送、数据存储等开箱即用的能力适合快速实现演示项目。绝大多数目标读者对 Spring Boot 比较熟悉代码更容易迁移到自己的项目中。规则引擎部分使用 Java 实现后续如果要替换成 Drools、Easy Rules迁移成本也很低。具体技术栈如下JDK需要 8 及以上版本建议使用 11 或 17。构建工具Maven 3.6。框架Spring Boot 2.7.x本文示例使用 2.7.18其他 2.x 版本基本兼容。数据存储为了降低部署成本演示阶段使用 H2 内存数据库生产环境可以无缝替换为 MySQL 或 PostgreSQL。前端展示使用 WebSocket 一个极简 HTML 页面用来演示实时告警推送效果。需要说明的是版本号请根据你本地的环境灵活调整。Spring Boot 3.x 的部分 API 有变化如果你使用 3.x注意javax包名改成了jakarta以及 WebSocket 相关类路径的变化。2.2 开发工具准备你需要准备以下工具IntelliJ IDEA 或 Eclipse推荐 IDEA Community 版即可。Postman 或 Apifox用于发送模拟事件请求。一个可用的邮箱用于接收告警邮件本文使用 SMTP 发送演示邮件如果你不想配置邮箱可以跳过邮件部分不影响其他功能。2.3 项目结构规划为了让代码结构清晰先规划好项目的包结构。public-safety-alert ├── pom.xml └── src/main ├── java/com/example/safety │ ├── SafetyAlertApplication.java │ ├── common │ │ ├── ApiResponse.java │ │ └── EventType.java │ ├── event │ │ ├── Event.java │ │ ├── EventController.java │ │ ├── EventService.java │ │ └── EventRepository.java │ ├── rule │ │ ├── RuleEngine.java │ │ └── AlertLevel.java │ └── notify │ ├── Notifier.java │ ├── EmailNotifier.java │ └── WebSocketNotifier.java └── resources ├── application.yml └── static/index.html这个结构遵循一个简单的分层思想common放通用定义event管事件接入和存储rule管判断逻辑notify管通知渠道。后续扩展时只需要在这几个包下增加新类不需要大范围改动已有代码。3. 核心设计与关键代码拆解3.1 事件模型设计事件是贯穿整个系统的核心数据结构。设计事件模型时必须想清楚两个问题哪些字段是所有事件都有的哪些字段是特定事件独有的。所有事件都具备的基础字段有eventId事件唯一编号用于幂等处理。sourceId事件源标识用来区分是哪个设备或系统上报的。sourceType事件源类型如 CAMERA、SENSOR、PANIC_BUTTON、WEATHER。eventType事件类型如 INTRUSION、SMOKE、FLOOD、ABNORMAL_STATUS。location事件发生位置尽量使用结构化地址或经纬度。reportedAt事件上报时间。payload原始数据用 JSON 字符串保存用来存放各事件源的扩展信息。这里要特别强调payload字段。很多人第一次设计事件系统时喜欢为每一种事件类型单独建一张表结果事件源一多表结构就失控了。更推荐的做法是保留核心公共字段把差异化的内容全部放进 payload既能保持灵活又方便对接新事件源。事件实体代码如下// 文件路径src/main/java/com/example/safety/event/Event.java package com.example.safety.event; import java.time.LocalDateTime; public class Event { private String eventId; private String sourceId; private String sourceType; private String eventType; private String location; private LocalDateTime reportedAt; private String payload; public Event() { } public Event(String eventId, String sourceId, String sourceType, String eventType, String location, LocalDateTime reportedAt, String payload) { this.eventId eventId; this.sourceId sourceId; this.sourceType sourceType; this.eventType eventType; this.location location; this.reportedAt reportedAt; this.payload payload; } // getter / setter 省略实际代码需要补全 public String getEventId() { return eventId; } public void setEventId(String eventId) { this.eventId eventId; } public String getSourceType() { return sourceType; } public void setSourceType(String sourceType) { this.sourceType sourceType; } public String getEventType() { return eventType; } public void setEventType(String eventType) { this.eventType eventType; } public String getLocation() { return location; } public void setLocation(String location) { this.location location; } public LocalDateTime getReportedAt() { return reportedAt; } public void setReportedAt(LocalDateTime reportedAt) { this.reportedAt reportedAt; } public String getPayload() { return payload; } public void setPayload(String payload) { this.payload payload; } public String getSourceId() { return sourceId; } public void setSourceId(String sourceId) { this.sourceId sourceId; } }3.2 事件类型与告警级别定义事件类型和告警级别都属于“系统字典”应该用枚举定义避免字符串散落在代码各处。事件类型初始定义以下四种INTRUSION入侵事件对应安防摄像头或门磁报警。SMOKE烟雾事件对应烟雾传感器。FLOOD水浸/洪水事件对应水位监测设备。ABNORMAL_STATUS异常状态事件可以扩展为人员行为异常、设备运行异常等。// 文件路径src/main/java/com/example/safety/common/EventType.java package com.example.safety.common; public enum EventType { INTRUSION(入侵), SMOKE(烟雾), FLOOD(水浸), ABNORMAL_STATUS(异常状态); private final String description; EventType(String description) { this.description description; } public String getDescription() { return description; } }告警级别建议分为三级LOW、MEDIUM、HIGH。级别决定了通知渠道的选择。// 文件路径src/main/java/com/example/safety/rule/AlertLevel.java package com.example.safety.rule; public enum AlertLevel { LOW(1), MEDIUM(2), HIGH(3); private final int priority; AlertLevel(int priority) { this.priority priority; } public int getPriority() { return priority; } }3.3 事件存储设计演示项目使用 H2 内存数据库。这里选择用 Spring Data JPA 来访问数据核心接口非常简单。// 文件路径src/main/java/com/example/safety/event/EventRepository.java package com.example.safety.event; import org.springframework.data.jpa.repository.JpaRepository; import java.time.LocalDateTime; import java.util.List; public interface EventRepository extends JpaRepositoryEvent, Long { long countByEventTypeAndReportedAtAfter(String eventType, LocalDateTime time); ListEvent findAllByOrderByReportedAtDesc(); }countByEventTypeAndReportedAtAfter这个方法是后面实现“时间窗口频次规则”的关键它能在指定时间段内统计某种事件出现的次数。这种写法利用了 Spring Data JPA 的方法命名约定不需要手写 SQL。3.4 规则引擎设计规则引擎是系统的“大脑”。文章开头提到公共安全场景里的规则往往并不复杂真正复杂的是规则的组合方式和触发条件。本文的规则引擎设计思路如下接收一个事件对象。根据事件类型判断是否命中某个规则。命中后计算告警级别。把告警信息发送到通知模块。一个完整的规则分为三部分规则名称、触发条件、动作。为了让代码演示直观我们先实现三个核心规则任何事件都至少产生 LOW 级别告警。同类型事件在 5 分钟内出现 3 次以上升级为 MEDIUM 告警。事件类型是 FLOOD 或 INTRUSION 时直接升级为 HIGH 告警。// 文件路径src/main/java/com/example/safety/rule/RuleEngine.java package com.example.safety.rule; import com.example.safety.common.EventType; import com.example.safety.event.Event; import com.example.safety.event.EventRepository; import org.springframework.stereotype.Component; import java.time.LocalDateTime; Component public class RuleEngine { private final EventRepository eventRepository; public RuleEngine(EventRepository eventRepository) { this.eventRepository eventRepository; } public AlertLevel evaluate(Event event) { // 规则1所有事件都至少是 LOW 告警 AlertLevel level AlertLevel.LOW; // 规则25分钟内同类型事件超过3次提升为 MEDIUM LocalDateTime windowStart event.getReportedAt().minusMinutes(5); long count eventRepository .countByEventTypeAndReportedAtAfter(event.getEventType(), windowStart); if (count 3) { level AlertLevel.MEDIUM; } // 规则3水浸和入侵事件直接升级为 HIGH if (EventType.FLOOD.name().equals(event.getEventType()) || EventType.INTRUSION.name().equals(event.getEventType())) { level AlertLevel.HIGH; } return level; } }这段代码看似简单但它体现了规则引擎最核心的思维把“判断”和“执行”分离。规则引擎只负责返回告警级别至于邮件通知还是 WebSocket 推送交给 Notifier 去处理。这样后续增加规则时不会影响通知模块反过来调整通知策略时也不影响规则判断。3.5 通知渠道抽象通知模块使用策略模式先定义一个统一接口再分别实现邮件和 WebSocket 两种渠道。// 文件路径src/main/java/com/example/safety/notify/Notifier.java package com.example.safety.notify; import com.example.safety.event.Event; import com.example.safety.rule.AlertLevel; public interface Notifier { boolean supports(AlertLevel level); void notify(Event event, AlertLevel level); }supports方法用来判断当前通知器是否支持某个告警级别。比如邮件通知器可以支持所有级别WebSocket 通知器可以只支持 MEDIUM 和 HIGH实际项目中还可能有短信通知器只支持 HIGH。这种设计让通知渠道与告警级别形成清晰的对应关系。// 文件路径src/main/java/com/example/safety/notify/EmailNotifier.java package com.example.safety.notify; import com.example.safety.event.Event; import com.example.safety.rule.AlertLevel; import org.springframework.mail.SimpleMailMessage; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.stereotype.Component; Component public class EmailNotifier implements Notifier { private final JavaMailSender mailSender; public EmailNotifier(JavaMailSender mailSender) { this.mailSender mailSender; } Override public boolean supports(AlertLevel level) { return true; } Override public void notify(Event event, AlertLevel level) { SimpleMailMessage message new SimpleMailMessage(); message.setTo(adminexample.com); message.setSubject([告警] level - event.getEventType()); message.setText(buildContent(event, level)); mailSender.send(message); } private String buildContent(Event event, AlertLevel level) { return 告警级别 level \n事件编号 event.getEventId() \n事件类型 event.getEventType() \n事件位置 event.getLocation() \n上报时间 event.getReportedAt() \n原始数据 event.getPayload(); } }邮件通知的关键点是邮件正文必须完整不能只写一句“有告警”。当处置人员收到邮件时邮件里的事件编号、位置、时间和原始数据就是他们判断现场情况的依据。这也是公共安全系统里“信息完整性”的要求。3.6 实时推送与前端页面WebSocket 是实时告警推送的主力渠道。Spring Boot 对 WebSocket 的支持比较完善只需要配置一个WebSocketHandler和握手拦截器即可。// 文件路径src/main/java/com/example/safety/notify/WebSocketNotifier.java package com.example.safety.notify; import com.example.safety.event.Event; import com.example.safety.rule.AlertLevel; import org.springframework.stereotype.Component; import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.handler.TextWebSocketHandler; import java.util.concurrent.CopyOnWriteArraySet; Component public class WebSocketNotifier extends TextWebSocketHandler { private static final CopyOnWriteArraySetWebSocketSession SESSIONS new CopyOnWriteArraySet(); Override public void afterConnectionEstablished(WebSocketSession session) { SESSIONS.add(session); } Override public void afterConnectionClosed(WebSocketSession session, org.springframework.web.socket.CloseStatus status) { SESSIONS.remove(session); } public void pushMessage(String message) { TextMessage textMessage new TextMessage(message); for (WebSocketSession session : SESSIONS) { try { if (session.isOpen()) { session.sendMessage(textMessage); } } catch (Exception e) { // 单次推送失败不影响其他会话 } } } }WebSocket 通知器没有实现Notifier接口因为它并不是被动地被规则引擎调用而是作为消息广播组件由通知编排器统一调用。它的职责很清晰维护一组在线会话并向这些会话广播消息。为了让前端可以实时展示告警还需要一个简单的 HTML 页面。这个页面通过 JavaScript 建立 WebSocket 连接收到服务端消息后动态渲染到列表里。!-- 文件路径src/main/resources/static/index.html -- !DOCTYPE html html langzh head meta charsetUTF-8 title公共安全告警大屏/title /head body h2实时告警推送/h2 div idalertList/div script const socket new WebSocket(ws://localhost:8080/ws/alert); const alertList document.getElementById(alertList); socket.onopen function () { console.log(WebSocket 已连接); }; socket.onmessage function (event) { const alertData JSON.parse(event.data); const div document.createElement(div); div.innerHTML [ alertData.alertLevel ] alertData.eventType alertData.location; alertList.prepend(div); }; socket.onclose function () { console.log(WebSocket 连接关闭); }; /script /body /html这里使用prepend让新告警排在最上方符合值班人员“先看最新消息”的习惯。4. 完整实战案例4.1 创建项目并引入依赖首先创建 Spring Boot 项目。可以用 Spring Initializr 生成也可以直接创建 Maven 项目后手动编写pom.xml。完整的pom.xml如下!-- 文件路径pom.xml -- ?xml version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent groupIdcom.example/groupId artifactIdpublic-safety-alert/artifactId version1.0.0/version namepublic-safety-alert/name description公共安全异常事件预警与应急通知系统演示/description properties java.version11/java.version /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-websocket/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-mail/artifactId /dependency dependency groupIdcom.h2database/groupId artifactIdh2/artifactId scoperuntime/scope /dependency /dependencies build plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId /plugin /plugins /build /project这里需要注意spring-boot-starter-mail依赖如果没有配置邮箱启动本身不会报错但发送邮件时会抛出MailSendException。所以如果你不想配置邮箱可以在调用邮件通知器前通过配置开关控制。4.2 编写配置文件application.yml负责数据库、WebSocket 和邮件相关的配置。# 文件路径src/main/resources/application.yml server: port: 8080 spring: datasource: url: jdbc:h2:mem:safetydb;DB_CLOSE_DELAY-1 driver-class-name: org.h2.Driver username: sa password: h2: console: enabled: true jpa: hibernate: ddl-auto: update show-sql: true mail: host: smtp.example.com port: 465 username: your-account password: your-password properties: mail: smtp: auth: true ssl: enable: true app: alert: rules: window-minutes: 5 threshold-count: 3配置项说明spring.datasource.url使用 H2 内存库DB_CLOSE_DELAY-1保证应用退出前数据库不关闭。spring.jpa.hibernate.ddl-autoupdate让 JPA 根据实体自动建表适合演示环境。生产环境建议改为validate通过 Flyway 管理表结构。spring.mail的host、port、username、password需要替换成你自己的邮箱 SMTP 配置。如果你使用 QQ 邮箱需要开启 SMTP 服务并填写授权码而不是登录密码。app.alert.rules是自定义配置可以通过ConfigurationProperties绑定到规则参数对象。为了简化本文在规则引擎中直接写死了阈值实际项目应使用这种方式动态配置。4.3 实现事件接入接口接下来实现事件上报接口。外部设备或系统通过 HTTP POST 请求向该接口上报事件接口校验基础字段后调用服务层保存事件、执行规则、触发通知。// 文件路径src/main/java/com/example/safety/event/EventController.java package com.example.safety.event; import com.example.safety.common.ApiResponse; import org.springframework.web.bind.annotation.*; import java.time.LocalDateTime; import java.util.UUID; RestController RequestMapping(/api/events) public class EventController { private final EventService eventService; public EventController(EventService eventService) { this.eventService eventService; } PostMapping public ApiResponse report(RequestBody Event event) { if (event.getEventId() null || event.getEventId().isEmpty()) { event.setEventId(UUID.randomUUID().toString()); } if (event.getReportedAt() null) { event.setReportedAt(LocalDateTime.now()); } AlertResult result eventService.handleEvent(event); return ApiResponse.success(result); } }ApiResponse是一个简单的统一返回结构这里不多展开它包含code、message和data三个字段。EventService是整个流程的编排者它会按顺序完成保存事件、评估规则、发送通知三个动作。// 文件路径src/main/java/com/example/safety/event/EventService.java package com.example.safety.event; import com.example.safety.notify.EmailNotifier; import com.example.safety.notify.WebSocketNotifier; import com.example.safety.rule.AlertLevel; import com.example.safety.rule.RuleEngine; import org.springframework.stereotype.Service; import java.util.HashMap; import java.util.Map; Service public class EventService { private final EventRepository eventRepository; private final RuleEngine ruleEngine; private final EmailNotifier emailNotifier; private final WebSocketNotifier webSocketNotifier; public EventService(EventRepository eventRepository, RuleEngine ruleEngine, EmailNotifier emailNotifier, WebSocketNotifier webSocketNotifier) { this.eventRepository eventRepository; this.ruleEngine ruleEngine; this.emailNotifier emailNotifier; this.webSocketNotifier webSocketNotifier; } public AlertResult handleEvent(Event event) { // 1. 保存事件 eventRepository.save(event); // 2. 规则评估 AlertLevel level ruleEngine.evaluate(event); // 3. 通知推送 emailNotifier.notify(event, level); webSocketNotifier.pushMessage(buildMessage(event, level)); // 4. 返回结果给调用方 return new AlertResult(event.getEventId(), level); } private String buildMessage(Event event, AlertLevel level) { MapString, String map new HashMap(); map.put(eventId, event.getEventId()); map.put(eventType, event.getEventType()); map.put(location, event.getLocation()); map.put(alertLevel, level.name()); return map.toString(); } public static class AlertResult { private String eventId; private AlertLevel alertLevel; public AlertResult(String eventId, AlertLevel alertLevel) { this.eventId eventId; this.alertLevel alertLevel; } public String getEventId() { return eventId; } public AlertLevel getAlertLevel() { return alertLevel; } } }这里有几个工程细节值得说明pushMessage使用的是简单 Map 转 String实际项目中应该使用 Jackson 序列化为 JSON让前端可以解析结构化字段。邮件通知和 WebSocket 推送是同步执行的演示没问题。如果告警量大建议把通知逻辑异步化或者使用消息队列削峰。保存事件必须放在规则评估之前因为规则引擎依赖eventRepository.countByEventTypeAndReportedAtAfter统计当前事件。4.4 配置 WebSocket 端点还需要把/ws/alert这个路径和 WebSocket 处理器关联起来。// 文件路径src/main/java/com/example/safety/notify/WebSocketConfig.java package com.example.safety.notify; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.config.annotation.EnableWebSocket; import org.springframework.web.socket.config.annotation.WebSocketConfigurer; import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; Configuration EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { private final WebSocketNotifier webSocketNotifier; public WebSocketConfig(WebSocketNotifier webSocketNotifier) { this.webSocketNotifier webSocketNotifier; } Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(webSocketNotifier, /ws/alert); } }4.5 启动与验证启动主类// 文件路径src/main/java/com/example/safety/SafetyAlertApplication.java package com.example.safety; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; SpringBootApplication public class SafetyAlertApplication { public static void main(String[] args) { SpringApplication.run(SafetyAlertApplication.class, args); } }运行mvn spring-boot:run看到 “Started SafetyAlertApplication” 后用 Postman 发送一个模拟事件请求。请求地址POST http://localhost:8080/api/events请求体{ sourceId: CAM-001, sourceType: CAMERA, eventType: INTRUSION, location: 3号楼东侧门禁, payload: {\videoUrl\:\rtsp://192.168.1.100/live\} }预期响应{ code: 0, message: success, data: { eventId: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx, alertLevel: HIGH } }由于事件类型是INTRUSION规则引擎直接判定为 HIGH 告警。如果发送的是SMOKE事件同样的规则下会先返回 LOW直到 5 分钟内同类事件超过 3 次才会触发 MEDIUM。打开http://localhost:8080保持页面打开再发送上面的事件请求可以看到页面顶部立即出现了一条红色告警信息。这就完成了一条“事件接入 → 规则判断 → 实时推送”的完整链路。4.6 模拟高频事件触发频次规则为了验证规则2的效果连续发送 4 次SMOKE类型事件。第一次返回 LOW第二次返回 LOW第三次发送时规则引擎统计到 5 分钟内已有 3 条 SMOKE 事件返回结果就应该变成 MEDIUM。第四次以后都保持 MEDIUM直到时间窗口过期。这条逻辑在真实场景中很常用。单次烟雾报警可能是误报但短时间内多次报警就说明火情风险显著升高告警级别自然需要升级。这正是规则引擎存在的意义从零散事件里提取相对准确的结论。5. 常见问题与排查思路5.1 WebSocket 无法连接现象前端页面一直显示WebSocket 连接关闭控制台报错。可能原因WebSocket 地址写错ws://localhost:8080/ws/alert和注解路径不一致。项目使用了 Spring Security拦截了 WebSocket 握手请求。端口不对应用没有启动在 8080。排查步骤确认/ws/alert路径与WebSocketConfig中的地址一致。检查浏览器控制台的具体报错信息404 表示路径错了403 表示被权限拦截。如果项目有安全配置用CrossOrigin或 Security 配置放行 WebSocket 握手。5.2 邮件发送报错现象启动正常但上报事件时抛出MailSendException或AuthenticationFailedException。可能原因SMTP 邮箱账号密码错误。邮箱未开启 SMTP 服务。使用了错误的端口如 465 端口但协议配置却是 TLS。排查步骤检查邮箱设置确认 SMTP 服务是否开启。QQ 邮箱、163 邮箱需要使用“授权码”而不是登录密码。如果使用 465 端口确保mail.smtp.ssl.enabletrue如果使用 587 端口则使用mail.smtp.starttls.enabletrue。5.3 内存数据库数据丢失现象重启后历史事件数据全部消失。原因演示项目使用 H2 内存数据库数据只存在于应用运行期间。解决方案生产环境替换为 MySQL修改pom.xml和application.yml即可。替换后不要忘记添加 MySQL 驱动的 Maven 依赖。5.4 事件重复处理现象同一设备上报同一条事件系统推送了两次告警。原因HTTP 请求重试导致事件重复提交或者事件源本身重复上报。解决方案事件处理逻辑必须幂等。最简单的方案是在事件表上对eventId建唯一索引保存前先查重。如果事件是分批批量上报的还要考虑并发场景下的锁竞争。5.5 告警风暴现象短时间内大量事件涌入邮件和推送通道被打爆数据库写入压力同步升高。原因没有对事件量做限制和聚合。解决方案引入消息队列如 RabbitMQ、Kafka缓冲流量同时做时间窗口内的事件聚合。同一设备同一类型 1 分钟内有 100 条事件不必推送 100 条告警合并成一条汇总告警更合理。6. 最佳实践与工程建议6.1 事件模型要预留扩展字段公共安全场景的事件源特别多摄像头、传感器、呼叫按钮、车载终端每种设备的属性都不一样。如果在建模阶段就给每一种设备建一张表后续每接入一种新设备都要改表结构、写新的存储逻辑维护成本会迅速上升。更推荐的做法是“公共字段 payload JSON”的组合。公共字段用于查询和过滤payload 用于保留设备原始数据。如果需要针对 payload 里的某个字段做查询或统计再考虑把它提升为正式的公共字段或者使用数据库的 JSON 索引能力。6.2 告警必须分级不能一刀切所有事件都推送给所有值班人员最终结果一定是没人看消息。告警分级的意义不仅在于提醒程度更在于确定通知范围。本文使用的三级告警对应关系可以这样设计LOW记录并发送邮件供值班人员自行查阅。MEDIUM发送邮件 推送到值班大屏和 Web 端。HIGH在 MEDIUM 基础上增加短信、电话语音、相关负责人单独通知。这种分级策略能避免“狼来了”效应保证真正严重的事件不会被海量普通告警淹没。6.3 规则配置要外部化避免改代码本文在规则引擎里直接写死了阈值这是为了降低演示复杂度。实际项目中3 次/5 分钟这类参数必须做成动态配置最好能放到 Apollo、Nacos 这类配置中心里。规则配置外部化有几个好处值班人员调整告警敏感度时不需要重新发版。不同区域可以配置不同阈值。配置中心提供变更审计和回滚能力方便追踪谁在什么时间改了规则。6.4 通知模块必须做降级处理邮件服务可能宕机短信通道可能欠费WebSocket 服务可能因长连接过多而出现内存溢出。通知模块在全链路里属于“下游依赖”下游故障不应该影响事件接入主流程。实际项目中应做到事件接入和存储通过消息队列异步解耦通知服务作为独立消费者。通知失败要进入重试队列重试几次仍失败就告警。任何通知渠道异常都不能抛出异常影响规则引擎的结果返回。6.5 权限与安全边界公共安全系统往往涉及人员轨迹、视频信息、联系方式等敏感数据权限设计从第一天就要考虑。几个关键建议事件查询接口必须做权限校验不同角色只能看到自己管辖区域的事件。大屏推送内容如果包含敏感信息要基于 WebSocket 连接的用户身份做过滤。上传的 payload 原始数据要脱敏后再存储尤其是涉及人脸、车牌、手机号等个人信息的字段。操作日志要完整记录包括谁查看了事件、导出了哪些数据。6.6 提前规划数据生命周期公共安全事件数据在法律和业务上都有保存要求但并不能无限期保存。视频文件和原始报文体积大通常会做冷热分离近期数据放在高速存储超过一定时间后转存到冷存储或归档系统。在有数据库保存策略的场景下注意 H2 参数DB_CLOSE_DELAY-1只适用于演示。生产环境要提前规划好表分区、归档任务和清理任务避免数据库无限扩张。7. 总结与学习路线本文围绕一个相对完整的公共安全异常事件预警系统梳理了从事件模型、事件存储、规则引擎到邮件和 WebSocket 通知的完整链路。你可以用这套代码作为骨架替换数据源接入真实的设备协议再把规则配置搬入配置中心就能快速搭出一个可用的预警系统原型。接下来值得深入的方向有三个第一学习消息队列的接入把事件上报和通知推送解耦系统才能抗住瞬时流量。推荐从 RabbitMQ 或 Kafka 入手。第二研究规则引擎的进阶用法Easy Rules 和 Drools 都支持更复杂的条件组合和规则优先级适合业务规则多的场景。第三把 WebSocket 改成基于 MQTT 的移动端推送方案因为在野外或移动场景下值班人员依赖的还是手机 App 而不是电脑大屏。公共安全系统的价值不在于算法多高级而在于信息链路是否可靠。一次事件从发生到被看见中间哪怕只节省一分钟都可能在真实场景里发挥关键作用。希望这篇文章能给你提供一个可扩展的起点动手改一版、跑通一条完整告警链路比停留在纸面上理解要有效得多。