简介:这份资源是基于若依框架构建的无人机智慧巡检后端系统源码,面向需要实现无人机实时视频流检测报警、数据报表查看和大屏可视化展示的Java后端开发人员。源码共405个文件,主体为333个Java源文件,搭配XML与YAML配置文件、VM模板文件以及PNG/JPG等界面图片,压缩包约11.3MB,目录结构清晰,便于模块化检索与后续维护。目前已有362人学习下载。系统利用若依框架的轻量级快速开发特性,实现了前后端分离架构,包含数据库脚本、Maven配置和readme说明,能够帮助读者从环境搭建、配置管理到核心功能实现形成完整认知。源码尤其突出实时视频流处理与报警逻辑,以及大屏数据的可视化呈现,适合作为无人机智慧巡检领域后端技术的学习参考和二次开发基础,也可直接用于相关项目的快速原型搭建与功能扩展。
1. 无人机智慧巡检后端,为什么我选若依而不是从零搭项目
前几天帮一家做园区安防的公司做无人机智慧巡检系统,客户第一句话就是:“任务在后台下发,飞手在平板收到,飞机飞完自动上传 4K 红外图,缺陷能在网页上圈出来,后台最好能直接看到无人机起降平台的状态。”面对这种需求,很多后端第一反应是用若依框架快速搭权限和菜单,但真正动手才发现:若依能给你用户、角色、菜单、定时任务、代码生成器,却不会告诉你巡检任务的状态机怎么设计,更不会告诉你航线上传、双数据源、平台对接这些坑怎么填。这套基于若依框架的无人机智慧巡检后端设计源码,做的就是这件事。它面向的读者不是架构师,而是手里有实际巡检业务、想用 Java 生态快速交付的 Java 后端工程师,以及做智能硬件接入的技术负责人。
2. 若依框架下的项目落地:从环境准备到跑通第一个巡检业务模块
2.1 为什么选若依:权限模型、代码生成器和定时任务正好卡在巡检场景痛点
我先说结论:如果只是做一个内部信息管理系统,那用不用若依无所谓;但无人机智慧巡检后端天然带着角色多、任务重、状态多的特点,若依的四个能力几乎是一对一匹配的。第一是权限模型,巡检系统中管理员要看全局,飞手只能看自己的任务,审核员只能看已完成的报告,这种数据权限若依已经帮你实现到注解级别。第二是代码生成器,设备表、任务表、报告表都是标准 CRUD,用生成器先搭出骨架,比纯手写快一个下午。第三是定时任务,航线预巡检、无人机电量低提醒、巡检报告日度拉取,都需要任务调度。第四是操作日志和在线用户管理,客户后期查问题的时候特别好用。从零搭一套这些功能,按正常开发效率至少要加两个星期,而用若依,你的主要精力可以放在业务逻辑上。
而且若依本身是前后端分离项目,后端是 Spring Boot,前端是 Vue,正好与无人机巡检中“后台配置、移动端接收”的多端场景兼容。有人说若依太重,但巡检后端涉及的菜单、角色、部门、字典本身就是业务的一部分,框架的骨架能省去重复的分页查询和权限判断。若依的代码生成器不是简单生成 CRUD,它生成的 domain、mapper、service、controller 以及前端 vue 文件,能让你把注意力集中在巡检业务上。所以我的建议是:接这一类无人机项目,只要客户的操作端是网页,就值得优先评估若依。
| 需求点 | 若依框架自带 | 从零搭建的成本 |
|---|---|---|
| 用户角色权限 | sys_user/sys_role 完整 RBAC | 1-2 周 |
| 菜单与按钮权限 | 动态菜单 + 权限注解 | 1 周 |
| 定时任务 | Quartz 封装 | 3-5 天 |
| 操作日志 | 切面注入 | 2-3 天 |
| 代码生成器 | 表结构生成前后端代码 | 难以估量 |
2.2 从下载到启动:源码包目录结构与后端启动步骤
拿到这套源码后,第一件事不是急着改代码,而是把包结构和跑通方法的顺序捋清楚。虽然不同版本目录有差异,但一般遵循若依标准结构:ruoyi-admin是启动模块,ruoyi-framework放框架配置(登录校验、异常处理),ruoyi-system放系统管理相关的业务,ruoyi-common放工具类,然后还有专门给巡检业务建的ruoyi-biz模块。如果你看到的源码包里有整合 Quarts 的场景,那也没必要慌,定时任务调度通常就放在这一层里。
启动步骤我一般按下面三步走:
# 第一步:创建数据库,导入若依初始化脚本和巡检业务表脚本 # 注意:源码包里的 sql 目录下一般有 ruoyi 基础库和 biz 业务库两个脚本,顺序不能反 mysql -uroot -p -e "CREATE DATABASE ruoyi DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;" mysql -uroot -p ruoyi < sql/ruoyi_init.sql mysql -uroot -p ruoyi < sql/biz_inspection.sql # 第二步:修改数据库连接、Redis 连接等配置 # 路径通常在 ruoyi-admin/src/main/resources/application-druid.yml vim ruoyi-admin/src/main/resources/application-druid.yml # 第三步:启动后端,默认端口 8080 cd ruoyi-admin && mvn spring-boot:run第一条命令的DEFAULT CHARACTER SET utf8mb4一定要带上。无人机巡检记录里可能有 emoji 备注,不带的话,最后写入图片标注或设备别名的时候,会出现Incorrect string value报错。第三条命令的mvn spring-boot:run是开发环境最直观的做法,如果你已经装好了 maven 和 JDK 17 的话,这一步基本不需要改代码就能看到 Tomcat started 日志。参数方面,-uroot -p代表用户名和密码,实际使用时把密码补上;Redis 配置在另一个 yml 里,若依的前后端分离必须依赖 Redis 做会话缓存,这一点能坑掉很多人。
提示:数据库脚本导入顺序错了,后边巡检菜单会 404。先导入若依基础脚本,再导入巡检业务脚本,因为业务菜单依赖
sys_menu表的父级记录。
2.3 用代码生成器生成无人机设备模块,再动手补业务字段
如果只是把后台跑起来,那还没有体现这份源码的价值。真正让你进入角色的是用若依的代码生成器生成无人机设备表模块。在系统管理→代码生成里,导入一张biz_drone表,然后生成代码。生成器会让你配置生成路径、模块包名、表前缀等。这里给一个典型的表脚本:
-- 无人机设备表(若依代码生成建议直接用单表,避免一次性生成主表加子表) DROP TABLE IF EXISTS `biz_drone`; CREATE TABLE `biz_drone` ( `drone_id` bigint NOT NULL AUTO_INCREMENT COMMENT '无人机ID', `drone_sn` varchar(64) NOT NULL COMMENT '无人机SN', `drone_name` varchar(128) DEFAULT NULL COMMENT '设备名称', `model` varchar(64) DEFAULT NULL COMMENT '型号', `dock_id` varchar(32) DEFAULT NULL COMMENT '起降平台编号', `battery_level` int DEFAULT NULL COMMENT '电量百分比', `last_flight_time` datetime DEFAULT NULL COMMENT '最后起飞时间', `status` char(1) DEFAULT '0' COMMENT '状态:0待机 1飞行中 2返航 3离线', `remark` varchar(500) DEFAULT NULL COMMENT '备注', PRIMARY KEY (`drone_id`) ) ENGINE=InnoDB COMMENT='无人机设备表';导入生成器后,把包名改成类似com.ruoyi.biz.drone,生成后代码会落在临时目录,然后复制到ruoyi-biz模块。要注意的是,生成器默认会把drone_id当成普通字段,不会自动设为雪花 ID 或自增,你需要确认 Controller 里@Service注入是否正确。另外,last_flight_time这种时间字段在生成的前端 form 表单里默认是 text 类型,你需要手动改为 datetime 控件,否则保存时格式会报 500。
生成完后,我一般会在 Service 接口里额外加一个updateDroneStatus(String droneSn, String status)方法,这是因为无人机飞控上传的状态数据是高频的,实时心跳不适合走到 Controller 再进 Service,而是应该通过消息队列直接消费。这一改造就是这套源码里最值得看的部分,它会直接影响下一章要讲的巡检任务调度。
3. 巡检任务调度与航线下发:TDengine 双数据源和状态机实战
3.1 巡检任务的状态机设计:从待执行到已完成,状态改变必须留痕
无人机巡检任务和普通审批任务最大的区别在于它存在“飞控回调”这个外部触达点。任务不是人工一步步点的,而是后端根据计划时间自动下发,再接收无人机起降平台的状态回传。所以任务表的状态必须是一个明确的状态机,否则后续对账、重试、数据统计都会乱。
在这套源码里,巡检任务的状态被定义为四个值,核心流转关系如下:
| 状态值 | 状态名称 | 触发条件 | 下游动作 |
|---|---|---|---|
| 0 | 待执行 | 管理员创建任务或系统预排 | 定时扫描到点任务并下发 |
| 1 | 进行中 | 起降平台确认起飞 | 实时记录轨迹 |
| 2 | 已完成 | 飞控回传完成信号 | 生成巡检报告入口 |
| 3 | 失败 | 超时或设备异常 | 告警并允许重试 |
有些人喜欢把状态直接写成 String 然后到处判断status.equals("2"),第一版确实快,但后面加一个“已取消”状态时你会想骂人。我在这套源码里看到的做法是先把状态定义在字典里,然后用一个常量类封装。这样定时任务调度、人工改单、飞控回调都走同一个changeStatus方法,每次修改都会写入状态变更日志表,客户查起历史来一目了然。任务状态变更日志表至少要有task_id、old_status、new_status、change_by、change_time五个字段,这不算过度设计,巡检业务出问题时靠它定位责任。
3.2 定时任务扫描到点任务:cron 表达式与运行实例的取舍
巡检任务下发是典型的定时任务场景。管理员可能一次性批量导入 50 条任务,这些任务计划开始时间分散在未来 24 小时内,如果都靠人工触发,飞手得守着后台刷新。源码里的做法是写一个后台扫描器,每 5 分钟扫一次未执行任务,到点就下发。
/** * 自动巡检任务扫描 * 每 5 分钟扫描一次,找出计划开始时间在 now 之前且状态为 0 的任务 */ @Scheduled(cron = "0 0/5 * * * ?") public void autoDispatch() { List<InspectionTask> tasks = taskMapper.selectPendingTask(LocalDateTime.now()); if (CollectionUtils.isEmpty(tasks)) { return; } for (InspectionTask task : tasks) { try { task.setStatus(TaskStatus.IN_PROGRESS); taskMapper.updateStatus(task); // 组装航线并推送给无人驾驶起降平台(Dock) dockClient.dispatch(task.getDockId(), task.getTaskId()); log.info("[autoDispatch] 任务 {} 下发给起降平台 {}", task.getTaskNo(), task.getDockId()); } catch (Exception e) { log.error("[autoDispatch] 任务 {} 下发异常", task.getTaskNo(), e); task.setStatus(TaskStatus.FAILED); taskMapper.updateStatus(task); } } }selectPendingTask对应的 Mapper SQL 条件是status = '0' and plan_start <= now()。这里注意,定时任务方法内的事务边界不要覆盖整个循环,否则一个任务下发失败会把整个批次回滚。我的习惯是每个任务单独提交一次,失败只标记当次任务。cron表达式0 0/5 * * * ?表示从 0 分开始每 5 分钟触发一次,如果你希望精确到 1 分钟,改成0 * * * * ?。
另外,如果你部署了多个应用节点,同一个任务会被多个节点同时扫描到,这就是重复下发。常见做法是给task_id加一个唯一索引配合UPDATE ... WHERE status='0'的乐观更新,或者直接用 Redis 分布式锁。在若依框架里,我一般选择后者,因为 Redis 本来就是必需的,开箱即用。加锁的细节在第 4 章的避坑清单里会专门讲。
3.3 双数据源接入:业务表放 MySQL,飞行轨迹放 TDengine
无人机起飞后,飞控会以每秒至少一次的频率上送高度、经纬度、速度、电池电压,这些数据如果全部塞进 MySQL,巡检任务一多,单表能轻松到几百万行,查询轨迹回放时明显变慢。所以源码里把时序数据放到 TDengine,业务数据留在 MySQL。要在一个若依项目里同时使用两个数据源,常见做法是引入dynamic-datasource动态数据源插件,然后以 MySQL 为主库,TDengine 为从库。
spring: datasource: dynamic: primary: mysql strict: true datasource: mysql: url: jdbc:mysql://localhost:3306/ruoyi?useUnicode=true&characterEncoding=utf8 driver-class-name: com.mysql.cj.jdbc.Driver username: root password: root tdsql: url: jdbc:taos://localhost:6030/patrol_db driver-class-name: com.taosdata.jdbc.TSDBDriver username: root password: taosdata tomcat: max-active: 10然后是轨迹写入 Mapper 的写法:
@DS("tdsql") @Mapper public interface TrackPointMapper { @Insert("insert into track_point (ts, task_id, lng, lat, alt, speed) values (#{ts}, #{taskId}, #{lng}, #{lat}, #{alt}, #{speed})") int saveTrackPoint(TrackPoint point); }@DS注解可以加在 Mapper 类或方法上,加在类上时整个 Mapper 的所有操作都走 TDengine。primary: mysql表示默认数据源还是 MySQL,所以只有标注了@DS("tdsql")的代码才会切换过去,不会影响若依自身的权限、日志这些表。这里要注意,TDengine 的 JDBC URL 端口是 6030,不是 MySQL 的 3306。如果你的 TDengine 是集群,需要在 taos.cfg 里配置多个 EP 列表,而不是在 URL 里写逗号。
把飞行轨迹写入 TDengine 和更新任务状态放在同一个事务方法里时,你可能会发现失败后无法回滚。这是动态数据源切换的天然缺陷,我在下一章会专门展开,它是我在巡检项目中踩得最深的一个坑。
4. 若依 + 无人机后端避坑清单:权限、双数据源与定时任务
4.1 菜单生成后 404:代码生成器只给了代码,没给路由
现象:用若依代码生成器生成设备管理模块,重启后端,登录系统后在菜单列表里找不到,或者手动配置菜单后点击一直白屏 / 404。
原因:若依的前端路由是动态生成的,前端会请求后端返回的菜单列表,然后按组件路径映射到页面。如果菜单配置中组件路径写错,或者没有新增菜单权限,前端就不会加载对应路由。很多新手只把代码复制进ruoyi-biz,忘了在sys_menu表里插入一条记录。
解决:手动插入菜单记录,关键字段如下:
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon) VALUES ('无人机设备管理', 0, 1, 'drone', 'biz/drone/index', 1, 0, 'C', '0', '0', 'biz:drone:list', 'device');component必须是前端源码里实际存在的views/biz/drone/index.vue,否则页面会 404。perms必须与后端@PreAuthorize("@ss.hasPermi('biz:drone:list')")保持一致。如果你拿到的源码包自己没有用这个包名,把biz/drone换成你实际的控制器路由即可。
4.2 TDengine 写入数据无法回滚:事务边界要拆开
现象:一个方法里先向 MySQL 更新任务状态,再往 TDengine 插入轨迹,MySQL 更新失败后抛出异常,但 TDengine 已经写入的轨迹还留在表里,导致任务状态与轨迹数据不对应。
原因:动态数据源@DS切换后,事务管理器默认还是 MySQL 的,TDengine 是非事务型数据库,不可能参与两阶段提交。换句话说,两个数据源之间没有跨库事务,失败后自然不能一起回滚。
解决:把两件事拆分。一种做法是先记录轨迹数据到本地消息表(MySQL),再异步消费并写入 TDengine;另一种做法是把 TDengine 写入放在事务提交之后,失败时通过重试或补偿清理。我在这套源码里使用的是第二种,具体来说就是在方法上加@Transactional(propagation = Propagation.NOT_SUPPORTED)的独立轨迹上报入口,让它脱离主事务。注意这样做的代价是轨迹上报可能重复,需要在写入语句里用task_id加ts做唯一约束。
4.3 定时任务执行了两次:多实例部署没有做互斥
现象:两台应用服务器同时启动,第二天发现同一个巡检任务被下发两次,无人机收到两条重复航线指令。
原因:Spring@Scheduled是进程内调度,多个实例各跑各的;Quartz 用 JDBC 集群模式时,如果触发器配置不当也可能重复。在分布式部署下,定时任务必须做互斥。
解决:在任务扫描前加 Redis 锁。常见代码是:
String lockKey = "lock:autoDispatch:" + task.getTaskId(); // 尝试加锁,超时 3 秒,成功后 30 秒自动释放 if (!redisTemplate.opsForValue().setIfAbsent(lockKey, "1", Duration.ofSeconds(30))) { log.warn("[autoDispatch] 任务 {} 已经有节点在处理,跳过", task.getTaskNo()); continue; } taskMapper.updateStatus(task);锁的过期时间 30 秒要大于任务处理的最大时间,避免任务没处理完锁就过期,另一个节点又进来处理。如果任务里还包含大图上传或航线同步,建议把过期时间调成 60 秒,并且把释放锁的动作放在finally里。
4.4 跨域问题:若依后端默认只允许本机前端
现象:前端挂在服务器上,用 IP 访问页面时登录无法保存 Session,接口全部跨域报错。
原因:若依的跨域配置默认只对localhost和127.0.0.1生效,没有把实际前端使用的 IP 或域名加进去。检查浏览器 Network 面板,通常能看到CORS error。
解决:在application.yml里找到若依的 cors 配置,把allowedOriginPatterns改成实际部署地址。注意不能用*,否则带凭证的请求会被浏览器拒绝。正确写法是把前端地址一一列出,比如:
cors: allowed-origin-patterns: - http://192.168.1.100:80 - https://patrol.example.com如果前端和后端通过 Nginx 同域转发,那只需要在后端配置上允许same-origin即可。每次部署新环境,我会先把 Origin 配置列成一个显式的白名单,避免排查时浪费时间。
5. 一个进阶技巧:用若依数据权限控制巡检任务归属,再验证接口权限
5.1 数据权限注解让飞手只看自己的任务
巡检系统上线后最麻烦的需求不是功能,而是数据边界:飞手 A 不应该看到飞手 B 的任务记录,部门领导可以看整个部门的记录。若依专门提供了@DataScope注解,但很多人在业务表里没有正确设置user_id和dept_id,导致注解没有作用。正确的做法是在查询方法上同时加权限注解和数据权限注解:
@GetMapping("/list") @PreAuthorize("@ss.hasPermi('biz:task:list')") @DataScope(deptAlias = "ts") public TableDataInfo list(TaskQuery query) { startPage(); List<InspectionTask> list = taskService.selectTaskList(query); return getDataTable(list); }这里的deptAlias必须和 SQL 里的表别名一致。并且 Mapper XML 中需要在查询 SQL 末尾追加${params.dataScope},这样若依的拦截器才会把当前用户的部门和数据范围条件拼接进去。例如:
<select id="selectTaskList" parameterType="TaskQuery" resultType="InspectionTask"> select ts.task_id, ts.task_no, ts.dock_id, ts.drone_sn, ts.status from biz_inspection_task ts <where> <if test="taskNo != null">AND ts.task_no like concat('%', #{taskNo}, '%')</if> </where> ${params.dataScope} </select>${params.dataScope}是若依数据权限拦截器生成的 SQL 片段,比如AND ts.dept_id in (103)。要保证这条条件能正常拼接,biz_inspection_task表里必须有dept_id字段,并且在插入任务时把创建人的deptId一起写入。这是很多人忽略的地方,只给表加了create_by,没加dept_id,结果部门领导看不到下属任务。
5.2 用 curl 验证权限控制是否生效
验证方式不能只靠前端,我一般用后端接口来测试。先拿管理员登录拿 token,再拿一个飞手账号登录,两次访问同一个任务列表接口,对比返回数据的条数差异:
curl -X GET "http://your-server:8080/biz/task/list?pageNum=1&pageSize=10" \ -H "Authorization: Bearer <admin-token>" | head -c 500curl -X GET "http://your-server:8080/biz/task/list?pageNum=1&pageSize=10" \ -H "Authorization: Bearer <pilot-token>" | head -c 500如果飞手账号的响应里只有自己被创建的task_id,说明数据权限生效;如果它列出了所有部门任务,说明dept_id没有正确写入。若依的 token 前缀是Bearer,中间有一个空格,不要把Bearer和 token 挤在一起。
自从有一次把一串任务数据在客户现场演示时不小心全量拉出来了,从那以后我每次在若依里加巡检相关接口,都会强制用两个不同角色的账号走一遍 curl 对比,确认数据边界没问题再交给测试。希望帮到你。
本文还有配套的精品资源,点击获取