尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

无人机巡检后端全流程闭环:任务、设备与媒体管理实战

发布时间:2026/9/23 18:09:03

资讯中心
01
ARTICLE

无人机巡检后端全流程闭环:任务、设备与媒体管理实战

无人机巡检后端全流程闭环:任务、设备与媒体管理实战
简介这款基于Java语言的uav-patrol-backend巡维保障系统后端代码设计源码定位为无人机巡维保障场景的服务器端参考实现适合Java后端开发人员、无人机巡检项目团队以及相关专业学生研读与二次开发。压缩包共51个文件、约93KB主要包含44个Java源文件、3个XML配置文件、2个YAML配置文件、1个Git忽略规则文件和1个说明文档。Java源文件按模块组织集中处理核心业务逻辑、数据持久化、权限校验与外部接口交互XML与YAML文件用于定义应用环境、数据库连接、日志策略等参数便于部署时灵活调整readme.txt给出了项目介绍、配置说明与启动方式配合pom.xml可快速完成Maven构建。资源目前已有332人学习浏览整体结构紧凑清晰适合理解模块化后端设计、配置分离思想及Maven工程组织方法。通过这份源码读者既能掌握无人机巡维后端的功能拆解与实现细节也能积累一套可迁移到类似企业级Java服务项目中的代码范式。1. 无人机巡检后端到底在管什么一套把任务、设备和媒体串起来的巡维体系做无人机巡检的后端关键不在“把无人机数据接进来”而在让整个巡维保障流程从任务创建到结果归档都能闭环。基于 Java 语言的 uav-patrol-backend 巡维保障系统就是把任务下发、设备调度、媒体回传、告警通知这些环节串成一套有状态、可追溯、能扛住现场实际运维节奏的后端代码设计。这套方案适合两类人看一类是要在公司内部从零搭建无人机巡维平台的后端开发另一类是正在做 Java 课程设计或毕业设计、想找一个能落地的后端源码骨架的学生。看完你能照着把核心流程跑通也能避开我在生产环境里踩过的权限和并发坑少走一两周弯路。2. 模块化单体与数据模型设计巡维保障系统的后台骨架一套巡维系统上线的时候最容易被问的问题是“用了什么架构”。我的回答通常很直接模块化单体Redis 做缓存和锁MQTT 接设备上行数据对象存储放照片和视频。业务量没到日均百万级之前微服务拆出来只是给自己找麻烦。这一章先把架构选型和数据模型讲清楚后面再展开任务下发和文件回传的代码。2.1 为什么不用微服务先从规模反推架构先看实际的并发特征。无人机巡维系统的高峰流量来自两个方向一是多台无人机同时回传遥测一般单台每秒 1~2 条二是现场飞手和管理员同时操作 Web 端人数通常在几十到几百。这样一个量级单体应用只要把数据库连接池和线程池调好完全不会有压力。真正的复杂度在业务状态而非并发量。任务要经过审核、下发、执行、回传、归档设备要区分离线、空闲、作业和维护媒体文件要和任务关联。如果一上来就拆成任务服务、设备服务、媒体服务三个微服务每一个的状态变更都用消息去同步现场的排查难度立刻翻倍。所以我一般这样划分工程结构com.patrol ├── controller // 对外 HTTP 接口只做参数校验与结果封装 ├── service // 业务逻辑任务编排、设备管理、数据回传 ├── mapper // MyBatis Plus 数据访问层 ├── entity // 数据库实体 ├── dto // 入参出参对象禁止前端直接透传实体 ├── enums // 任务和设备的状态枚举统一管理状态机 ├── listener // MQTT 监听接收无人机上报的数据 ├── scheduler // 定时任务处理超时和重试 └── config // 安全、缓存、对象存储等配置依赖方向是 controller 调 service 调 mapperlistener 和 scheduler 也走 service不直接操作 mapper。这条规则看起来简单但能保证后面加协议适配时不用大改业务层。2.2 核心表结构与建表 SQL任务、设备、媒体、日志四张表我习惯把数据分成四类巡检任务、设备档案、媒体文件、飞行日志。业务上所有操作基本都在围绕这四张表转。先看任务表。CREATE TABLE patrol_task ( id BIGINT AUTO_INCREMENT PRIMARY KEY, task_no VARCHAR(32) NOT NULL COMMENT 任务编号格式 TyyyyMMddHHmmss, device_id BIGINT NOT NULL COMMENT 执行本次巡检的设备ID, route_id BIGINT DEFAULT NULL COMMENT 飞行航线ID由航线规划服务生成, task_type TINYINT NOT NULL COMMENT 1-日常巡维 2-应急保障 3-训练, status TINYINT NOT NULL DEFAULT 0 COMMENT 0-待审核 1-待执行 2-执行中 3-已完成 4-已取消 5-执行失败, priority TINYINT DEFAULT 2 COMMENT 优先级1-最高 2-普通, assignee BIGINT DEFAULT NULL COMMENT 执行飞手ID, audit_by BIGINT DEFAULT NULL COMMENT 审核人ID, start_time DATETIME DEFAULT NULL, end_time DATETIME DEFAULT NULL, fail_reason VARCHAR(255) DEFAULT NULL, created_time DATETIME DEFAULT CURRENT_TIMESTAMP, updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_device_id (device_id), KEY idx_status (status), KEY idx_time_range (start_time, end_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT巡检任务表;几个字段要说明一下。task_no 单独用一列存业务编号不要直接用自增 id 展示给现场人员不然现场抄写任务号的时候容易抄错位。status 用数字不要在数据库里存字符串状态解释放在枚举里统一维护。start_time 和 end_time 联合索引是为了支撑按时间段筛选任务的统计页面实际项目里这类查询最多。设备表CREATE TABLE device_info ( id BIGINT AUTO_INCREMENT PRIMARY KEY, device_code VARCHAR(64) NOT NULL UNIQUE COMMENT 设备出厂编号, device_type TINYINT NOT NULL COMMENT 1-无人机 2-红外挂载 3-光学相机, device_name VARCHAR(64) NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0-离线 1-空闲 2-作业中 3-维护中, last_lat DECIMAL(10, 6) DEFAULT NULL, last_lng DECIMAL(10, 6) DEFAULT NULL, last_online_time DATETIME DEFAULT NULL, warn_level TINYINT DEFAULT 0 COMMENT 0-正常 1-关注 2-告警, created_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_status (status), KEY idx_warn_level (warn_level) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT设备信息表;设备状态和任务状态是两套独立的枚举不要复用。设备的一直在变任务的是一次生命周期二者用不同的枚举维护后面写逻辑清晰很多。last_lat 和 last_lng 是冗余字段只用来在地图上展示最近一次位置实时轨迹放到 Redis 和飞行日志里这个设计下一节细讲。媒体文件表CREATE TABLE media_file ( id BIGINT AUTO_INCREMENT PRIMARY KEY, task_id BIGINT NOT NULL COMMENT 归属任务, device_id BIGINT NOT NULL COMMENT 采集设备, bucket_name VARCHAR(64) NOT NULL COMMENT 对象存储桶名, object_name VARCHAR(255) NOT NULL COMMENT 对象存储对象名, file_size BIGINT NOT NULL DEFAULT 0, content_type VARCHAR(64) DEFAULT NULL, width INT DEFAULT NULL COMMENT 照片宽度视频可为空, height INT DEFAULT NULL COMMENT 照片高度视频可为空, upload_status TINYINT NOT NULL DEFAULT 0 COMMENT 0-待上传 1-已上传 2-上传失败, created_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_task_id (task_id), KEY idx_device_id (device_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT媒体文件表;这张表不存文件字节只存对象存储的位置。task_id 必须建索引因为现场最常做的操作就是“把这个任务的照片全部打包下载”。upload_status 用来标识上传状态配合第 4 章的预签名 URL 使用。飞行日志表CREATE TABLE flight_log ( id BIGINT AUTO_INCREMENT PRIMARY KEY, task_id BIGINT NOT NULL, device_id BIGINT NOT NULL, log_type TINYINT NOT NULL COMMENT 1-遥测 2-事件 3-告警, content JSON DEFAULT NULL COMMENT 不同厂商的字段差异大用 JSON 承载, record_time DATETIME NOT NULL, KEY idx_task_id_time (task_id, record_time), KEY idx_device_id_time (device_id, record_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT飞行日志表;JSON 字段在这张表里是合理的选择。不同厂商上报的遥测字段差异很大建一列就有一列的成本而 JSON 能先把原始数据完整保存等真正要高频查询某个字段时再做字段提取。这个权衡在数据采集类表里经常遇到记住“先写入后查询”这个顺序就不会选错。2.3 设备状态与实时位置的数据组织方式设备位置是高频数据。无人机飞行时每 3 到 5 秒上报一次经纬度和高度一台设备一天飞 2 小时就是 2400 条。如果这些数据每一帧都直接写 MySQL数据库会立刻成为瓶颈而且 Web 端地图刷新根本不需要这么高的历史精度。实时位置放在 Redis。我一般用一个 Hash 结构key 是device:gps:{deviceId}字段分别是 lat、lng、alt、heading、updateTime。无人机端每上报一条listener 就更新一次 Hash同时把原始报文追加写入飞行日志表。这样地图页面查询实时位置走 Redis毫秒级返回历史轨迹查数据库天级数据量也能接受。这里有个常见取舍要不要用 Redis GEOGEO 适合做“查找附近设备”但巡维场景里设备位置基本固定只有执行任务时才移动附近查找功能一致性不高没必要引入额外概念。设备状态则加一层状态缓存把判断逻辑收敛在 service 层不要在 controller 里直接读 Redis key否则前端会拼出一堆 Redis key后期一改命名规则前端也要跟着改这种耦合要尽早避免。3. 任务下发与状态机从工单审核到无人机起飞的全链路代码任务模块是这个系统的核心也是并发问题的高发区。把任务状态机设计好后面加“一键下发”“批量排班”都只是顺手的事如果状态机没设计好你会发现改业务时任何状态变更都拖着一堆奇怪的判断条件越改越乱。3.1 状态机选型不要用 if/else 写完一个任务生命周期任务生命周期其实很清晰待审核、待执行、执行中、已完成、已取消、执行失败。真正容易做错的不是状态数量而是“允许谁转到谁”。比如执行中的任务不能直接被改成已完成前提是设备已经落地、媒体已经回传完成待审核的任务不能跳转到执行中必须经过审核确认。我把这些规则收敛到一个枚举里用 canTransitTo 方法统一判断。public enum TaskStatus { PENDING_AUDIT(0, 待审核), APPROVED(1, 待执行), EXECUTING(2, 执行中), COMPLETED(3, 已完成), CANCELLED(4, 已取消), FAILED(5, 执行失败); private final int code; private final String desc; TaskStatus(int code, String desc) { this.code code; this.desc desc; } public boolean canTransitTo(TaskStatus target) { switch (this) { case PENDING_AUDIT: return target APPROVED || target CANCELLED; case APPROVED: return target EXECUTING || target CANCELLED; case EXECUTING: return target COMPLETED || target FAILED; default: return false; } } }注意把 switch 的 default 返回 false。状态机的价值在于非法流转在入口就被拦截而不是让它在业务代码里一路传播最后产生一条“已完成的待审核任务”。写状态机时不要用字符串常量散落在 service 里统一回收成枚举引用后面把枚举换成数值类型时也比较顺手。3.2 任务下发接口实现带分布式锁的完整 Java 代码任务下发是整个巡维流程里最容易出问题的接口。如果两个管理员同时审核同一个任务系统不能发出两条起飞指令如果任务已经因为超时被取消下发的指令就要立刻作废。这里的关键是“先抢锁再改状态最后下发”。Service public class TaskDispatchService { Autowired private StringRedisTemplate redisTemplate; Autowired private PatrolTaskMapper taskMapper; Autowired private DeviceInfoMapper deviceInfoMapper; Autowired private FlightLogMapper flightLogMapper; public boolean dispatchTask(Long taskId) { String lockKey lock:task:dispatch: taskId; Boolean locked redisTemplate.opsForValue() .setIfAbsent(lockKey, 1, Duration.ofSeconds(10)); if (Boolean.FALSE.equals(locked)) { throw new BizException(任务正在处理中请勿重复提交); } try { PatrolTask task taskMapper.selectById(taskId); if (task null) { return false; } TaskStatus currentStatus TaskStatus.fromCode(task.getStatus()); if (!currentStatus.canTransitTo(TaskStatus.EXECUTING)) { taskMapper.saveFailReason(taskId, 任务状态不正确不能下发); return false; } DeviceInfo device deviceInfoMapper.selectById(task.getDeviceId()); if (device.getStatus() ! DeviceStatus.IDLE.getCode()) { taskMapper.saveFailReason(taskId, 设备不在空闲状态); return false; } String command buildPatrolCommand(task); // 用条件更新保证只有待执行状态才能推进 int updated taskMapper.compareAndSetStatus(taskId, TaskStatus.APPROVED.getCode(), TaskStatus.EXECUTING.getCode()); if (updated 0) { taskMapper.saveFailReason(taskId, 任务已被其他操作修改); return false; } deviceInfoMapper.updateDeviceStatus(task.getDeviceId(), DeviceStatus.WORKING.getCode()); flightLogMapper.insert(taskId, task.getDeviceId(), 任务下发, command); return true; } finally { redisTemplate.delete(lockKey); } } }这段代码有四个参数值得关注。锁的过期时间定为 10 秒不是越长越好如果业务线程卡死锁要能自动释放否则下一次人工重试永远会被“正在处理中”拦住。设备状态判断放在锁内是为了避免选中设备后又发现设备已经被别的任务占用。compareAndSetStatus 的 SQL 是UPDATE patrol_task SET status #{target} WHERE id #{taskId} AND status #{expect}它和 Redis 锁构成双重保险Redis 锁失效时数据库的条件更新仍然能挡一次。日志在最后插入保证只有成功下发的任务才有日志避免出现“日志有了但任务没下发”的假象。有个实际项目中经常遇到的问题锁的 key 粒度按 taskId 维度的好处是不同任务互不阻塞但一台设备如果可能同时执行多个任务还需要再加一把设备维度锁把 deviceId 作为锁 key否则同一台无人机会被并发下发两个冲突的航线。3.3 超时重试与断点续传任务执行中断后怎么恢复任务下发了无人机也起飞了结果现场网络断了Web 端一直显示“执行中”。这时候如果不去管任务会永远挂在执行中设备也会被占用成作业状态后续排班直接被卡死。我一般会用定时任务做超时扫描。做法是每两分钟扫描一次执行中且超过十五分钟没有新遥测的任务把任务标记为执行失败同时把设备状态释放为空闲。扫描条件写成一个简单的 SQLSELECT id, device_id FROM patrol_task WHERE status 2 AND updated_time NOW() - INTERVAL 15 MINUTE。执行到这个分支后把任务状态置为 FAILED再根据设备是否正在执行其他任务决定是否重置为空闲。这里要留意一个细节超时时间不能写死在代码里。现场不同机型飞行时长不同长航时无人机单次任务可能超过四十分钟十五分钟的阈值就不适用。把超时阈值放到配置中心或者数据库配置表里运维调整时不用重新发布这套系统在真实环境里少一次发布就少一次风险。4. 媒体回传与文件落库航拍照片、视频和飞行日志的存储方案现场飞手在手机上报一张巡检照片文件大小动辄几 MB一个任务回传几百张很常见。如果后端用 MultipartFile 接收再转发到对象存储Tomcat 默认的 10MB 限制和内存拷贝会先让你吃苦头。这一章讲的是我实践下来比较顺手的方案预签名 URL 直传对象存储。4.1 MinIO 预签名 URL 上传后端只发凭证不传字节流程是无人机端或者飞手 App 调用后端接口申请上传凭证后端生成一个带有效期的预签名 URL然后前端直接朝这个 URL 发 PUT 请求对象存储把文件收下。后端全程不接触文件字节内存开销极小速度也快。PostMapping(/media/presign) public ResultPresignResp presign(RequestBody PresignReq req) { String objectName buildObjectName(); objectName.append(req.getTaskId()).append(/) .append(req.getDeviceId()).append(/) .append(System.currentTimeMillis()).append(_) .append(UUID.randomUUID().toString().replace(-, )) .append(suffix); MapString, String headers new HashMap(); headers.put(Content-Type, req.getContentType()); String url minioClient.getPresignedObjectUrl( GetPresignedObjectUrlArgs.builder() .method(Method.PUT) .bucket(patrol-media) .object(objectName.toString()) .expiry(600) .extraHeaders(headers) .build()); mediaFileMapper.insertPendingRecord(req.getTaskId(), req.getDeviceId(), patrol-media, objectName.toString(), req.getFileSize(), req.getContentType()); return Result.ok(new PresignResp(url, objectName.toString())); }参数里有几个值得讲清楚。expiry 定 600 秒也就是 10 分钟。时间太短现场弱网环境上传大文件会超时时间太长泄露的 URL 会被人拿去随意上传文件。objectName 按“任务ID/设备ID/时间戳_随机值”组织这样同一个任务的文件在存储桶里自然聚在一起运维导数据方便。Content-Type 必须透传到预签名请求的 header 里否则 MinIO 会默认按 application/octet-stream 存储后端起缩略图时会识别不了图片格式。有个容易被忽略的坑预签名 URL 只保证上传通道不保证上传一定会完成。所以必须先把媒体文件记录插到数据库upload_status 置为 0上传完成后回调后端接口确认。这一步一旦漏掉下次想统计“哪些照片没传回来”就只能去存储桶里翻。4.2 文件记录与任务绑定防止磁盘满是隐患对象存储也有容量上限磁盘满了会直接影响整个集群的写入性能。我一般加一个“文件待确认”定时任务每隔十分钟扫描 upload_status0 且创建时间已超过半小时的媒体记录把状态改成上传失败同时调用对象存储接口确认对象是否存在避免存储中残留孤儿文件。确认上传完成的回调解法也很直接。前端上传完调用/media/confirm后端只做一件事把对应记录的 upload_status 置为已上传并补充文件大小。这个接口不需要接收文件字节所以性能压力几乎为零。真正的文件校验交给对象存储的 etag 字段用它在回调里比对大小。这样设计之后任务详情页拿到的媒体列表就是一张稳定的记录表查询走索引回显时再拼对象存储的访问地址。数据库里永远只有文件的元数据不会因为一次传大文件把业务库的连接池占满这也是我在几个项目里反复验证过的方式。4.3 遥测数据分表写入每 5 秒一条数据时的写入策略巡维保障系统里除了照片还有一类高频数据飞行遥测。每台无人机每 3 到 5 秒上报一次经纬度、高度、速度、电量一天飞下来就是几万条。全表存储的话坏消息是查历史轨迹会越来越慢。我的做法是对飞行日志表按月分表表名规则是 flight_log_202506。写入时在 listener 层按当前时间拼表名查询时按时间范围路由到对应表。代码层面用一个 TableNameHandler 做动态表名MyBatis 框架对这类场景支持已经比较完善。这里有个细节跨月的任务会跨两张表查询时要按区间拆成两个子查询再合并否则会丢数据。多数时光其实不需要过度设计按索引 分表的组合足够应付现场使用。5. 避坑注意权限拦截、并发入库和协议解析的踩坑记录这个章节我整理了自己在类似项目里踩过的坑。写成“现象—原因—解决”三段式方便你在现场快速对照。5.1 权限配置被全局拦截器卡住白名单顺序的问题现象登录接口突然返回 401或者所有接口都变成匿名可访问前端页面要么无法登录要么绕过登录直接看到数据。原因做权限拦截时把.anyRequest().authenticated()放到了白名单前面导致所有请求先经过认证判断白名单形同虚设反过来如果把permitAll()不小心覆盖了所有路径安全机制就完全失效。解决把白名单放在最前面最后再放anyRequest().authenticated()统一兜底。同时加一个启动自检项目启动后自动扫描所有 Controller 的公开接口检查是否都存在于白名单中避免新增接口时漏配。5.2 任务被重复下发并发审批引发的“幽灵任务”现象两个管理员同时点击审核通过同一个任务被下发两次无人机收到两条起飞指令现场乱套。原因任务状态在数据库里还是待执行两个请求同时读到都认为自己是合法操作。没有 Redis 锁或数据库条件更新保护时步骤会完整执行两遍。解决按照第 3.2 节的方案Redis 锁加条件更新双保险。单独靠 Redis 锁并不可靠因为锁可能在业务执行完之前过期单独靠条件更新也不能防止重复日志两边一起上才稳。压测时专门模拟并发审批这个场景日志里出现“任务正在处理中”且只有一条成功记录才算通过。5.3 媒体扫描线程把业务线程池耗尽磁盘 IO 冲突现象点击“导出全部照片”后整个系统接口变慢地图刷新也卡顿。原因导出功能直接在主线程里遍历对象存储逐个生成缩略图把磁盘 IO 和 CPU 都吃满了业务接口被拖垮。解决导出和缩略图生成全部丢到异步线程池并加信号量控制并发数同时把缩略图按任务缓存到本地临时目录避免重复生成。这里的教训是任何涉及大规模文件遍历的操作都不能直接挂在请求线程上否则一个功能就能拖垮整个服务。5.4 协议解析乱码不同厂商的字段对不齐现象某厂无人机上报的经纬度偶尔会偏到海里排查半天发现是字节序问题。原因厂商 A 用高字节在前厂商 B 用低字节在前后端代码却只用一套解析逻辑。解决定义 ProtocolParser 接口按 device_type 用策略模式匹配解析器在报文体里保留原始报文方便回放排查。策略模式在这个场景里非常合适加新厂商时只需要新增一个实现类不用改原有代码。解析错误时把整条原始报文落库这条数据就是后续排查的唯一依据。6. 进阶接口耗时分析与上线前压测这个系统能不能稳定上线最后要看两个指标接口平均耗时和任务下发成功率。我习惯在开发环境完成第一阶段验证后再加一个切面统计用接口耗时作为日常排障的入口。6.1 用注解加切面给接口加上耗时统计Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface ApiTimeLog { String value() default ; }在 Controller 的方法上标一下切面统一处理。核心逻辑在切面里把开始时间和结束时间算出来超过 500ms 的接口单独打一条 warn 日志并把参数摘要带进去。这样上线后只要翻日志就能快速看到“哪个接口、哪个参数导致慢查询”。日志的落库不能直接写在切面里要用一个异步线程池发布事件否则统计行为本身会把接口拖慢这就本末倒置了。6.2 压测任务下发接口的注意事项我用 ab 或 JMeter 做压测。命令一般是ab -n 2000 -c 50 -p ./dispatch.json -T application/json http://localhost:8080/task/dispatch-c 50 是并发用户数-n 2000 是总请求数。跑完看两个数据Requests per second 是否稳定以及响应时间的中位数和 90% 分位。更重要的是观察业务日志里有没有大量“任务正在处理中”这样的异常。如果有说明锁起到作用了如果一条都没有可能是压测参数没有真正产生并发也可能是锁粒度太大把所有请求都挡掉了。我习惯在压测前把 Redis 里的锁 key 都清一遍否则上一次压测留下的残留锁会让结果失真。压测后还要检查任务表里有没有超过 15 分钟未完成的任务确认超时扫描能正常兜底。做完这两步现场上线才有底气。从做这个巡维系统以来我的习惯一直没变先画状态机再动表结构先保底超时任务再优化导出报表。把这些小事控制住系统多半就不会在大规模上线时翻车。希望帮到你。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。