news 2026/9/23 22:44:32

无人机巡维保障系统后端:状态机与轨迹数据处理实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
无人机巡维保障系统后端:状态机与轨迹数据处理实践

简介:基于Java语言的uav-patrol-backend巡维保障系统后端源码,面向无人机巡维业务的后端开发人员,提供了一套模块化、可扩展的服务端实现,可支撑无人机巡维任务管理、设备状态监控与数据上报等核心服务。项目包含51个文件、共93KB,以44个Java源文件为主体,涵盖核心业务逻辑、数据处理与接口交互;另含3个XML及2个YAML配置文件,用于数据库连接、环境参数与日志级别等灵活配置,并附带版本忽略规则文件与说明文档,便于版本管理与快速上手。已有332人学习浏览,适合正在搭建无人机巡维管理系统或希望参考Java后端工程结构的中高级开发者。整套代码按模块划分,清晰展示了Maven项目构建、多环境配置与源码组织方式;通过阅读源码可掌握Spring Boot工程配置、REST接口设计等典型后端实践,可直接用于功能扩展、二次开发或学习参考。

1. 巡维保障系统后端:一套 Java 工程到底在解决什么

先还原一个场景:一架无人机按计划从机巢起飞,沿输电线路巡检,拍到疑似隐患点,后台要实时看到飞行轨迹、接收照片、判断是否需要派人去现场复核。这个过程里,飞手只是“按了启动”,真正干活的是一套后端服务——它负责下发任务、接收设备上报、处理海量轨迹点、管理隐患工单、对接前端页面。uav-patrol-backend 这个命名,直译就是“无人机巡检后端”,所谓巡维保障,指的是围绕巡检、维护、保障这三个动作做闭环管理。它是典型的 Java 后端工程,承载的是设备接入层、任务调度层、业务处理层和对外 API 层。

这类系统最容易让新手误判的地方在于:以为核心难点是“调用无人机厂商的 SDK”。真实情况是,厂商 SDK 大多只负责最底层的飞行控制,业务后端真正要解决的是状态机设计、并发上报处理、轨迹数据存储、任务调度和权限管控。换句话说,哪怕你完全不用真实无人机,用一个模拟器上报坐标和图片,这套后端的前 80% 工作量照样存在。因此,这套代码适合两类人:一类是在做毕业设计或课程设计、需要一套完整可演示的 Java Web 工程作为基线;另一类是已经在做物联网或巡检类项目、想看看别人怎么组织任务状态和设备接入的工程师。下文按我实际搭这类系统的习惯,把设计、实现、踩坑一次讲清。

2. 选型与工程结构:为什么是 Spring Boot 3 + MyBatis-Plus,而不是别的组合

2.1 技术选型的三个约束条件

巡维保障后端不是高并发互联网应用,它的流量特征非常明确:终端设备数量有限(几十到几百台无人机和摄像头),但单台设备上报频率高、数据格式杂、偶尔有突发批量补传;业务操作集中在任务流转和工单处理上,事务边界清晰。这个特征决定了选型方向:不需要 Spring Cloud 全家桶,不需要分布式事务中间件,但要求开发效率高、容易扩展设备协议。

我一般会选 Spring Boot 3 + MyBatis-Plus + MySQL + Redis,JDK 用 17。原因有三条:第一,Spring Boot 3 已经全面拥抱 Jakarta EE,和 JDK 17 的长期支持周期匹配,没必要再开 JDK 8 的新项目;第二,MyBatis-Plus 的代码生成器和 LambdaQueryWrapper 能省掉大量单表 CRUD 的样板代码,这类系统的数据表动辄二三十张,手写 Mapper 会拖慢进度;第三,Redis 在这个项目里不是缓存装饰品,而是承担设备会话状态、分布式锁、轨迹热数据三个核心职责,后面会展开。前端部分不用管,你只需要知道接口按前后端分离设计,返回统一 JSON 结构即可。

2.2 包结构与模块边界:按业务域切,不按技术层切

很多 Java 新手拿到这类项目会问:是不是要建 controller / service / mapper / entity 四个包就完事了?我的做法是,顶层按业务域切分包,每个域内部再分 controller / service / mapper。下面是这套工程最常用的包结构:

com.uav.patrol ├── common # 统一返回体、异常处理、常量、工具类 ├── config # 配置类:Redis、MyBatis-Plus、拦截器、CORS ├── module │ ├── device # 设备管理:注册、在线状态、协议解析 │ ├── task # 巡检任务:创建、调度、执行、状态流转 │ ├── track # 轨迹数据:上报接收、存储、回放查询 │ ├── alarm # 隐患告警:识别结果接收、工单生成、闭环处理 │ ├── file # 文件服务:图片/视频上传、访问鉴权 │ └── system # 用户、角色、菜单、日志 └── UavPatrolApplication.java

按业务域切包的好处是:一个人开发时,心智负担小,改任务调度不会误触设备协议代码;团队协作时,每个人负责一个域,合并冲突概率低。体系结构上,这套代码里最常见的错误设计是把所有实体类扔进一个 entity 包,然后 service 层互相调用一团乱麻,最后改一个需求动半套代码。按域隔离后,跨域调用统一走对方的 Service 接口,不允许直接操作对方的 Mapper,这个纪律能保证后期维护不失控。

2.3 统一返回体与全局异常的必要性

前后端分离项目里,前端最怕的不是接口报错,而是每个接口返回结构都不一样。这套系统的 common 包里必须定义统一的返回包装类,这是第一个要写的类。它的核心设计如下:

@Data public class R<T> { private int code; // 业务状态码:200 成功,非 200 失败 private String message; // 提示信息 private T data; // 业务数据 private long timestamp; // 服务端时间戳,用于排查时对齐日志 public static <T> R<T> ok(T data) { R<T> r = new R<>(); r.code = 200; r.message = "success"; r.data = data; r.timestamp = System.currentTimeMillis(); return r; } public static <T> R<T> fail(int code, String message) { R<T> r = new R<>(); r.code = code; r.message = message; r.timestamp = System.currentTimeMillis(); return r; } }

这段代码的逻辑说明:所有接口的返回值统一走 R 包装,code 只表示业务成功与否,HTTP 状态码仍然按 REST 规范使用(404 表示资源不存在、401 表示未认证)。前端只需要判断 code 是否为 200,不需要针对每个接口单独处理 error 结构。timestamp 字段容易被忽略,但它对排查问题非常重要——当设备上报时间和服务器处理时间不一致时,对齐时间戳才能定位是网络延迟还是处理堆积。

参数说明:code 的设计要预留业务码段,比如 10001 表示设备离线、10002 表示任务状态冲突,不要直接用 HTTP 状态码当业务码。message 要面向使用者可读,不能直接抛 SQLException 的原始信息给前端。把全局异常处理器(@RestControllerAdvice)配好后,Controller 里的 try-catch 就能彻底清掉,业务代码只关注正常流程。

2.4 核心数据表:任务、轨迹、告警三条主线的 ER 关系

先把数据模型立住,后面写业务才有据可依。巡维保障系统的表设计有一条隐含的主线:设备是资源,任务驱动设备工作,工作过程中产生轨迹和媒体数据,媒体数据经过分析变成告警,告警再生成工单。这条链路上,最重要的五张表要单独列出来讲。

表名核心字段说明
patrol_taskid, task_no, device_id, route_id, plan_start_time, plan_end_time, status, created_by巡检任务主表,status 是状态机核心
task_executionid, task_id, device_id, real_start_time, real_end_time, flight_count, result一次任务的实际执行记录,一个任务可能多次执行
device_infoid, device_code, device_type, protocol_type, online_status, last_online_time, lat, lng设备台账,online_status 用 Redis 缓存辅助判断
track_pointid, task_id, device_id, lat, lng, altitude, speed, direction, gps_time, create_time轨迹点表,按天分表或冷热分离
alarm_infoid, task_id, device_id, alarm_type, lat, lng, media_url, status, handler_id告警信息,status 决定是否已生成工单

这五张表的关联关系,一句话说清:patrol_task 关联 device_info 决定谁去执行,关联 task_execution 记录真实飞行的开始结束时间,飞行过程中设备上报的每个点落到 track_point,识别算法产出的异常结果落到 alarm_info。注意 track_point 表不要加唯一索引在 task_id 上,因为一次任务会落成千上万个点。

3. 任务调度与状态机:让巡检任务按计划跑起来还不重不漏

3.1 为什么状态字段不能只靠 if-else 硬编码

任务状态是整个系统的骨架。一个巡检任务从创建到归档,至少要经历:待执行 -> 已下发 -> 执行中 -> 已完成 -> 已归档,中间还可能插入已取消、执行异常两个旁路状态。新手最容易犯的错是在 Service 里写 if (status == 1) { doSomething(); } else if (status == 2) { doAnother(); }——前两次改动还很爽,到第三次加需求时,你会发现自己根本记不清哪个数字代表哪个状态,而且可能出现“从待执行直接跳到已完成”这种非法流转。

正确做法是引入状态机枚举,把每个状态允许的流转动作显式建模。这套系统里我会直接定义一个 TaskStatusEnum,并且用 Map 预先定义好合法流转路径,非法流转直接抛业务异常。代码如下:

public enum TaskStatusEnum { PENDING(0, "待执行"), DISPATCHED(1, "已下发"), EXECUTING(2, "执行中"), FINISHED(3, "已完成"), CANCELED(4, "已取消"), EXCEPTION(5, "执行异常"), ARCHIVED(6, "已归档"); private final int code; private final String desc; private static final Map<Integer, Set<Integer>> TRANSITIONS = new HashMap<>(); static { TRANSITIONS.put(PENDING.code, Set.of(DISPATCHED.code, CANCELED.code)); TRANSITIONS.put(DISPATCHED.code, Set.of(EXECUTING.code, CANCELED.code)); TRANSITIONS.put(EXECUTING.code, Set.of(FINISHED.code, EXCEPTION.code)); TRANSITIONS.put(EXCEPTION.code, Set.of(DISPATCHED.code, ARCHIVED.code)); TRANSITIONS.put(FINISHED.code, Set.of(ARCHIVED.code)); } public static void validateTransition(int from, int to) { Set<Integer> allowed = TRANSITIONS.get(from); if (allowed == null || !allowed.contains(to)) { throw new BizException("非法状态流转: " + from + " -> " + to); } } }

逻辑说明:这个枚举把状态码与状态描述绑定,同时用 Map 定义了每个状态的合法去向。比如执行中只能流转到已完成或执行异常,不能在任务还在天上飞的时候直接把它取消。Service 层每次更新状态前必须先调用 validateTransition 做校验,校验通过才执行 UPDATE。

参数说明:状态码不要用 1、2、3 这种裸数字,必须用枚举常量引用。desc 字段要给中文描述,因为前端下拉框、日志打印、告警消息都要用。实际项目里状态可能还要细分,比如任务下发后要区分“等待设备确认”和“设备已确认”,这时新增枚举值即可,不需要改已有流转逻辑。这套设计还有一个附加价值:控制台日志里搜索状态流转时,能看到”from=0 to=1“这类信息,配合 traceId 就能还原整个任务的生命周期。

3.2 定时巡检任务的两种触发机制:调度框架还是延时队列

巡检任务有两种触发场景:一种是用户手动创建、立即执行;另一种是按计划定时执行,比如每天上午 9 点飞一遍指定线路。实现定时任务,业界最常见两类方案:一是用 XXL-Job 这类分布式调度平台,把任务模型建在调度中心,由调度中心回调业务接口;二是用 Spring 自带的 @Scheduled 注解配合 Redis 分布式锁。对于这套系统的体量,我建议直接上 XXL-Job 或同类框架,理由有两条:第一,调度平台自带失败重试、任务依赖、调度日志,这些功能自己写要花不少时间;第二,任务执行日志是巡维系统的硬需求,出了问题要能回看”几点几分触发、执行结果是什么“,调度平台天然满足这一点。

在业务代码里,你的职责是提供一个“接收调度回调并创建巡检任务”的入口。简单示意如下:

@Component public class PatrolTaskTrigger { @XxlJob("patrolTaskDispatchJob") public ReturnT<String> dispatch(String param) { // param 格式约定: taskId,调度平台在创建任务时传入 Long taskId = Long.parseLong(param); TaskService taskService = SpringContextHolder.getBean(TaskService.class); boolean success = taskService.dispatchTask(taskId); return success ? ReturnT.SUCCESS : ReturnT.FAIL; } }

逻辑说明:调度平台负责在指定时间点调用这个入口,业务侧只做一件事——根据 taskId 把任务从待执行变成已下发,并真正向设备下发命令。这样调度逻辑和业务逻辑彻底解耦:调度平台挂了,任务只是不触发,不会破坏业务数据;业务服务升级重启,调度平台到点照样回调,重启完成自然恢复。

参数说明:param 的格式必须和调度平台的任务配置约定好。常见做法是传任务 ID,但更稳妥的是传任务编号(task_no),因为任务 ID 在分库分表场景下可能冲突。回调接口要做好幂等,同一个回调如果因为网络重试到达两次,dispatchTask 里要判断当前状态是否已经是已下发,是则直接返回成功,避免重复向设备下发指令。

3.3 任务下发与设备指令:异步还是同步

把任务下发给无人机,逻辑上就一步——调用设备服务层的接口,往 MQTT 或 TCP 通道发一条指令。但这里有个极易踩坑的决策点:要不要等待设备回复“收到”?如果同步等待,一个设备响应慢 5 秒,整个任务创建接口就卡 5 秒,前端转圈,用户烦躁;如果完全异步,设备没收到指令,任务状态已经变成已下发,后面就悬空了。

常用做法是:任务状态先置为“已下发”,但下发动作走异步通道,同时启动一个延时检查任务。具体实现上,用 Redis 存下”待确认指令“的 key,15 秒后检查设备是否上报告了“指令已确认”事件。代码如下:

public void dispatchTask(Long taskId) { PatrolTask task = getById(taskId); TaskStatusEnum.validateTransition(task.getStatus(), TaskStatusEnum.DISPATCHED.getCode()); // 1. 状态先落库 updateStatus(taskId, TaskStatusEnum.DISPATCHED); // 2. 发送指令到设备通道,不等待结果 DeviceCommand command = new DeviceCommand(); command.setDeviceId(task.getDeviceId()); command.setTaskId(taskId); command.setType("START_PATROL"); deviceGateway.send(command); // 3. 记录待确认 key,15 秒后检查 String confirmKey = "patrol:task:confirm:" + taskId; redisTemplate.opsForValue().set(confirmKey, "0", Duration.ofSeconds(15)); }

逻辑说明:三步走的核心思想是“快速响应前端 + 最终一致”。用户点创建任务后,接口立刻返回成功;真正和设备通信的结果由后续确认机制兜底。如果 15 秒内设备上报了确认事件,就把 confirmKey 删掉;如果没删,延时任务发现 key 还在且值还是 0,就标记任务为执行异常,并在告警表里写一条“任务下发超时”。

参数说明:15 秒这个超时时间不是拍脑袋定的。无人机从收到指令到完成自检并回复确认,通常需要 8 到 12 秒,设成 15 秒留了余量,又不至于让用户等太久才看到异常。如果是大型固定翼或者复杂航线加载场景,要按设备型号调整,建议把超时时间做成设备类型的配置项,不要硬编码。

4. 设备接入与轨迹数据处理:高并发上报怎么接得住、存得下

4.1 设备协议层:统一网关设计,别让业务代码依赖厂商 SDK

市面上的无人机/摄像头厂家都有自己的 SDK 或协议,但是巡维系统的业务逻辑(任务、轨迹、告警)不关心你的设备是哪个牌子——它只关心三个动作:设备上线、上报轨迹点、上报媒体文件。因此设备接入层必须抽象出一个统一网关接口,厂商差异全部封在适配器里。这套系统里最常见的抽象如下:

public interface DeviceGateway { /** * 设备上线 * @param deviceCode 设备唯一编码 * @param protocolType 协议类型,如 mqtt / tcp / http */ void online(String deviceCode, String protocolType); /** * 下发指令 */ void sendCommand(DeviceCommand command); /** * 设备主动上报数据(轨迹、状态等) */ void onDataReport(DeviceReport report); /** * 设备离线 */ void offline(String deviceCode); }

逻辑说明:这个接口放在 module/device 包下,所有厂商适配器都实现它。比如大疆的适配器内部把轨迹点转成统一的坐标模型,另一个厂商的适配器做同样的转换,转换完的数据结构是一致的,上层业务根本感知不到厂商差异。这样一个新设备接入时,工作量主要集中在适配器里,业务层零改动。

参数说明:DeviceReport 的字段要覆盖设备的通用能力描述,至少要包含 deviceCode、reportType(TRACK / STATUS / MEDIA)、latitude、longitude、altitude、speed、direction、timestamp、ext(扩展字段)。注意 ext 是 JSON 字符串,用来兜底厂商特殊字段,防止每接入一家新设备就改一次核心表结构。

4.2 轨迹上报的接收链路:Netty 还是 MQTT

轨迹上报是这套系统流量最大的接口。一架飞机每秒上报 1 个点,10 架飞机同时飞就是每秒 10 个请求,看起来不大,问题是上报是脉冲式的——起飞和降落阶段集中上报,而且网络抖动时积压的数据会瞬间补传。处理链路我最常用的有两套:如果项目要求完全自主可控,用 Netty 直接走 TCP 长连接,自己解析协议;如果允许引入中间件,用 EMQX 这类 MQTT Broker,Java 服务通过 MQTT 客户端订阅主题接收数据。

这里我推荐 MQTT 方案,原因很实际:无人机在野外飞行,网络质量不稳定,MQTT 的 QoS 机制天然处理了消息重传和离线消息保留;自研 TCP 协议看起来简单,但断线重连、心跳保活、粘包拆包,每个细节都要自己填坑,周期至少多两周。订阅主题的命名规则建议按设备维度切分:patrol/{deviceCode}/track,这样业务端订阅 patrol/+/track 就能收到所有轨迹数据,按设备维度隔离也方便排查单台设备的问题。

消息进来后,第一件事不是落库,而是做轻量过滤和转换:

@Component public class TrackReportConsumer { @KafkaListener(topics = "patrol-track-raw", groupId = "patrol-track-group") public void onTrackReport(ConsumerRecord<String, String> record) { DeviceReport report = JSON.parseObject(record.value(), DeviceReport.class); // 1. 基础校验:坐标范围、时间戳合法性 if (!isValidCoordinate(report.getLatitude(), report.getLongitude())) { log.warn("invalid coordinate: {}", record.value()); return; } // 2. 过滤漂移点:速度突变、距离突变 String deviceCode = report.getDeviceCode(); TrackPoint lastPoint = trackCache.getLastPoint(deviceCode); if (lastPoint != null && isDriftPoint(lastPoint, report)) { log.warn("drift point ignored, device={}, distance={}m", deviceCode, calculateDistance(lastPoint, report)); return; } // 3. 落库 + 更新设备最新位置缓存 trackService.saveTrackPoint(convertToEntity(report)); trackCache.updateLastPoint(deviceCode, report); } }

逻辑说明:这段代码展示的是消费端的处理策略。先做合法性校验,坐标范围不对直接丢弃;再做漂移点过滤,比如两个相邻轨迹点距离超过了飞机在该时间间隔内的物理可达范围,判定为 GPS 漂移,丢弃但写入日志。这两步能拦掉大量脏数据,降低存储压力和分析误差。最终有效数据才真正落库,同时更新 Redis 缓存里的设备最新位置,供 Web 端地图实时展示。

参数说明:isDriftPoint 的判断逻辑是速度阈值和距离阈值的组合,我常用单点位移不超过 500 米且速度不超过 150km/h 作为默认值,具体要到现场根据飞机型号和采样频率调。Kafka 的 topic 分区数建议按设备量级设置,设备少于 50 台时 3 个分区足够,这里的关键不是吞吐而是消费顺序——同一台设备的轨迹点如果被分到不同分区,会出现乱序,所以分区键必须用 deviceCode。

4.3 轨迹表的分表策略与冷热数据分离

轨迹点表是这套系统里体积膨胀最快的表。假设每架飞机每秒上报 1 个点,1 天飞行 2 小时,1 台设备一天就是 7200 条,50 台设备一个月下来超过 1000 万条。如果全堆在一张表里,三个月后查询“某次任务的轨迹回放”会明显变慢,因为 MySQL 的普通 B+ 树索引扛不住千万级数据的范围查询。

常见的解法按数据量分三档:千万级以内,给 track_point 表加联合索引 (task_id, gps_time),单表能撑;亿级以内,按天分表,表名形如 track_point_20240611,查询时根据时间范围路由到对应表;亿级以上,上时序数据库,比如 TDengine 或 IoTDB,它们对时序数据做了存储和查询层面的专门优化。对于 uav-patrol-backend 这类项目,我建议做到第二档就够。分表路由的逻辑写在 MyBatis-Plus 的 DynamicTableNameInnerInterceptor 里,代码示意如下:

@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); DynamicTableNameInnerInterceptor dynamicTableName = new DynamicTableNameInnerInterceptor(); dynamicTableName.setTableNameHandler((sql, tableName) -> { if ("track_point".equals(tableName)) { // 从上下文中取出当前要写入的时间,决定路由到哪张物理表 LocalDate targetDate = TrackContextHolder.getCurrentDate(); return "track_point_" + targetDate.format(DateTimeFormatter.BASIC_ISO_DATE); } return tableName; }); interceptor.addInnerInterceptor(dynamicTableName); return interceptor; } }

逻辑说明:MyBatis-Plus 的 DynamicTableNameInnerInterceptor 会在 SQL 执行前改写表名。业务代码里写的还是 track_point,实际执行时被替换成 track_point_20240611 这类物理表。关键在于,路由条件(当前日期)必须通过 ThreadLocal 或请求参数传入,否则你无法确定这条数据该落到哪天。这里我用了 TrackContextHolder,它是一个 ThreadLocal 包装类,在 MQ 消费入口处设置当前时间即可。

参数说明:分表不要做得太激进,建议按月分而不是按天分。按天分表会导致建表脚本太多,管理麻烦;按月分表,一张表 30 天的数据量通常在百万到千万之间,查询性能可控,建表 12 张一年也容易维护。历史轨迹的 TTL 一般是 90 天,超过这个时间的冷数据要么归档到 OSS,要么直接删,不需要无限保留。

5. 避坑与排查手册:这套系统最容易翻车的 5 个环节

5.1 时区问题:GPS 时间是 UTC,业务时间是东八区

现象:前端地图上显示的轨迹点和实际航线偏差 8 个小时,或者任务计划时间明明设置的是 9 点,任务却在 1 点被触发了。原因:设备上报的 GPS 时间和服务器系统时区不一致。绝大多数 GPS 模块输出的时间是 UTC 标准时间,不随设备所在地变化;而 MySQL 连接串里如果配置了 serverTimezone=Asia/Shanghai,JDBC 会自动做时区转换。一旦两边的时区信息错位,时间就乱了。解决:规范三处配置——数据库连接串统一用 serverTimezone=Asia/Shanghai;Jackson 序列化 LocalDateTime 时统一格式化并指定时区;设备上报的时间戳在协议解析层就明确语义。协议解析处必须加上 UTC 转东八区的逻辑,这句话写在代码注释里,防止后人“优化”掉。

5.2 数据库连接池被打满:轨迹批量补传时接口集体超时

现象:设备断网半小时后恢复,积压的轨迹点瞬间补传,此时 Web 端用户点任何页面都转圈,后台日志报连接获取超时。原因:轨迹上报接口是 IO 密集型操作,每个请求占用一个数据库连接做 INSERT。积压数据把 HikariCP 默认的 10 个连接全部占满后,其他业务请求排队等连接,整站瘫痪。解决:把轨迹落库改为批量写入——在消费端攒 500 条或 1 秒滑动窗口内攒到的所有数据,一次性 batch insert。这个方案能把连接占用降低一到两个数量级,同时改造成本极低。另外把 HikariCP 的最大连接数从默认值调到 50,并设置 connection-timeout 为 3000ms,宁可快速失败也不要无限排队。

5.3 设备上下线状态在 Redis 里失效:明明在线却下发失败

现象:设备列表显示某台无人机在线,但下发指令时设备没有任何响应,后台也没有报错。原因:设备在线状态是通过心跳机制维护的,设备每 30 秒上报一次心跳,服务端收到后刷新 Redis 里的 last_heartbeat_time。但如果 Redis key 的过期时间设置得过短,比如设成 30 秒,而设备心跳恰好因网络抖动晚到几秒,key 就过期了,服务端判断设备离线,消息队列里的指令被丢弃。解决:key 过期时间设置成心跳间隔的 3 倍,即 90 秒,同时下发指令前不要只查 Redis,要主动给设备发一条 ping 探测指令,等待 3 秒内是否有 pong 响应。这样能同时处理“状态缓存过期”和“设备假在线”两个问题。

5.4 前端跨域问题:Vue3 联调时接口能通但带不了 Cookie

现象:前端用 Vite 开发服务器启动在 5173 端口,后端跑在 8080,请求发过去总是报跨域错误。解决过程:后端加了 CORS 过滤器,允许来源 http://localhost:5173,结果发现非登录接口正常了,但登录接口始终带不上 Session。原因:fetch 默认不带 Cookie,需要在请求里设置 credentials: include,同时后端的 CORS 配置里 allowCredentials 必须为 true,且 allowedOrigin 不能是 *。这三者必须同时满足,缺一个都不行。另外如果你用了 Spring Security 或 Sa-Token,还要放行 OPTIONS 预检请求,否则前端每次复杂请求都会先被拦一道。

5.5 任务取消后轨迹还在写:状态机约束漏掉了数据写入链路

现象:操作员取消了任务,但任务关联的轨迹点数量还在涨,地图上还有飞机在飞。原因:任务取消只是改了 patrol_task 表的 status,但轨迹上报的消费链路并没有校验任务状态。设备不知道任务被取消了,还在按原计划上报数据,消费端照单全收。解决:在 TrackReportConsumer 落库前加一道校验,查询任务当前状态,只有 EXECUTING 状态才允许写入轨迹;如果任务已经被取消或完成,将设备上报的数据写入一个单独的 abandoned_track 表,同时触发设备停机指令。这个校验不能用查库的方式,否则每次上报都多一次数据库查询,应该把任务状态缓存在 Redis 里,消费时只查缓存。

6. 进阶:从能跑到能上线,Redis 分布式锁与告警闭环的细节打磨

巡维保障系统从“功能实现”到“稳定运行”,还有两个坎必须过:一是定时任务在集群部署时不重复执行,二是告警产生后能真正闭环,而不是变成一个只读列表。第一个坎靠 Redis 分布式锁解决。假设你有两个后端实例同时运行,XXL-Job 的调度是打给某个实例的,但如果你自己写了 @Scheduled 定时扫描待执行任务,就要防止两个实例同时扫到同一条任务。代码套路如下:

public boolean tryLock(String lockKey, long expireSeconds) { Boolean success = redisTemplate.opsForValue() .setIfAbsent(lockKey, "1", Duration.ofSeconds(expireSeconds)); return Boolean.TRUE.equals(success); } public void clearLock(String lockKey) { redisTemplate.delete(lockKey); }

逻辑说明:setIfAbsent 就是 Redis 的 SETNX 命令,只有 key 不存在时才能设置成功。多个实例同时抢同一把锁,只有一个能成功,拿到锁的实例执行任务,其他实例直接跳过。锁的过期时间要设长一点,比如 30 秒,保证任务能正常执行完;任务执行完后主动删锁,不要让锁活到过期。这里有一个细节:删除锁之前要校验 value 是不是自己的,防止 A 实例的锁过期后被 B 实例拿到,A 执行完把 B 的锁删了,导致 B 和 C 同时执行。加一个 value 比对再删除即可。

第二个坎是告警闭环。巡维系统如果只做到“识别出隐患并记录”,运营价值会大打折扣——隐患必须流转到工单,工单必须指派给人,人处理完必须回填结果。闭环链路在代码上要定义清楚:alarm_info 表新增 status 字段,0 表示待确认、1 表示已派单、2 表示已处理、3 表示已关闭。PUSH 到前端页面的事件流用 WebSocket 推送,实现在线提醒,避免前端轮询浪费请求。我的习惯是每周检查一次告警表的历史数据,找出超过 7 天没流转的记录,沿着时间线重放告警生成时的日志和轨迹,定位是算法误报还是流程卡住——这种“复盘日志”的习惯,比写再多代码都能更快提升系统质量,希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/23 22:44:19

曼彻斯特与差分曼彻斯特编码:从原理推导到STM32实现

数字信号编码这块内容&#xff0c;我在做嵌入式通信和工业总线的时候踩过不少坑。最早接触曼彻斯特编码&#xff0c;是因为一个STM32项目里需要把传感器数据通过单根信号线传出去&#xff0c;同时还要让接收端能自己恢复时钟——当时试过NRZ编码&#xff0c;结果接收端时钟漂移…

作者头像 李华
网站建设 2026/9/23 22:42:18

多制式车牌识别系统:支持新能源绿牌、港澳粤Z等10+类型开箱即用

简介&#xff1a;这是一套面向计算机视觉初学者与进阶开发者的中文车牌检测与识别实战源码&#xff0c;聚焦蓝牌、黄牌、双层黄牌、农用车、警车、校车、教练车、港澳车牌、使领馆车牌及新能源车牌等10余类复杂场景&#xff0c;解决真实交通图像中多制式车牌的准确定位与字符识…

作者头像 李华
网站建设 2026/9/23 22:41:38

同比环比怎么用?区别、应用场景与实操技巧一次讲透

做数据分析这些年&#xff0c;被问得最多的问题之一就是“同比和环比到底啥区别&#xff0c;我该看哪个”。每次月度经营会、周报复盘&#xff0c;总有人把这两个口径混着用&#xff0c;要么拿环比涨跌说趋势&#xff0c;要么拿同比波动说短期变化&#xff0c;结论自然跑偏。这…

作者头像 李华
网站建设 2026/9/23 22:40:42

汽车缺陷检测为何坚持用VOC格式?工业级数据建模指南

简介&#xff1a;本资源是面向计算机视觉初学者与目标检测实践者的专业级汽车缺陷检测图像数据集&#xff0c;采用标准VOC格式标注&#xff0c;可直接用于YOLOv5等主流框架的训练与验证&#xff0c;解决工业质检中细粒度缺陷识别的数据匮乏问题。数据包共2001个文件&#xff0c…

作者头像 李华