尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

救援调度系统设计:气象预警、状态机与地图可视化

救援调度系统设计:气象预警、状态机与地图可视化 在风暴、暴雨、台风等极端天气来临时救援调度系统需要在最短时间内把气象预警变成可执行的任务再交给一线救援队并持续跟踪每一条任务的进展。这类系统的难点不在单个接口能跑通而在数据来源多、任务状态变化快、现场网络不稳定、地图坐标经常不准确。下面以一套可运行的极端天气救援调度系统为例从数据接入、任务状态机、通知通道、地图可视化到生产环境排查逐步拆解设计思路和落地细节。文中代码用于说明实现思路实际项目要结合自己的包名、技术栈和第三方接口文档调整。1. 先理解极端天气救援场景下的系统需求1.1 这类系统要解决什么问题在极端天气新闻里“风暴过境、道路中断、多人被困”只是最后呈现的画面。画面背后是气象预警、灾情确认、救援队派发、现场反馈、任务关闭这一整条应急链条。救援调度系统不是用来替代人做判断的而是把判断之后的动作变成有状态的记录保证每一步都能追踪、每一条消息都能触达、每一次复盘都有数据支撑。具体来说系统要解决四类问题。第一预警信息不统一。不同来源的气象预警字段格式差异很大有的带行政区划编码有的只有经纬度有的时间是时间戳有的是日期字符串。如果不做标准化后续所有模块都会拿到脏数据。第二任务状态容易混乱。救援任务不是一条直线现场可能因道路中断暂停也可能因情况恶化转派给另一支队伍。如果没有统一流转规则直接在数据库里把状态从“待派发”改成“已完成”整个处理过程就失去了依据。第三通知到达率不可控。调度员确认任务后需要同时通知救援队负责人、值班领导甚至当事人。短信可能被拦截App 推送可能因为离线延迟群机器人消息也可能发送失败。通知模块必须设计重试和幂等否则会出现“没通知到”或“重复通知”两个极端。第四现场位置不精确。电话口述的位置转成坐标后偏差可能达到几百米。系统如果只展示原始坐标救援队到了现场还要二次找人。接入层至少要做坐标规范化并允许人工修正位置。1.2 核心链路是什么一次完整流程可以这样描述。气象预警接入模块定时拉取或接收第三方推送的预警报文。系统把报文标准化提取出时间、级别、受影响区域、影响范围。值班人员或自动规则把预警升级为灾情事件。调度模块根据区域和灾情类型推荐救援队伍。调度员确认后创建救援任务任务进入待派发状态。通知模块通过短信、App 推送、群机器人等通道通知救援队。救援队接单后任务进入执行中。现场持续上报位置和文字进展。任务完成后关闭所有状态变化被写入日志表形成完整链路。这条链路的核心数据实体包括预警事件、救援任务、状态日志、救援队伍和位置上报。把链路画清楚后模块边界自然也就清楚了。1.3 系统模块怎么划分接入模块负责气象接口、人工录入、Excel 导入。这个模块要能容忍上游字段变化最好在入库前做校验和映射而不是把上游字段直接塞进数据库。调度模块负责事件转任务、任务分配、状态流转。这是状态一致性要求最高的模块所有状态变化必须走同一个服务端入口。通知模块负责短信、App 推送、群机器人消息。不能只写一个 send 方法就结束还要处理通道故障、重试、幂等和失败告警。地图模块负责任务点、预警区域、热力图的展示同时处理坐标系转换和实时位置更新。统计模块负责响应时长、任务完成率、队伍负载等指标。统计依赖前面模块的数据质量所以排期上适合放到后面做但数据埋点要从一开始就设计好。2. 环境准备与项目结构2.1 技术栈选型与版本建议示例项目采用 Spring Boot 作为后端PostgreSQL 保存业务数据Redis 做去重和幂等缓存前端使用 Leaflet 展示地图。这个组合适合已有 Java 工程经验的团队。如果团队更熟悉 Python可以把 Spring Boot 换成 FastAPI核心设计不变。组件建议版本用途学习环境替代方案Spring Boot2.7 或 3.x后端 API、定时任务、WebSocketFastAPI 或 Node.jsPostgreSQL14 以上任务与日志持久化本地 Docker 容器Redis6 以上去重键、幂等键、缓存本地单机 RedisLeaflet1.9 左右Web 地图展示可先不做前端Spring WebSocket与 Spring Boot 匹配实时位置推送短轮询替代注意版本只是参考落地前要确认团队现有依赖。Spring Boot 从 2.7 升级到 3.x 时还需要检查 JDK 版本、Spring Cloud 和相关第三方库的兼容性。2.2 项目目录结构一个适用于中小团队的目录结构如下。rescue-dispatch/ ├── pom.xml ├── src/main/java/com/example/rescue/ │ ├── RescueApplication.java │ ├── controller/ │ │ ├── AlertController.java │ │ ├── TaskController.java │ │ └── LocationController.java │ ├── service/ │ │ ├── AlertIngestService.java │ │ ├── TaskService.java │ │ ├── TaskStateMachine.java │ │ └── NotifyService.java │ ├── repository/ │ │ ├── AlertEventRepository.java │ │ ├── RescueTaskRepository.java │ │ └── TaskStatusLogRepository.java │ ├── entity/ │ │ ├── AlertEvent.java │ │ ├── RescueTask.java │ │ └── TaskStatusLog.java │ └── config/ │ ├── SchedulerConfig.java │ └── WebSocketConfig.java ├── src/main/resources/ │ ├── application.yml │ └── db/ │ └── schema.sql └── frontend/ └── index.htmlcontroller 只做参数校验和接口暴露业务逻辑放在 service 层。状态机单独拆一个类方便做单元测试和后续扩展。2.3 数据表设计核心表包括预警事件表、救援任务表、状态日志表和位置上报表。先看 DDL。CREATE TABLE alert_event ( id BIGSERIAL PRIMARY KEY, alert_code VARCHAR(64) UNIQUE NOT NULL, title VARCHAR(255) NOT NULL, level VARCHAR(16) NOT NULL, area_code VARCHAR(16), longitude NUMERIC(10, 6), latitude NUMERIC(10, 6), event_time TIMESTAMP NOT NULL, raw_payload JSONB, status VARCHAR(16) NOT NULL DEFAULT NEW, create_time TIMESTAMP NOT NULL DEFAULT now() ); CREATE TABLE rescue_task ( id BIGSERIAL PRIMARY KEY, event_id BIGINT NOT NULL, team_id BIGINT, task_no VARCHAR(32) UNIQUE NOT NULL, task_type VARCHAR(32) NOT NULL, description TEXT, address VARCHAR(255), longitude NUMERIC(10, 6), latitude NUMERIC(10, 6), status VARCHAR(16) NOT NULL DEFAULT PENDING, assignee_phone VARCHAR(32), version INT NOT NULL DEFAULT 0, accept_time TIMESTAMP, finish_time TIMESTAMP, create_time TIMESTAMP NOT NULL DEFAULT now() ); CREATE TABLE task_status_log ( id BIGSERIAL PRIMARY KEY, task_id BIGINT NOT NULL, from_status VARCHAR(16), to_status VARCHAR(16) NOT NULL, operator_id VARCHAR(64), remark TEXT, create_time TIMESTAMP NOT NULL DEFAULT now() ); CREATE TABLE location_report ( id BIGSERIAL PRIMARY KEY, task_id BIGINT NOT NULL, longitude NUMERIC(10, 6) NOT NULL, latitude NUMERIC(10, 6) NOT NULL, report_time TIMESTAMP NOT NULL, create_time TIMESTAMP NOT NULL DEFAULT now() );关键设计点有三个。alert_code设置唯一约束防止同一条预警重复入库。rescue_task增加version字段更新状态时用乐观锁避免两个操作同时修改任务状态。task_status_log单独建表保存每次状态变化后续复盘和响应时长统计都依赖这张日志。任务状态的枚举值如下。任务状态含义可操作角色PENDING待派发调度员ASSIGNED已派发调度员、救援队EXECUTING执行中救援队PAUSED暂停调度员COMPLETED已完成救援队、调度员CANCELLED已取消调度员状态枚举要维护在代码里不要散落在 SQL 文件中。数据库可以建约束兜底但应用层状态机才是真正的规则入口。3. 实现核心功能3.1 预警数据接入预警接入最常用的方式是定时轮询。系统按固定周期请求第三方接口解析返回 JSON再落库。Service public class AlertIngestService { private final String alertApi https://example-weather.example/v1/alerts; Scheduled(fixedDelay 30000) public void pullLatest() { String body restTemplate.getForObject(alertApi, String.class); ListAlertDTO alerts parse(body); for (AlertDTO dto : alerts) { ingest(dto); } } public void ingest(AlertDTO dto) { String lockKey alert:dup: dto.getAlertCode(); Boolean first redisTemplate.opsForValue() .setIfAbsent(lockKey, 1, Duration.ofHours(24)); if (!Boolean.TRUE.equals(first)) { return; } alertEventRepository.save(convert(dto)); } }这里的轮询间隔fixedDelay 30000表示上一次执行完成后等待 30 秒。实时性要求较高的场景应该优先使用第三方提供的 Webhook 推送而不是轮询。去重键放在 Redis 而不是只依赖数据库唯一约束是因为 Redis 查询成本更低而且键可以设置过期时间。但 Redis 和数据库不是原子操作极限情况下仍可能重复所以alert_code的唯一约束必须保留作为兜底。外部接口返回结构可能差异很大下面是一个简化示例。{ alertCode: ST-20260823-001, title: 暴雨红色预警, level: RED, areaCode: 320100, longitude: 118.796, latitude: 32.060, eventTime: 2026-08-23T02:30:00Z }示例坐标使用 WGS84实际接入时一定要先确认上游用的什么坐标系。如果上游是 GCJ-02入库前必须转换否则地图展示会偏移几百米。3.2 灾情上报与任务创建预警入库后需要生成灾情事件再由人工或规则转换为救援任务。先看任务创建接口。PostMapping(/api/v1/events/{eventId}/tasks) public Result createTask( PathVariable Long eventId, RequestBody CreateTaskRequest request) { AlertEvent event alertEventRepository.findById(eventId) .orElseThrow(() - new BizException(event not found)); RescueTask task new RescueTask(); task.setEventId(eventId); task.setTaskNo(generateTaskNo()); task.setTaskType(request.getTaskType()); task.setDescription(request.getDescription()); task.setLongitude(request.getLongitude()); task.setLatitude(request.getLatitude()); task.setStatus(TaskStatus.PENDING); rescueTaskRepository.save(task); taskStatusLogRepository.save( TaskStatusLog.from(null, TaskStatus.PENDING, currentOperator())); return Result.ok(task.getId()); }任务编号要用系统生成的task_no不要把外部系统编号直接当任务号。后续和第三方系统对接、线下沟通时报编号比报数据库自增 id 更安全。任务创建后必须立即写一条状态日志。这样从任务出生到关闭每个状态都能追溯。如果只在状态变化时才补日志创建操作本身会缺失记录。3.3 救援任务状态机状态机是这个系统里最需要设计严谨的部分。先定义枚举和允许流转的路径。public enum TaskStatus { PENDING, ASSIGNED, EXECUTING, PAUSED, COMPLETED, CANCELLED }允许的流转路径如下表。从状态允许到达状态PENDINGASSIGNEDCANCELLEDASSIGNEDEXECUTINGPAUSEDCANCELLEDEXECUTINGPAUSEDCOMPLETEDPAUSEDASSIGNEDEXECUTINGCANCELLED状态机核心逻辑如下。public void changeStatus(Long taskId, TaskStatus target, String operatorId) { RescueTask task rescueTaskRepository.findById(taskId) .orElseThrow(() - new BizException(task not found)); TaskStatus current task.getStatus(); if (!allowedTransitions.get(current).contains(target)) { throw new BizException( invalid transition: current - target); } int updated rescueTaskRepository.updateStatus( taskId, target, current, task.getVersion()); if (updated 0) { throw new BizException(task status changed by others); } taskStatusLogRepository.save( TaskStatusLog.from(current, target, operatorId)); }关键在于更新状态时把当前状态和版本号都放进 where 条件。如果另一个操作已经改过状态update 返回 0当前操作直接失败。不要用“先 select 再 update”的方式否则并发下状态会被覆盖。为什么不能直接执行 UPDATE 改状态因为任务状态不只是数据库里的一个值它代表了业务规则。从 PENDING 直接改成 COMPLETED 在数据库层面完全合法但等于跳过了派发和救援过程复盘时无法解释发生了什么。状态机就是把业务规则集中起来不让它散落在各个业务代码里。注意不要让前端直接修改任务状态字段。前端只能调用状态机接口后端负责校验流转路径和并发控制。3.4 地图可视化与 GeoJSON地图模块通常由前端展示服务端只需要把任务和预警区域输出成 GeoJSON。{ type: FeatureCollection, features: [ { type: Feature, properties: { id: 1001, taskNo: T20260823001, status: EXECUTING, address: 某街道 }, geometry: { type: Point, coordinates: [118.796, 32.060] } } ] }前端用 Leaflet 加载的简化示例。fetch(/api/v1/map/tasks) .then(res res.json()) .then(data { L.geoJSON(data, { pointToLayer: (feature, latlng) { const color feature.properties.status EXECUTING ? #f00 : #00f; return L.circleMarker(latlng, { radius: 8, color });
返回列表