资讯动态

无人机巡逻后端:任务状态闭环与Spring Boot实现

发布时间:2026/10/9 6:44:03 来源:尧图企业网站定制
简介uav-patrol-backend 巡维保障系统后端代码设计源码是一份基于 Java 语言构建的无人机巡维智能化管理后端项目适合有一定 Java 基础的后端开发者、无人机运维系统设计人员及毕业设计开发者参考。资源共 51 个文件压缩包约 93KB以 44 个 Java 源文件为主体覆盖核心业务逻辑、数据处理与接口交互另含 3 个 XML、2 个 YAML 配置文件用于环境参数、数据库连接及日志等级管理同时提供 .gitignore 规则和 readme 说明文档便于项目理解与版本管理。通过阅读源码可掌握模块化分层设计思路、Spring 常见配置方式以及巡维场景下后端服务的组织方法对快速搭建同类管理系统具有直接借鉴价值。目前已有 332 人学习下载。1. uav-patrol-backend 巡维保障系统难点不在飞控而在任务状态闭环uav-patrol-backend 这个名字乍看只是「无人机巡逻后端」但真正把巡维保障系统跑起来的人会告诉你飞控指令下发反而简单最难的是巡逻任务从派发、起飞、巡线到生成缺陷工单的整个状态闭环。状态一多业务代码就退化成一堆 if-else换个人接手根本不敢改。下面这套方案从源码设计的角度把领域建模、任务下发、实时位置接入和异常兜底拆开讲代码骨架可以直接照着复现。适合正在做无人机巡检平台、或者刚拿到 patrol 类后端源码想快速梳理架构的 Java 工程师。2. 巡维保障数据建模五张核心表与巡逻任务状态机的 Java 实现2.1 先画五张核心表uav-patrol-backend 的领域模型从哪里开始巡维保障的业务链路并不复杂划一条航线包含若干航点无人机按计划起飞飞行中不断上报坐标识别到缺陷就记录一条缺陷任务闭环后自动生成工单。顺着这条链路走核心表就是五张巡逻任务表、飞行架次表、轨迹点表、缺陷记录表、缺陷工单表。至于无人机台账、用户权限、航线模板这些按通用做法单独建模块不要在 patrol 核心表里堆字段。CREATE TABLE patrol_task ( id BIGINT AUTO_INCREMENT PRIMARY KEY, task_no VARCHAR(32) NOT NULL COMMENT 任务编号, drone_id BIGINT NOT NULL COMMENT 无人机ID, route_id BIGINT NOT NULL COMMENT 航线模板ID, status VARCHAR(16) NOT NULL COMMENT 任务状态, planned_start DATETIME NOT NULL COMMENT 计划开始时间, planned_end DATETIME NOT NULL COMMENT 计划结束时间, max_retry TINYINT NOT NULL DEFAULT 3 COMMENT 最大重派次数, retried_count TINYINT NOT NULL DEFAULT 0 COMMENT 已重派次数, create_by VARCHAR(64) NULL COMMENT 创建人, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_task_no (task_no), KEY idx_drone_status (drone_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT巡逻任务表; CREATE TABLE patrol_track_point ( id BIGINT AUTO_INCREMENT PRIMARY KEY, flight_id BIGINT NOT NULL COMMENT 飞行架次ID, seq INT NOT NULL COMMENT 上报序号客户端递增, lng DECIMAL(10, 6) NOT NULL COMMENT 经度, lat DECIMAL(10, 6) NOT NULL COMMENT 纬度, altitude DECIMAL(8, 2) NULL COMMENT 海拔米数, speed DECIMAL(6, 2) NULL COMMENT 速度m/s, report_time DATETIME NOT NULL COMMENT 设备上报时间, UNIQUE KEY uk_flight_seq (flight_id, seq), KEY idx_flight_time (flight_id, report_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT巡逻轨迹点表;表设计的核心在uk_flight_seq这个唯一键。很多人会直接用上报时间去重但设备时钟一旦漂移或断线重连同一秒可能有多条数据只有「架次 序号」才能真正保证轨迹不重复、不乱序。patrol_task里的retried_count是给后面自动重派用的不放在工单表因为重派是任务自己的责任。用 MyBatis-Plus 的话实体类加TableName注解后确实可以自动生成建表 SQL但生产环境我一般还是让 DBA 审过显式 DDL 再执行自动生成只适合开发库。2.2 巡逻任务状态机用 Java 枚举管理五态流转巡逻任务不是简单的待执行/已完成中间还有派发、起飞、异常等状态。把这些状态直接散落在 Service 里用 if 判断短期看着省事等出现「已完成的任务被再次派发」「异常任务被当成飞行中」这类问题就变成靠加班补漏洞了。更好的方式是做成领域枚举把流转规则收口到枚举本身。public enum TaskStatusEnum { DRAFT(待派发), ASSIGNED(已派发), FLYING(飞行中), FINISHED(已完成), ABNORMAL(异常终止); private final String desc; TaskStatusEnum(String desc) { this.desc desc; } /** * 判断从当前状态是否能跳转到目标状态 */ public boolean canTransitTo(TaskStatusEnum target) { switch (this) { case DRAFT: return target ASSIGNED; case ASSIGNED: return target FLYING || target ABNORMAL; case FLYING: return target FINISHED || target ABNORMAL; default: return false; } } }调用方在使用前先做一次currentStatus.canTransitTo(target)校验不通过直接抛业务异常。这就是把状态变迁封装成对象行为的好处校验逻辑集中不会被别的业务代码绕过。面向对象编程里最容易被忽略的恰恰不是继承多态而是用对象约束业务规则。这版枚举没有把 ABNORMAL 到 ASSIGNED 的流转放进去因为重派是一个生成新任务的过程旧任务保持异常终止状态存档而不是改状态复活。2.3 架次与任务分离把「任务」和「一次飞行」拆开的设计理由很多 patrol 系统设计者会把任务和飞行混在一张表里理由是无非「一个任务飞一次没必要拆」。等遇到周期巡逻任务就吃亏了同一个任务每天执行一次如果只存一条任务记录那么轨迹、缺陷、工单都挂在同一条记录下。按时间一查三个月的数据全混在一起回放时画面直接乱套。所以任务和架次必须拆开。patrol_task是业务单patrol_flight是任务的每次执行实例。一个任务可以关联多个架次每个架次再关联自己的轨迹和缺陷记录互不污染。这样设计还有一个好处统计「某个航线飞了多少次」「某台无人机平均故障率」这类指标时直接查架次表做聚合不需要动任务表。CREATE TABLE patrol_flight ( id BIGINT AUTO_INCREMENT PRIMARY KEY, task_id BIGINT NOT NULL COMMENT 巡逻任务ID, flight_no VARCHAR(32) NOT NULL COMMENT 架次编号, drone_id BIGINT NOT NULL COMMENT 无人机ID, status VARCHAR(16) NOT NULL COMMENT 架次状态, start_time DATETIME NULL COMMENT 实际起飞时间, end_time DATETIME NULL COMMENT 实际结束时间, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_flight_no (flight_no), KEY idx_task_id (task_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT飞行架次表;任务的status和架次的status是两个层面的事任务是调度状态架次是执行状态。任务飞完了可以再派新架次但架次不能自己改了状态再飞一次架次要么成功要么异常没有中间态。这套「任务-架次」两级模型是巡检类后端和普通工单系统最大的区别也是新手最容易踩的设计分岔路。3. 用 Spring Boot MyBatis-Plus 跑通巡逻任务下发的最小闭环3.1 依赖选型为什么是 Spring Boot MyBatis-Plus而不是其他全家桶巡逻后端的技术栈推荐 Spring Boot MyBatis-Plus MySQL这套组合在 Java 后端项目里最成熟招聘好找人出了问题社区答案也多。相比 RuoYi 这种全家桶脚手架MyBatis-Plus 更轻只解决数据访问层不绑架你的业务结构。RuoYi 适合管理后台快速出页面但巡维场景里的实时推送、状态流控和飞行任务编排最后都要自己改核心逻辑与其和脚手架磨合不如直接搭基础框架。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-spring-boot3-starter/artifactId version3.5.7/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-websocket/artifactId /dependency依赖里特意把 WebSocket starter 列出来因为无人机的实时坐标上报通常不走 HTTP 轮询那太浪费带宽也不够实时。MyBatis-Plus 的 3.5.x 对 Spring Boot 3.x 有独立的 starter直接引入就能用。包里没有写版本号的依赖由 Spring Boot 父工程统一管理。如果你用宝塔面板部署这类常规 Java 项目打 jar 包后丢上去跑就行不需要额外改依赖。3.2 任务下发接口从 Controller 到 Service 到 Mapper 的完整链路任务下发的业务动作是「创建任务并派给指定无人机」。前后端分离项目实战里前端提交的只是任务参数后端要校验无人机状态、生成任务编号、落库并返回结果。我把核心链路分成三层Controller 只做参数接收和响应封装Service 管业务校验与状态流转Mapper 层交给 MyBatis-Plus 的BaseMapper处理单表读写。RestController RequestMapping(/patrol/task) public class PatrolTaskController { private final PatrolTaskService patrolTaskService; public PatrolTaskController(PatrolTaskService patrolTaskService) { this.patrolTaskService patrolTaskService; } PostMapping(/dispatch) public ResultLong dispatch(RequestBody Valid DispatchTaskRequest request) { // request 里包含 droneId、routeId、plannedStart、plannedEnd return Result.ok(patrolTaskService.dispatchTask(request)); } }Service public class PatrolTaskServiceImpl implements PatrolTaskService { private final PatrolTaskMapper patrolTaskMapper; public PatrolTaskServiceImpl(PatrolTaskMapper patrolTaskMapper) { this.patrolTaskMapper patrolTaskMapper; } Override Transactional(rollbackFor Exception.class) public Long dispatchTask(DispatchTaskRequest request) { // 1. 校验无人机当前是否空闲避免重复派单 Long busyCount patrolTaskMapper.countBusyByDroneId(request.getDroneId()); if (busyCount ! null busyCount 0) { throw new BizException(该无人机存在未结束的巡逻任务); } // 2. 创建任务初始状态为 DRAFT PatrolTask task new PatrolTask(); task.setTaskNo(PT System.currentTimeMillis()); task.setDroneId(request.getDroneId()); task.setRouteId(request.getRouteId()); task.setPlannedStart(request.getPlannedStart()); task.setPlannedEnd(request.getPlannedEnd()); task.setStatus(TaskStatusEnum.DRAFT.name()); task.setMaxRetry(3); patrolTaskMapper.insert(task); // 3. 直接派发状态变为 ASSIGNED patrolTaskMapper.updateStatus(task.getId(), TaskStatusEnum.ASSIGNED.name()); return task.getId(); } }countBusyByDroneId是自定义 SQL查的是patrol_task里status IN (ASSIGNED,FLYING)且drone_id ?的记录数。这一步必须在 insert 之前做否则业务上会出现并发重复派单。把事务注解放在 Service 方法上而不是 Controller 上保证业务操作要么全部成功要么全部回滚。这里max_retry初始设 3到时自动重派逻辑会用到。3.3 定时巡逻任务Scheduled 的参数细节周期性巡逻是巡维保障的刚需比如变电站每天早晚各巡一次。Spring 的Scheduled是最轻量的实现方式单实例部署完全够用。要注意的是 cron 表达式在 Spring 里是 6 位和 Quartz 的 7 位不一样写错一位就静默不执行。Component public class PatrolScheduleJob { private final PatrolTaskService patrolTaskService; public PatrolScheduleJob(PatrolTaskService patrolTaskService) { this.patrolTaskService patrolTaskService; } /** * 每天 06:00 和 18:00 各生成一次巡逻任务 */ Scheduled(cron 0 0 6,18 * * ?) public void generateDailyPatrol() { ListRouteTemplate routes patrolTaskService.listActiveRoutes(); for (RouteTemplate route : routes) { DispatchTaskRequest request new DispatchTaskRequest(); request.setDroneId(route.getDefaultDroneId()); request.setRouteId(route.getId()); request.setPlannedStart(LocalDateTime.now().plusMinutes(5)); request.setPlannedEnd(LocalDateTime.now().plusHours(1)); try { patrolTaskService.dispatchTask(request); } catch (BizException e) { // 记录日志后继续生成下一个任务不能因为单条失败中断整批 log.warn(定时生成巡逻任务失败, routeId{}, reason{}, route.getId(), e.getMessage()); } } } }cron 0 0 6,18 * * ?里的6,18表示 6 点和 18 点两个时刻执行。如果你用fixedRate它是按任务开始时间间隔触发的前一个任务卡住时下一个会立刻补上容易造成任务堆积fixedDelay则是等上一个任务执行完才开始计时。定时生成任务这种场景用 cron 最合适。这里的 catch 很关键批量生成任务时一条失败不能拖垮整批但要把失败记录打到日志里否则夜里定时任务挂了白天才发现。4. 无人机位置接入与轨迹回放WebSocket 推送和存储方案选型4.1 用 WebSocket 接收无人机坐标握手鉴权与心跳保活无人机端到后端的数据通道工业界通用做法是 MQTT但很多自研系统的无人机端只支持 WebSocket尤其是一些轻量级飞控板。以 WebSocket 方案为例Spring 的TextWebSocketHandler足够支撑几千个并发连接重点在于握手时做鉴权以及连接建立后的心跳兜底。Component public class DroneLocationHandler extends TextWebSocketHandler { private static final ConcurrentHashMapLong, WebSocketSession SESSION_MAP new ConcurrentHashMap(); Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { Long droneId (Long) session.getAttributes().get(droneId); if (droneId null) { session.close(CloseStatus.NOT_ACCEPTABLE); return; } SESSION_MAP.put(droneId, session); } Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { Long droneId (Long) session.getAttributes().get(droneId); // message payload 形如 {flightId:123,seq:1,lng:113.9,lat:22.5,alt:120.5} LocationReport report JSON.parseObject(message.getPayload(), LocationReport.class); locationService.saveReport(droneId, report); } Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { Long droneId (Long) session.getAttributes().get(droneId); if (droneId ! null) { SESSION_MAP.remove(droneId); } } }握手鉴权通过配置类实现在HandshakeInterceptor里从查询参数或 Header 里取 token解析出 droneId 后塞进session.getAttributes()。这样 handler 里不需要再解析 token业务更干净。心跳这里有个现实问题无人机端如果突然断电TCP 连接不会立刻断服务端要定期扫SESSION_MAP超过 30 秒没收到任何消息的连接主动关掉把对应架次标记为通信中断。4.2 轨迹存储MySQL 追加写还是 Redis GEO坐标数据落库有两条路直接写 MySQL 轨迹表或者用 Redis GEO 做实时位置。我见过不少项目纠结其实两者不是二选一而是分管不同场景。存储方案适合场景主要问题MySQL 追加写历史轨迹回放、报表统计、缺陷定位复现写入频繁需要批量插入和索引优化Redis GEO实时位置查询、附近无人机搜索内存成本高重启丢数据不适合长期存档轨迹回放要的是「当时完整路径」MySQL 一条条追加写完全够用。实时位置展示才用 Redis GEO 存最新坐标比如大屏上要实时显示无人机在地图上的当前位置。两个存储之间的数据不是互斥的无人机每上报一条坐标先写 MySQL再把最新坐标覆盖写进 Redis GEO 对应 key。查询当前在线位置走 Redis查询历史航线走 MySQL各干各的活。4.3 轨迹回放接口按架次拉有序坐标点的查询设计轨迹回放接口的入参是 flightId返回按时间排序的坐标数组。这里最容易翻车的是排序字段选择。设备上报时间report_time可能因为 GPS 信号不好产生几秒偏差多条数据在秒级相同排序结果不稳定回放时画面会抖。解决思路是让无人机端维护一个自增 seq服务端以seq为准排序report_time只做展示字段。public ListTrackPointVO replayTrack(Long flightId) { LambdaQueryWrapperPatrolTrackPoint wrapper new LambdaQueryWrapper(); wrapper.eq(PatrolTrackPoint::getFlightId, flightId) .orderByAsc(PatrolTrackPoint::getSeq); ListPatrolTrackPoint points trackPointMapper.selectList(wrapper); return points.stream().map(p - { TrackPointVO vo new TrackPointVO(); vo.setLng(p.getLng()); vo.setLat(p.getLat()); vo.setAltitude(p.getAltitude()); vo.setReportTime(p.getReportTime()); return vo; }).toList(); }这个查询走uk_flight_seq唯一索引数据量大时也不会慢。如果轨迹点超过几千条前端回放加载会卡可以在接口里加一个采样参数 step比如每隔 5 个点返回一个减轻前端渲染压力。采样只影响展示不影响原始数据完整性。还有一点轨迹点不要一次查出来全放内存再排序MyBatis-Plus 的orderByAsc直接在 SQL 里排好数据库干这活比 Java 快得多。5. uav-patrol-backend 避坑清单五个让巡维后端翻车的真实场景5.1 现象同一架无人机被重复派单两套任务同时起飞有一回同事反馈线上出现两台无人机往同一航线飞排查后发现是操作员在界面上连点了两次「下发」。原因是任务创建时只检查了无人机状态但两次请求并发进来两个线程同时读到「空闲」于是都通过了校验。原因就是缺少数据库层面的兜底约束。解决分两步第一步在patrol_task表加一个「无人机进行中唯一」的约束第二步用 Redis 分布式锁把派单动作串行化。数据库约束兜底Redis 锁提升响应速度两道保险缺一不可。只靠代码 if 判断做并发控制迟早会漏。提示MySQL 没有「部分索引」概念但可以用生成列解决。建一个drone_busy_status生成列值在 ASSIGNED 或 FLYING 时为 drone_id否则为 NULL然后对该生成列建唯一索引即可实现「同一无人机最多一个进行中任务」。5.2 现象断线重连后坐标乱序回放轨迹打结无人机穿越信号死角时 WebSocket 会断开重连后客户端把断线期间的缓存坐标一次性补发。后端按收到顺序落库结果序号 101、105、102 这样交错的顺序被写进了表回放时飞机坐标忽前忽后。原因是表结构里没有唯一键约束重复和乱序数据都能插入。解决就是 2.1 节里uk_flight_seq (flight_id, seq)唯一索引再配合插入时的「忽略冲突」语义。MyBatis-Plus 里可以自定义一条INSERT IGNORE INTO patrol_track_point ...的 SQL重复序号直接丢弃不乱序。断线重传的数据虽然有重复但幂等处理后轨迹依然是完整的。5.3 现象批量生成巡逻任务时连接池耗尽接口全部超时定时任务一次性给 200 台无人机生成巡逻任务代码里 if 循环逐条 insert每条 insert 占一次数据库连接。任务本身还要查无人机空闲状态每个循环至少两次数据库往返连接池默认 10 个连接根本不够用最后整个后端接口全部超时。这是典型的「代码写法没考虑连接复用」。解决批量任务用 MyBatis-Plus 的saveBatch或者自己写foreach批量 insert一批 100 条只占一次连接。连接池参数也要对应调整HikariCP 的maximum-pool-size根据应用并发量设到 20~50 比较稳太大反而浪费内存。5.4 现象定时巡逻任务到点没触发集群部署后却跑了两遍单实例部署时Scheduled很乖一旦上两台实例做负载均衡定时任务就会在各实例上同时执行导致重复派单。更隐蔽的是时区问题服务器时区设置为 UTC 的话cron 里写的 6 点实际会在北京时间 14 点执行。遇到这种情况第一反查服务器date命令输出第二确认应用启动参数里的-Duser.timezoneAsia/Shanghai。解决集群重复执行简单可靠的做法是数据库分布式锁定时任务开始时往一张锁表插入一条带日期和任务类型的记录插入成功才执行插入失败说明已有其他实例在跑。等任务结束再删除记录。这套方案足够支撑中小规模集群非要去上 XXL-Job 反而增加运维成本。5.5 现象前端联调报跨域token 过期后任务中断没有任何提示前后端分离项目实战里最常碰到的接口问题就是跨域。巡维系统的前端地图页面跑在 8080后端接口在 8081浏览器默认拦截。后端不做 CORS 配置前端只能靠代理转发绕路本地能跑部署到生产环境又冒出新问题。配置一个全局 CORS 过滤器把允许的域名、方法、Header 白名单写清楚一次解决。token 过期的问题是另一个坑拦截器没有把 WebSocket 握手路径加进白名单无人机连接被 401 挡在门外或者前端轮询时收到 401 但全局拦截器只处理了 200页面一直转圈没提示。拦截器里要排除/ws/**这类握手路径同时统一返回结构的 HTTP 状态码保持 401前端 axios 拦截器才能统一跳登录。6. 异常任务自动重派与人工复核把兜底逻辑做成可验证的源码巡逻任务异常终止后不能就这么晾着否则整套巡维保障就成了定时器空转。常见的做法是任务进入 ABNORMAL 后如果失败原因不是人为取消且重派次数没超上限等待 5 分钟自动生成新任务重新执行。人工只介入真正需要判断的场景比如识别置信度低于阈值时自动创建「待人工复核」工单而不是直接生成正式缺陷单。Scheduled(fixedDelay 60000) public void autoRedispatchAbnormalTasks() { ListPatrolTask abnormalTasks patrolTaskMapper.listAbnormalTasksBefore(5, TimeUnit.MINUTES); for (PatrolTask task : abnormalTasks) { if (task.getRetriedCount() task.getMaxRetry()) { continue; // 超过重派上限转入人工介入队列 } DispatchTaskRequest request new DispatchTaskRequest(); request.setDroneId(task.getDroneId()); request.setRouteId(task.getRouteId()); request.setPlannedStart(LocalDateTime.now().plusMinutes(1)); request.setPlannedEnd(LocalDateTime.now().plusHours(1)); try { patrolTaskService.dispatchTask(request); patrolTaskMapper.incrementRetriedCount(task.getId()); } catch (BizException e) { log.warn(自动重派失败taskId{}, reason{}, task.getId(), e.getMessage()); } } }这段逻辑里最该关注的是listAbnormalTasksBefore它必须加WHERE status ABNORMAL AND update_time NOW() - INTERVAL 5 MINUTE条件否则任务刚异常就立刻被重派无人机可能还在返航途中状态根本没复位。验证这套兜底逻辑不要只在浏览器里看接口返回用 MockMvc 写一个集成测试创建一个 DRAFT 任务强行把状态改成 ABNORMAL再触发定时方法断言生成了新任务且旧任务的重派次数加一。压测时也别只测并发创建要把状态流转和重派穿插在一起压才能真正发现补偿逻辑的边界问题。我自己在这套状态机上栽过跟头最初把重派次数放在任务表里但没加max_retry上限结果一架失联无人机在异常后反复被派单直到人工凌晨三点被电话叫起来才发现。后来才把重派控制做成结构化的字段和枚举校验。做巡维后端宁可多写两行校验也不要让无人机干等指令希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑