Compare commits

..

269 Commits
test ... main

Author SHA1 Message Date
孙小云 b6c2149ba5 修改数据 2026-03-14 11:22:11 +08:00
孙小云 be9fed1b06 修改日志逻辑 2026-03-14 11:04:00 +08:00
孙小云 35ac0122ac 修改状态判断 2026-03-14 10:06:19 +08:00
孙小云 30461e134e 添加字段 2026-03-14 09:26:42 +08:00
孙小云 9d2f882d8d 修改SN获取逻辑 2026-03-13 09:42:16 +08:00
孙小云 23a9df7750 xx 2026-03-13 09:38:00 +08:00
孙小云 8dda5e5c6d xx 2026-03-12 18:50:28 +08:00
孙小云 0f9de39fc5 xx 2026-03-10 16:31:12 +08:00
孙小云 24422d8e93 添加一键起飞和返航 2026-03-10 15:56:15 +08:00
孙小云 2bab07ecdd 修改一键起飞 2026-03-10 15:45:57 +08:00
孙小云 525ff0b8eb 添加飞行日志 2026-03-10 15:42:32 +08:00
孙小云 6da33d3d9c 修改一键起飞 2026-03-10 15:08:51 +08:00
孙小云 fb387aaa1e 添加一键起飞功能 2026-03-10 14:56:42 +08:00
孙小云 7d632576ea 添加配置 2026-03-10 14:04:43 +08:00
孙小云 22eddd52a9 添加日志 2026-03-10 14:00:54 +08:00
孙小云 708bc3174b 修改配置 2026-03-10 13:20:56 +08:00
孙小云 ac1210d76e 添加同步日志 2026-03-10 13:19:37 +08:00
孙小云 dfbfee9e49 xx 2026-03-10 11:55:04 +08:00
孙小云 db2d1a70f5 修改环境配置,修改测试环境 2026-03-10 10:47:15 +08:00
gyb 946e80288d Merge remote-tracking branch 'origin/main' 2026-03-04 16:40:43 +08:00
gyb 17b6cadddd feat:增加无人机类型负载属性 2026-03-04 16:40:27 +08:00
孙小云 c5b490be17 修改 航线起飞 悬停 继续任务指令 2026-03-04 15:04:21 +08:00
孙小云 8f86e92fcc 天气缓存 2026-03-03 14:02:02 +08:00
孙小云 03861e366d 修改天气接口 2026-03-03 13:16:31 +08:00
孙小云 4a9f55a1b1 添加飞控指令 2026-03-02 16:07:11 +08:00
孙小云 67f465b364 处理机场飞行控制数据 2026-02-28 15:45:59 +08:00
孙小云 a4950f3d0e 处理机场飞行控制数据 2026-02-28 15:38:16 +08:00
孙小云 69bb49c869 处理机场飞行控制数据 2026-02-28 14:53:07 +08:00
孙小云 47b6d88e2c 处理机场飞行控制数据 2026-02-28 14:52:42 +08:00
孙小云 2348168502 处理机场飞行控制数据 2026-02-28 10:05:38 +08:00
孙小云 3f15e9f3ed 处理机场飞行控制数据 2026-02-28 09:43:02 +08:00
孙小云 eb4651f682 处理机场飞行控制数据 2026-02-28 09:42:28 +08:00
孙小云 f20216fc29 处理机场飞行控制数据 2026-02-28 09:29:50 +08:00
孙小云 6bda88ab0c 处理机场飞行控制数据 2026-02-28 09:25:16 +08:00
孙小云 f5663d7552 处理机场飞行控制数据 2026-02-28 09:18:37 +08:00
孙小云 ae7b74a3f5 处理机场飞行控制数据 2026-02-28 09:18:00 +08:00
孙小云 30f3ec344d 处理机场飞行控制数据 2026-02-27 15:30:09 +08:00
孙小云 9f6b5f21bb 处理机场飞行控制数据 2026-02-27 14:15:31 +08:00
孙小云 b7fee49c77 Merge branch 'main' of http://th.local.t-aaron.com:13000/THENG/a-tuoheng-device 2026-02-27 08:58:41 +08:00
孙小云 b509f5e997 添加日志 2026-02-27 08:58:36 +08:00
gyb 95b91299c9 Merge remote-tracking branch 'origin/main' 2026-02-26 17:43:22 +08:00
gyb a45dbfa9d2 feat:无人机类型增加类型唯一值 2026-02-26 17:43:08 +08:00
孙小云 b4809cf94d 修复依赖注入的问题 2026-02-26 13:37:41 +08:00
孙小云 f62d2798f6 处理机场飞行控制数据 2026-02-26 09:58:53 +08:00
孙小云 cae0018b88 处理机场飞行控制数据 2026-02-26 09:46:45 +08:00
孙小云 5bd7ba0d8f 处理机场飞行控制数据 2026-02-26 09:44:25 +08:00
孙小云 5d4403f55c 修改日志 2026-02-26 09:37:55 +08:00
孙小云 59e83358f2 添加自检消息日志 2026-02-26 09:14:56 +08:00
孙小云 9df90b97c4 xx 2026-02-26 09:04:43 +08:00
孙小云 5985f4fa1c 添加日志 2026-02-26 08:41:14 +08:00
孙小云 2d5076351b 修改自检存放逻辑 2026-02-25 13:17:42 +08:00
孙小云 5cd7f39d6e websocket日志 2026-02-25 13:06:56 +08:00
孙小云 c4a32f3acb 测试“ 2026-02-25 08:55:38 +08:00
孙小云 37e5837223 添加指令 2026-02-24 11:08:58 +08:00
孙小云 9b9febab9c Merge branch 'main' of http://th.local.t-aaron.com:13000/THENG/a-tuoheng-device 2026-02-24 10:35:08 +08:00
孙小云 75b7b1ba09 添加一键起飞和返航接口 2026-02-24 10:35:02 +08:00
gyb 1a90e9852f fit:修改无人机类型表和接口,增加条件查询;增加拓恒云003-FM系列 2026-02-13 13:29:37 +08:00
孙小云 a367415167 设置升降架 X轴 Y轴的获取逻辑 2026-02-11 17:07:24 +08:00
孙小云 81e6199812 修改无人机的任务判断 2026-02-11 16:42:46 +08:00
孙小云 a5e51849d9 修改无人机的状态判断 2026-02-11 16:37:27 +08:00
孙小云 5e9111e879 修改无人机的状态判断 2026-02-11 16:28:03 +08:00
孙小云 a0eb7202ff 修改 TuohengBufferDeviceImpl 中机场和无人机的状态判断 2026-02-11 16:13:40 +08:00
孙小云 15fbad2334 开仓关仓状态同步 2026-02-11 15:34:12 +08:00
孙小云 ab6318aa9c 添加开关仓指令 2026-02-11 14:49:48 +08:00
孙小云 d6391ee295 舱内状态 2026-02-11 14:24:40 +08:00
孙小云 3b8ceac025 开仓关仓状态同步 2026-02-11 14:06:42 +08:00
孙小云 d50d27e749 开光仓状态 2026-02-11 14:00:28 +08:00
孙小云 aba19deb5b xx 2026-02-11 13:33:47 +08:00
孙小云 7530b9b119 舱内状态同步 2026-02-11 13:22:47 +08:00
孙小云 5aeff3d815 xx 2026-02-11 11:17:50 +08:00
孙小云 dd0a0b1bb4 添加日志 2026-02-11 11:08:33 +08:00
孙小云 f3b6873293 修改超时时间 2026-02-11 10:56:08 +08:00
孙小云 dc671f31ba xx 2026-02-11 10:54:06 +08:00
孙小云 ab53e533c6 修改日志打印 2026-02-11 10:47:58 +08:00
孙小云 880f98e8e1 处理OSD未知字段 2026-02-11 10:37:36 +08:00
孙小云 1507e758cd 修改回调机制 2026-02-11 10:24:15 +08:00
孙小云 8f85fc7a1e 先注册再发送命令 2026-02-11 10:12:14 +08:00
孙小云 96561c749c xx 2026-02-11 09:51:33 +08:00
孙小云 f521b2091d xx 2026-02-11 09:46:41 +08:00
孙小云 8ab18a5860 xx 2026-02-11 09:42:07 +08:00
孙小云 572c13a93a xxx 2026-02-11 09:26:00 +08:00
孙小云 d075d913fb xx 2026-02-11 09:16:35 +08:00
孙小云 42e6643b52 开关机 2026-02-11 09:03:44 +08:00
孙小云 02a6b3ecf8 添加开机功能 2026-02-10 17:32:36 +08:00
孙小云 b9e9b99b3f xx 2026-02-10 17:17:04 +08:00
孙小云 87cdb465d9 xx 2026-02-10 17:14:39 +08:00
孙小云 6eb29ba1f4 xx 2026-02-10 16:33:41 +08:00
孙小云 c35842fdeb 添加confirm 2026-02-10 16:24:52 +08:00
孙小云 350785234c 添加解析方法 2026-02-10 16:16:53 +08:00
孙小云 9ace8e92f7 xx 2026-02-10 16:03:37 +08:00
孙小云 2de46a69c3 xx 2026-02-10 15:29:53 +08:00
孙小云 20f11d581b 添加拓恒机场和无人机状态 2026-02-10 15:07:15 +08:00
孙小云 7a7af0a496 忽略未知字段 2026-02-10 13:54:39 +08:00
孙小云 43e9e9862f xx 2026-02-10 13:39:49 +08:00
孙小云 f873262c35 忽略未知字段 2026-02-10 13:36:05 +08:00
孙小云 d49de6e37a xx 2026-02-10 13:28:15 +08:00
孙小云 8951696146 xx 2026-02-10 13:19:02 +08:00
孙小云 2a8b5b53ca xx 2026-02-10 12:46:05 +08:00
孙小云 3280110a11 xx 2026-02-10 12:42:31 +08:00
孙小云 f0ebbf5732 修改拓恒数据结构 2026-02-10 12:26:52 +08:00
孙小云 820d087861 AirportOsdData.java 2026-02-10 11:28:59 +08:00
孙小云 d10c9a9cb7 修复EventsData 2026-02-10 11:17:35 +08:00
孙小云 bc3b043b45 xx 2026-02-10 11:14:19 +08:00
孙小云 b3bcaa89d1 拓恒接入 2026-02-10 10:54:54 +08:00
孙小云 81242c6ef5 Merge branch 'main' of http://th.local.t-aaron.com:13000/THENG/a-tuoheng-device 2026-02-10 10:51:48 +08:00
孙小云 b97ba6c332 添加拓恒接入 2026-02-10 10:51:42 +08:00
gyb b8c9800942 fit:设备分类接口增加,分类分组-重新构建 2026-02-09 14:00:20 +08:00
gyb 73f089b673 fit:设备分类接口增加,分类分组-新老写法兼容 2026-02-09 13:49:38 +08:00
gyb 87d7de414e fit:设备分类接口增加,分类分组 2026-02-09 13:44:02 +08:00
孙小云 ae6b8bcc93 优化脚本 2026-02-09 08:58:24 +08:00
孙小云 422930b264 xx 2026-02-07 09:17:04 +08:00
孙小云 b242852434 修改获取逻辑 2026-02-06 16:42:08 +08:00
孙小云 9fba4a59e6 修改电池逻辑 2026-02-06 16:16:43 +08:00
孙小云 2dca7d780c 添加电池数据 2026-02-06 15:41:40 +08:00
孙小云 5eafcff343 环境温度和湿度也从IOT读取 2026-02-06 14:38:49 +08:00
孙小云 e5a642b4d5 空调模式 2026-02-06 14:24:39 +08:00
孙小云 b5ccef48d0 设置舱门状态 2026-02-06 14:15:24 +08:00
孙小云 fca4eff00d 架次和运行时长 2026-02-06 14:12:34 +08:00
孙小云 18832acf17 修改备降点经纬度的获取 2026-02-06 13:56:09 +08:00
孙小云 97c671f2ed 添加拓恒经纬度数据 2026-02-06 13:48:13 +08:00
孙小云 1caafd787e 添加IOT设置属性的接口 2026-02-06 13:19:08 +08:00
孙小云 d73d664743 添加固件版本 2026-02-06 11:46:36 +08:00
孙小云 8328978768 xx 2026-02-06 11:27:52 +08:00
孙小云 f54c4e784e xx 2026-02-06 11:22:22 +08:00
孙小云 b7e7899958 修改dock status 2026-02-06 11:16:20 +08:00
孙小云 384dac68a3 修改无人机状态 2026-02-06 11:09:23 +08:00
孙小云 0b6724d31d 修改无人机状态判断 2026-02-06 11:02:45 +08:00
孙小云 3caead4779 修改无人机状态 2026-02-06 10:55:23 +08:00
孙小云 e0b778333c xx 2026-02-06 10:30:14 +08:00
孙小云 2035a03135 aircraftManufacturer 赋值 2026-02-06 10:23:28 +08:00
孙小云 841f53cb4e dockStatus字段逻辑修正 2026-02-06 10:21:34 +08:00
孙小云 582a3d0e0c 设置机场离线逻辑 2026-02-06 09:44:30 +08:00
孙小云 b76d0ef0b4 添加 dockManufacturer 2026-02-06 09:20:58 +08:00
孙小云 82b63271b7 添加缓存 2026-02-05 14:37:07 +08:00
孙小云 4704cbb94b xx 2026-02-04 18:55:35 +08:00
孙小云 1160635703 xx 2026-02-04 18:37:57 +08:00
孙小云 2173a5ef83 xx 2026-02-04 17:28:01 +08:00
孙小云 c79c2d4d5b xx 2026-02-04 17:17:53 +08:00
孙小云 ff22ce08ca xx 2026-02-04 16:52:21 +08:00
孙小云 bad9e733bc 代码重构 2026-02-04 16:36:18 +08:00
孙小云 dcc4835180 无人机兼容 2026-02-04 16:20:50 +08:00
孙小云 392add0212 xx 2026-02-04 15:38:21 +08:00
孙小云 86b7ffe93d xx 2026-02-04 15:02:38 +08:00
孙小云 a6f6231562 修改同步逻辑 2026-02-04 14:45:37 +08:00
孙小云 dad7be582e 拓恒接入 2026-02-04 14:36:52 +08:00
孙小云 8f0d69b42f 修改IThingsBoardDomain.java 2026-02-04 13:16:34 +08:00
gyb 2521bca9ae fit:调整类名及类路径 2026-02-04 11:17:21 +08:00
孙小云 98d4a08d9d xx 2026-02-04 09:54:59 +08:00
孙小云 8d73fe4303 xx 2026-02-03 15:21:12 +08:00
孙小云 f330e739a9 xx 2026-02-03 09:46:09 +08:00
孙小云 1ffd849ebc xx 2026-02-02 11:03:21 +08:00
孙小云 ee02045c65 xx 2026-02-02 09:53:46 +08:00
孙小云 b3b3b41f02 xx 2026-01-31 13:41:00 +08:00
孙小云 9d730c217d xx 2026-01-31 13:35:07 +08:00
孙小云 a2476df5bf xx 2026-01-31 13:17:42 +08:00
孙小云 5797fa6c76 xx 2026-01-31 11:51:16 +08:00
孙小云 ff3fbce1f3 修改天气的缓存方式 2026-01-31 11:02:39 +08:00
孙小云 178829ee35 添加日志 2026-01-31 10:47:54 +08:00
孙小云 fcdac8ba40 添加天气日志 2026-01-31 10:39:37 +08:00
孙小云 940b9999b9 xxx 2026-01-31 10:31:44 +08:00
孙小云 030baffab2 xx 2026-01-30 18:16:31 +08:00
孙小云 863879c031 xx 2026-01-30 18:15:01 +08:00
孙小云 86a1ca4a1d feat: 优化DjiService缓存清除逻辑
- 无人机和机场state消息触发时统一清除机场和所有关联无人机的ThingsBoard缓存
- 添加WebSocket广播机场ID功能
- 优化缓存失效策略,确保数据实时性
2026-01-30 17:40:09 +08:00
孙小云 73e41bdb68 xx 2026-01-30 17:23:12 +08:00
孙小云 dc9fe61784 xx 2026-01-30 17:19:25 +08:00
孙小云 dc5c2e1396 修改缓存 2026-01-30 17:13:12 +08:00
孙小云 3f53fe82ec 添加缓存 2026-01-30 16:34:58 +08:00
孙小云 557cd153c6 添加天气缓存 2026-01-30 16:16:02 +08:00
孙小云 f228313312 xx 2026-01-30 16:06:29 +08:00
孙小云 2b5d02691f xx 2026-01-30 15:44:04 +08:00
孙小云 6a94e799d6 添加排序 2026-01-30 15:34:59 +08:00
孙小云 4e81906fc0 xx 2026-01-30 15:29:11 +08:00
孙小云 d469656148 xx 2026-01-30 15:03:55 +08:00
孙小云 50bd48b0e1 xx 2026-01-30 14:52:28 +08:00
孙小云 80f22702b1 xx 2026-01-30 14:51:35 +08:00
孙小云 e3f58244be xx 2026-01-30 14:48:59 +08:00
孙小云 46b86e9c57 xx 2026-01-30 14:44:10 +08:00
孙小云 21f7f26aa0 x 2026-01-30 14:42:33 +08:00
孙小云 f736031e29 xx 2026-01-30 14:35:27 +08:00
孙小云 e8d7996c0f xx 2026-01-30 14:31:45 +08:00
孙小云 0c4c16bb31 xx 2026-01-30 14:31:14 +08:00
孙小云 b9778447ce xx 2026-01-30 14:29:53 +08:00
孙小云 c911ef5dc9 修改配置 2026-01-30 14:09:55 +08:00
孙小云 78c4d5268e 获取环境信息 2026-01-30 14:06:38 +08:00
gyb e5f5dc3727 fit:修复bug,触发构建 2026-01-30 11:54:10 +08:00
gyb ed9425b415 Merge remote-tracking branch 'origin/main' 2026-01-30 11:50:58 +08:00
gyb aa24675bcc fit:修复bug,触发构建 2026-01-30 11:50:54 +08:00
孙小云 7e1aca3cb8 Merge branch 'main' of http://th.local.t-aaron.com:13000/THENG/a-tuoheng-device 2026-01-30 11:37:38 +08:00
孙小云 fa0799bf66 添加充放电状态 2026-01-30 11:37:13 +08:00
gyb 4901e52b85 Merge remote-tracking branch 'origin/main'
# Conflicts:
#	src/main/java/com/ruoyi/device/controller/convert/AirTypeGeneralEnumVOConvert.java
2026-01-30 11:35:48 +08:00
gyb 3f8845de5f fit:增加注释 2026-01-30 11:35:28 +08:00
孙小云 e8fe5fb703 添加充放电状态 2026-01-30 11:33:03 +08:00
孙小云 4319960f29 Merge branch 'main' of http://th.local.t-aaron.com:13000/THENG/a-tuoheng-device 2026-01-30 11:24:41 +08:00
孙小云 3115110c4a 添加充放电状态 2026-01-30 11:24:11 +08:00
高大 d5a6e05d23 fit:修改触发构建 2026-01-30 11:23:15 +08:00
高大 8ae4123274 fit:修改触发构建 2026-01-30 09:05:49 +08:00
高大 b0da268f0b fit:航线管理,增加批量移动分组的功能 2026-01-29 19:52:30 +08:00
高大 e3969b1560 fit:修复flyway导致的启动报错 2026-01-29 17:24:30 +08:00
高大 2ac899c9f7 fit:增加无人机厂商分组接口 2026-01-29 17:12:27 +08:00
高大 eac151b384 fit:增加无人机厂商分组接口 2026-01-29 17:03:27 +08:00
高大 48b87722d6 fit:增加无人机厂商分组接口 2026-01-29 16:58:20 +08:00
孙小云 d3db54776d xx 2026-01-29 11:55:55 +08:00
孙小云 ff75d4158c xx 2026-01-29 11:39:45 +08:00
孙小云 2eecee1f60 xx 2026-01-29 11:38:55 +08:00
孙小云 4c829335c5 xx 2026-01-29 11:37:52 +08:00
孙小云 143fad9306 xx 2026-01-29 11:37:03 +08:00
孙小云 7a9504ebcf 添加充放电状态 2026-01-29 11:35:20 +08:00
孙小云 dc1014af60 添加充放电状态 2026-01-29 10:18:40 +08:00
孙小云 e8a05e3f9a xx 2026-01-29 10:15:23 +08:00
孙小云 46b11f34df xx 2026-01-29 10:14:26 +08:00
孙小云 efa74d3e3e xx 2026-01-29 10:03:02 +08:00
孙小云 296af4f342 xx 2026-01-29 10:00:13 +08:00
孙小云 f733eff2ea xx 2026-01-29 09:42:59 +08:00
孙小云 ed1489092b xx 2026-01-29 09:21:19 +08:00
孙小云 32de6a11a8 xx 2026-01-28 17:45:07 +08:00
孙小云 a83ea68751 xx 2026-01-28 17:39:44 +08:00
孙小云 770fed6d7b xx 2026-01-28 17:25:12 +08:00
孙小云 3ae347b948 添加充放电状态 2026-01-28 17:14:50 +08:00
孙小云 7bfda81a8e 添加充放电状态 2026-01-28 17:10:15 +08:00
孙小云 ab711bcc73 xx 2026-01-28 16:46:32 +08:00
孙小云 3f3ed27efd xx 2026-01-28 16:42:45 +08:00
孙小云 cec824f8a5 Merge branch 'main' of http://th.local.t-aaron.com:13000/THENG/a-tuoheng-device 2026-01-28 16:39:45 +08:00
孙小云 e0b074dde1 xxx 2026-01-28 16:39:35 +08:00
高大 b2683cd7fe Merge remote-tracking branch 'origin/main' 2026-01-28 16:36:11 +08:00
高大 ac6e682689 fix:增加无人机/遥控器/机场 类型枚举表 2026-01-28 16:35:42 +08:00
孙小云 8c50ade937 添加充放电状态 2026-01-28 16:21:09 +08:00
孙小云 8a418c063d 添加充放电状态 2026-01-28 15:57:01 +08:00
孙小云 a636c9a858 添加充放电状态 2026-01-28 15:52:20 +08:00
孙小云 dfc5033eab x 2026-01-28 15:26:47 +08:00
孙小云 fe6e2c6cf8 xx 2026-01-28 15:16:29 +08:00
孙小云 9f0efa5320 xxx 2026-01-28 15:11:06 +08:00
孙小云 cec370f0dd xx 2026-01-28 15:10:25 +08:00
孙小云 faa7ca1790 xx 2026-01-28 15:03:05 +08:00
孙小云 d4b3d86b3c xx 2026-01-28 14:57:29 +08:00
孙小云 9999fd922b xx 2026-01-28 14:52:30 +08:00
孙小云 035e558ef7 xx 2026-01-28 14:42:25 +08:00
孙小云 0b47cbbf57 xx 2026-01-28 14:29:48 +08:00
孙小云 ad38bce459 xx 2026-01-28 14:26:08 +08:00
孙小云 a7b3fa2ee2 xx 2026-01-28 14:13:45 +08:00
孙小云 bbda3b541d 接入大疆MQTT 2026-01-28 14:05:09 +08:00
孙小云 a6b37bd269 迁移MQTT 2026-01-28 13:57:03 +08:00
孙小云 a205823773 迁移状态机 2026-01-28 11:27:02 +08:00
孙小云 bef017bf63 修改剩余里程的获取方式 2026-01-28 09:55:56 +08:00
孙小云 1a4c0508c0 打印电池信息 2026-01-28 09:48:47 +08:00
孙小云 6990e3fb30 修改rtk的获取 2026-01-28 09:07:50 +08:00
孙小云 bc5e98e48d xx 2026-01-27 18:16:35 +08:00
孙小云 2c18173951 测试失败场景 2026-01-27 18:16:04 +08:00
孙小云 891f9c6721 xx 2026-01-27 18:15:07 +08:00
孙小云 7a3a0e3aca 修复maven错误 2026-01-27 17:51:58 +08:00
孙小云 db503192be 测试失败场景 2026-01-27 17:51:13 +08:00
孙小云 8cc817f132 reademe.md 2026-01-27 17:50:00 +08:00
孙小云 b9d2afa7d7 xx 2026-01-27 17:45:00 +08:00
孙小云 72cc321ab1 xx 2026-01-27 14:40:57 +08:00
孙小云 ab3a1bca28 添加部署测试 2026-01-27 14:19:11 +08:00
孙小云 b04c08bb01 添加默认分组逻辑 2026-01-27 14:01:03 +08:00
孙小云 28bfcc8c6b 添加默认分组逻辑 2026-01-27 14:00:58 +08:00
孙小云 d5a9cf7839 xx 2026-01-27 13:52:04 +08:00
孙小云 4b4dede75a 添加默认分组逻辑 2026-01-27 13:44:19 +08:00
孙小云 c3d7142e3c xx 2026-01-27 13:34:40 +08:00
孙小云 6faf537b25 x 2026-01-27 13:22:33 +08:00
孙小云 54bd25b7dc xx 2026-01-27 13:02:44 +08:00
孙小云 46b0d9614e 添加默认分组逻辑 2026-01-27 11:44:39 +08:00
孙小云 53f085032c xx 2026-01-26 16:59:25 +08:00
孙小云 410f5fb4f0 修改文案 2026-01-26 15:30:17 +08:00
孙小云 e93a9b3f67 修改统计逻辑 2026-01-26 11:08:44 +08:00
孙小云 9a74d198ae group/switch 参数添加支持列表 2026-01-26 09:02:11 +08:00
孙小云 9cef58104b group/switch 参数添加支持列表 2026-01-24 17:23:44 +08:00
孙小云 b9cf1428f9 修改返回错误 2026-01-24 15:23:43 +08:00
孙小云 6bd0dbe4e4 修改代码 2026-01-24 15:10:16 +08:00
265 changed files with 21368 additions and 430 deletions

35
pom.xml
View File

@ -28,6 +28,12 @@
<artifactId>tuoheng-api-device</artifactId> <artifactId>tuoheng-api-device</artifactId>
</dependency> </dependency>
<!-- RuoYi System API -->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-api-system</artifactId>
</dependency>
<!-- RuoYi Common Core --> <!-- RuoYi Common Core -->
<dependency> <dependency>
<groupId>com.ruoyi</groupId> <groupId>com.ruoyi</groupId>
@ -105,6 +111,35 @@
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<!-- Caffeine Cache -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<!-- WebSocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- Spring Integration MQTT -->
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-mqtt</artifactId>
</dependency>
<!-- Eclipse Paho MQTT v5 Client (支持MQTT 5.0) -->
<dependency>
<groupId>org.eclipse.paho</groupId>
<artifactId>org.eclipse.paho.mqttv5.client</artifactId>
<version>1.2.5</version>
</dependency>
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>tuoheng-api-task</artifactId>
</dependency>
</dependencies> </dependencies>
<build> <build>

View File

@ -1 +1,2 @@
ddddddddddddddeeedddddddd
iddddddddddddddddddddddddddddddddddddddddddddddddD堆堆ddddddddddddddddddddeeedddddddd

View File

@ -2,6 +2,7 @@ package com.ruoyi.device;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.EnableScheduling;
import com.ruoyi.common.security.annotation.EnableCustomConfig; import com.ruoyi.common.security.annotation.EnableCustomConfig;
import com.ruoyi.common.security.annotation.EnableRyFeignClients; import com.ruoyi.common.security.annotation.EnableRyFeignClients;
@ -17,8 +18,11 @@ import com.ruoyi.common.security.annotation.EnableRyFeignClients;
@SpringBootApplication @SpringBootApplication
public class TuohengDeviceApplication public class TuohengDeviceApplication
{ {
public static void main(String[] args) public static void main(String[] args)
{ {
SpringApplication.run(TuohengDeviceApplication.class, args); SpringApplication.run(TuohengDeviceApplication.class, args);
System.out.println("(♥◠‿◠)ノ゙ 设备模块启动成功 ლ(´ڡ`ლ)゙ \n" + System.out.println("(♥◠‿◠)ノ゙ 设备模块启动成功 ლ(´ڡ`ლ)゙ \n" +
" .-------. ____ __ \n" + " .-------. ____ __ \n" +

View File

@ -0,0 +1,89 @@
package com.ruoyi.device.config;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
/**
* Device模块缓存配置
* 为Domain层提供分布式缓存支持
*
* @author ruoyi
* @date 2026-01-30
*/
@Configuration
@EnableCaching
public class DeviceCacheConfig {
/**
* 缓存名称常量
*/
public static final String DEVICE_CACHE = "device";
public static final String DOCK_CACHE = "dock";
public static final String AIRCRAFT_CACHE = "aircraft";
public static final String PAYLOAD_CACHE = "payload";
public static final String DOCK_AIRCRAFT_CACHE = "dockAircraft";
public static final String AIRCRAFT_PAYLOAD_CACHE = "aircraftPayload";
public static final String THINGSBOARD_ATTRIBUTES_CACHE = "thingsboardAttributes";
public static final String THINGSBOARD_TELEMETRY_CACHE = "thingsboardTelemetry";
public static final String WEATHER_CACHE = "weather";
/**
* 配置缓存管理器
* 为不同的实体配置不同的过期时间
*/
@Bean
public CacheManager deviceCacheManager(RedisConnectionFactory connectionFactory) {
// 默认缓存配置30分钟过期
RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.disableCachingNullValues();
// 为不同实体配置不同的过期时间
Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>();
// 设备信息30分钟基础数据变化较少
cacheConfigurations.put(DEVICE_CACHE, defaultConfig.entryTtl(Duration.ofMinutes(30)));
// 机场信息30分钟基础数据变化较少
cacheConfigurations.put(DOCK_CACHE, defaultConfig.entryTtl(Duration.ofMinutes(30)));
// 无人机信息30分钟基础数据变化较少
cacheConfigurations.put(AIRCRAFT_CACHE, defaultConfig.entryTtl(Duration.ofMinutes(30)));
// 挂载信息30分钟基础数据变化较少
cacheConfigurations.put(PAYLOAD_CACHE, defaultConfig.entryTtl(Duration.ofMinutes(30)));
// 关联关系15分钟关联关系可能变化
cacheConfigurations.put(DOCK_AIRCRAFT_CACHE, defaultConfig.entryTtl(Duration.ofMinutes(15)));
cacheConfigurations.put(AIRCRAFT_PAYLOAD_CACHE, defaultConfig.entryTtl(Duration.ofMinutes(15)));
// ThingsBoard 设备属性90秒属性数据变化较少
cacheConfigurations.put(THINGSBOARD_ATTRIBUTES_CACHE, defaultConfig.entryTtl(Duration.ofSeconds(90)));
// ThingsBoard 设备遥测15秒遥测数据实时性要求高
cacheConfigurations.put(THINGSBOARD_TELEMETRY_CACHE, defaultConfig.entryTtl(Duration.ofSeconds(15)));
// 天气信息5分钟天气数据变化较慢
cacheConfigurations.put(WEATHER_CACHE, defaultConfig.entryTtl(Duration.ofMinutes(5)));
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(defaultConfig)
.withInitialCacheConfigurations(cacheConfigurations)
.transactionAware()
.build();
}
}

View File

@ -0,0 +1,14 @@
package com.ruoyi.device.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}

View File

@ -5,14 +5,18 @@ import com.ruoyi.common.core.web.controller.BaseController;
import com.ruoyi.common.security.annotation.InnerAuth; import com.ruoyi.common.security.annotation.InnerAuth;
import com.ruoyi.device.api.domain.AircraftDetailVO; import com.ruoyi.device.api.domain.AircraftDetailVO;
import com.ruoyi.device.api.domain.AircraftUpdateRequest; import com.ruoyi.device.api.domain.AircraftUpdateRequest;
import com.ruoyi.device.api.domain.DockAircraftVO;
import com.ruoyi.device.controller.convert.AircraftDetailVOConvert; import com.ruoyi.device.controller.convert.AircraftDetailVOConvert;
import com.ruoyi.device.mapper.DockAircraftMapper;
import com.ruoyi.device.service.api.IAircraftService; import com.ruoyi.device.service.api.IAircraftService;
import com.ruoyi.device.service.api.IBufferDeviceService; import com.ruoyi.device.service.impl.DefaultBufferDeviceImpl;
import com.ruoyi.device.service.dto.AircraftDetailDTO; import com.ruoyi.device.service.dto.AircraftDetailDTO;
import com.ruoyi.device.service.dto.AircraftDTO; import com.ruoyi.device.service.dto.AircraftDTO;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.List;
/** /**
* 无人机Controller * 无人机Controller
* *
@ -27,7 +31,10 @@ public class AircraftController extends BaseController
private IAircraftService aircraftService; private IAircraftService aircraftService;
@Autowired @Autowired
private IBufferDeviceService bufferDeviceService; private DefaultBufferDeviceImpl bufferDeviceService;
@Autowired
private DockAircraftMapper dockAircraftMapper;
/** /**
* 查看无人机详情 * 查看无人机详情
@ -60,4 +67,16 @@ public class AircraftController extends BaseController
aircraftService.updateAircraft(dto); aircraftService.updateAircraft(dto);
return R.ok(); return R.ok();
} }
/**
* 获取所有机场和机场的无人机
*
* @return 机场无人机列表
*/
@GetMapping("/dock-aircraft-list")
public R<List<DockAircraftVO>> getDockAircraftList()
{
List<DockAircraftVO> list = dockAircraftMapper.selectDockAircraftWithDetails();
return R.ok(list);
}
} }

View File

@ -0,0 +1,486 @@
package com.ruoyi.device.controller;
import com.alibaba.fastjson.JSON;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.web.controller.BaseController;
import com.ruoyi.device.api.domain.*;
import com.ruoyi.device.api.enums.DroneCurrentStatusEnum;
import com.ruoyi.device.api.enums.DroneMissionStatusEnum;
import com.ruoyi.device.domain.impl.machine.MachineCommandManager;
import com.ruoyi.device.domain.impl.machine.command.CommandResult;
import com.ruoyi.device.domain.impl.machine.command.CommandType;
import com.ruoyi.device.domain.impl.machine.state.MachineStates;
import com.ruoyi.device.service.FlightService;
import com.ruoyi.task.api.enums.StatusEnum;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
/**
* 无人机飞控Controller
*
* @author ruoyi
* @date 2026-02-04
*/
@Slf4j
@Tag(name = "无人机飞控管理", description = "无人机飞控相关接口")
@RestController
@RequestMapping("/drone")
public class AircraftFlyController extends BaseController
{
@Autowired
private MachineCommandManager machineCommandManager;
@Autowired
private com.ruoyi.device.domain.impl.machine.statemachine.MachineStateManager machineStateManager;
@Autowired
private FlightService flightService;
/**
* 无人机飞控命令
*
* @param request 飞控命令请求
* @return 结果
*/
@Operation(summary = "无人机飞控命令", description = "发送飞控指令")
@PostMapping("/flight-control")
public R<Void> flightControl(@RequestBody DroneFlightControlRequest request)
{
if (request.getCommand() == null || request.getSn() == null) {
return R.fail("飞控命令和机场SN号不能为空");
}
String sn = request.getSn();
log.info("收到飞控命令: sn={}, command={}", sn, request.getCommand());
try {
CommandType commandType;
java.util.Map<String, Object> params = new java.util.HashMap<>();
// 处理消息ID
if (request.getMessageID() != null) {
params.put("messageID", request.getMessageID());
} else {
params.put("messageID", System.currentTimeMillis());
}
// 处理扩展参数
if (request.getEvalue() != null) {
params.put("evalue", request.getEvalue());
}
if (request.getValue() != null) {
params.put("value", request.getValue());
}
if (request.getLightMode() != null) {
params.put("lightMode", request.getLightMode());
}
// 处理航线飞行悬停继续任务所需的参数
if (request.getAirlineFileUrl() != null) {
params.put("airlineFileUrl", request.getAirlineFileUrl());
}
if (request.getFlyBatteryMin() != null) {
params.put("flyBatteryMin", request.getFlyBatteryMin());
}
if (request.getIsMustFly() != null) {
params.put("isMustFly", request.getIsMustFly());
}
if (request.getTaskId() != null) {
params.put("taskId", request.getTaskId());
}
if (request.getZhilin() != null) {
params.put("zhilin", request.getZhilin());
}
switch (request.getCommand()) {
case FORWARD:
commandType = CommandType.FORWARD;
break;
case BACKWARD:
commandType = CommandType.BACKWARD;
break;
case LEFT:
commandType = CommandType.LEFT;
break;
case RIGHT:
commandType = CommandType.RIGHT;
break;
case ROTATE_LEFT:
commandType = CommandType.ROTATE_LEFT;
break;
case ROTATE_RIGHT:
commandType = CommandType.ROTATE_RIGHT;
break;
case UP:
commandType = CommandType.UP;
break;
case DOWN:
commandType = CommandType.DOWN;
break;
case SWITCH_VISIBLE_LIGHT:
commandType = CommandType.SWITCH_VISIBLE_LIGHT;
break;
case GIMBAL_ZOOM:
commandType = CommandType.GIMBAL_ZOOM;
break;
case SWITCH_IR:
commandType = CommandType.SWITCH_IR;
break;
case SWITCH_WIDE_ANGLE:
commandType = CommandType.SWITCH_WIDE_ANGLE;
break;
case GIMBAL_MOVE_RIGHT:
commandType = CommandType.GIMBAL_MOVE_RIGHT;
break;
case GIMBAL_MOVE_LEFT:
commandType = CommandType.GIMBAL_MOVE_LEFT;
break;
case GIMBAL_PITCH_UP:
commandType = CommandType.GIMBAL_PITCH_UP;
break;
case GIMBAL_PITCH_DOWN:
commandType = CommandType.GIMBAL_PITCH_DOWN;
break;
case GIMBAL_RESET:
commandType = CommandType.GIMBAL_RESET;
break;
case AIRLINE_FLIGHT:
commandType = CommandType.AIRLINE_FLIGHT;
break;
case HOVER:
commandType = CommandType.HOVER;
break;
case CONTINUE_TASK:
commandType = CommandType.CONTINUE_TASK;
break;
case EMERGENCY_STOP:
return R.fail("急停命令暂不支持");
default:
return R.fail("不支持的飞控命令");
}
CompletableFuture<CommandResult> future = machineCommandManager.executeCommand(sn, commandType, params);
CommandResult result = future.get();
if (result.isSuccess()) {
log.info("飞控命令执行成功: sn={}, command={}", sn, request.getCommand());
return R.ok();
} else {
log.error("飞控命令执行失败: sn={}, command={}, reason={}", sn, request.getCommand(), result.getErrorMessage());
return R.fail("飞控命令执行失败: " + result.getErrorMessage());
}
} catch (Exception e) {
log.error("飞控命令执行异常: sn={}, command={}", sn, request.getCommand(), e);
return R.fail("飞控命令执行异常: " + e.getMessage());
}
}
/**
* 无人机实时信息展示
*
* @param taskId 任务ID
* @return 实时信息
*/
@Operation(summary = "无人机实时信息展示", description = "根据任务ID获取无人机的实时飞行信息包括速度、高度、姿态角等")
@GetMapping("/realtime-info/{taskId}")
public R<DroneRealtimeInfoVO> getRealtimeInfo(
@Parameter(description = "任务ID", required = true, example = "1")
@PathVariable("taskId") Long taskId)
{
// TODO: 实现获取实时信息逻辑
DroneRealtimeInfoVO vo = new DroneRealtimeInfoVO();
vo.setClimbSpeed(0);
vo.setCruiseSpeed(0);
vo.setDistanceToAirport(0);
vo.setAltitude(0);
vo.setPitch(0);
vo.setYaw(0);
vo.setGimbalPitch(0);
vo.setGimbalYaw(0);
vo.setMissionStatus(DroneMissionStatusEnum.IDLE);
return R.ok(vo);
}
/**
* 无人机当前状态查询
*
* @param dockId 机场ID
* @return 当前状态
*/
@Operation(summary = "无人机当前状态查询", description = "根据机场ID查询无人机的当前飞行状态")
@GetMapping("/current-status/{dockId}")
public R<DroneCurrentStatusVO> getCurrentStatus(
@Parameter(description = "机场ID", required = true, example = "1")
@PathVariable("dockId") Long dockId)
{
// TODO: 实现查询当前状态逻辑
DroneCurrentStatusVO vo = new DroneCurrentStatusVO();
vo.setDockId(dockId);
vo.setCurrentStatus(DroneCurrentStatusEnum.HOVERING);
return R.ok(vo);
}
//从配置文件获取
private final static String airlineFileUrl = "https://minio-dx.t-aaron.com:2443/th-airport/testFile/191ec54c-062c-4828-aab6-cefc901add78.waypoints";
/**
* 无人机一键起飞
*
* @param request 起飞请求对象
* @return 起飞响应
*/
@Operation(summary = "无人机一键起飞", description = "控制指定机场的无人机执行起飞操作")
@PostMapping("/takeoff")
public R<String> takeoff(@RequestBody DroneTakeoffRequest request)
{
// Long taskId = flightService.createClickTakeOffTask(request.getSn(),airlineFileUrl);
log.info("一键起飞,生成一键起飞任务 {} ", JSON.toJSONString(request));
try {
java.util.Map<String, Object> params = new java.util.HashMap<>();
params.put("airlineFileUrl", airlineFileUrl);
params.put("flyBatteryMin", request.getFlyBatteryMin());
params.put("messageID", request.getTaskId());
CompletableFuture<CommandResult> future = machineCommandManager.executeCommand(request.getSn(), CommandType.TAKE_OFF, params);
CommandResult result = future.get();
if (result.isSuccess()) {
log.info("无人机起飞命令发送成功: sn={}", request.getSn());
flightService.updateFlightStatus(request.getTaskId(), StatusEnum.CHECKING);
return R.ok("无人机起飞命令发送成功");
} else {
log.error("无人机起飞命令发送失败: sn={}, reason={}", request.getSn(), result.getErrorMessage());
flightService.updateFlightStatus(request.getTaskId(), StatusEnum.FAILED);
return R.fail("无人机起飞命令发送失败: " + result.getErrorMessage());
}
} catch (Exception e) {
log.error("无人机起飞命令发送失败: sn={}", request.getSn(), e);
flightService.updateFlightStatus(request.getTaskId(), StatusEnum.FAILED);
return R.fail("无人机起飞命令发送失败: " + e.getMessage());
}
}
/**
* 无人机开机接口
*
* @param sn 机场SN号
* @return 开机响应
*/
@Operation(summary = "无人机开机", description = "控制指定机场的无人机执行开机操作")
@PostMapping("/power-on/{sn}")
public R<String> powerOn(
@Parameter(description = "机场SN号", required = true, example = "THJSQ03B2309DN7VQN43")
@PathVariable("sn") String sn)
{
log.info("收到无人机开机请求: sn={}", sn);
try {
// 调用机器命令管理器执行开机命令
CompletableFuture<CommandResult> future = machineCommandManager.executeCommand(sn, CommandType.POWER_ON);
// 等待命令执行完成
CommandResult result = future.get();
if (result.isSuccess()) {
log.info("无人机开机成功: sn={}", sn);
return R.ok("开机命令执行成功");
} else {
log.error("无人机开机失败: sn={}, reason={}", sn, result.getErrorMessage());
return R.fail("开机命令执行失败: " + result.getErrorMessage());
}
} catch (Exception e) {
log.error("无人机开机异常: sn={}", sn, e);
return R.fail("开机命令执行异常: " + e.getMessage());
}
}
/**
* 无人机关机接口
*
* @param sn 机场SN号
* @return 关机响应
*/
@Operation(summary = "无人机关机", description = "控制指定机场的无人机执行关机操作")
@PostMapping("/power-off/{sn}")
public R<String> powerOff(
@Parameter(description = "机场SN号", required = true, example = "THJSQ03B2309DN7VQN43")
@PathVariable("sn") String sn)
{
log.info("收到无人机关机请求: sn={}", sn);
try {
// 调用机器命令管理器执行关机命令
CompletableFuture<CommandResult> future = machineCommandManager.executeCommand(sn, CommandType.POWER_OFF);
// 等待命令执行完成
CommandResult result = future.get();
if (result.isSuccess()) {
log.info("无人机关机成功: sn={}", sn);
return R.ok("关机命令执行成功");
} else {
log.error("无人机关机失败: sn={}, reason={}", sn, result.getErrorMessage());
return R.fail("关机命令执行失败: " + result.getErrorMessage());
}
} catch (Exception e) {
log.error("无人机关机异常: sn={}", sn, e);
return R.fail("关机命令执行异常: " + e.getMessage());
}
}
/**
* 查询无人机状态
*
* @param sn 设备SN号
* @return 状态信息
*/
@Operation(summary = "查询无人机状态", description = "根据设备SN号查询无人机的实时状态信息")
@GetMapping("/machine-state/{sn}")
public R<MachineStateVO> getMachineState(
@Parameter(description = "设备SN号", required = true, example = "TH001")
@PathVariable("sn") String sn)
{
log.info("查询无人机状态: sn={}", sn);
try {
// MachineStateManager 获取状态
MachineStates states = machineStateManager.getStates(sn);
// 转换为 VO 对象
MachineStateVO vo = new MachineStateVO();
vo.setSn(sn);
vo.setDroneState(states.getDroneState().name());
vo.setAirportState(states.getAirportState().name());
vo.setCoverState(states.getCoverState().name());
vo.setDrcState(states.getDrcState().name());
vo.setDebugModeState(states.getDebugModeState().name());
log.info("查询到状态: sn={}, vo={}", sn, vo);
return R.ok(vo);
} catch (Exception e) {
log.error("查询无人机状态异常: sn={}", sn, e);
return R.fail("查询状态失败: " + e.getMessage());
}
}
/**
* 出舱接口
*
* @param sn 机场SN号
* @return 出舱响应
*/
@Operation(summary = "出舱", description = "控制指定机场执行出舱操作")
@PostMapping("/cover-open/{sn}")
public R<String> coverOpen(
@Parameter(description = "机场SN号", required = true, example = "THJSQ03B2309DN7VQN43")
@PathVariable("sn") String sn)
{
log.info("收到出舱请求: sn={}", sn);
try {
// 调用机器命令管理器执行出舱命令
CompletableFuture<CommandResult> future = machineCommandManager.executeCommand(sn, CommandType.OPEN_COVER);
// 等待命令执行完成
CommandResult result = future.get();
if (result.isSuccess()) {
log.info("出舱成功: sn={}", sn);
return R.ok("出舱命令执行成功");
} else {
log.error("出舱失败: sn={}, reason={}", sn, result.getErrorMessage());
return R.fail("出舱命令执行失败: " + result.getErrorMessage());
}
} catch (Exception e) {
log.error("出舱异常: sn={}", sn, e);
return R.fail("出舱命令执行异常: " + e.getMessage());
}
}
/**
* 回舱接口
*
* @param sn 机场SN号
* @return 回舱响应
*/
@Operation(summary = "回舱", description = "控制指定机场执行回舱操作")
@PostMapping("/cover-close/{sn}")
public R<String> coverClose(
@Parameter(description = "机场SN号", required = true, example = "THJSQ03B2309DN7VQN43")
@PathVariable("sn") String sn)
{
log.info("收到回舱请求: sn={}", sn);
try {
// 调用机器命令管理器执行回舱命令
CompletableFuture<CommandResult> future = machineCommandManager.executeCommand(sn, CommandType.CLOSE_COVER);
// 等待命令执行完成
CommandResult result = future.get();
if (result.isSuccess()) {
log.info("回舱成功: sn={}", sn);
return R.ok("回舱命令执行成功");
} else {
log.error("回舱失败: sn={}, reason={}", sn, result.getErrorMessage());
return R.fail("回舱命令执行失败: " + result.getErrorMessage());
}
} catch (Exception e) {
log.error("回舱异常: sn={}", sn, e);
return R.fail("回舱命令执行异常: " + e.getMessage());
}
}
/**
* 无人机返航接口
*
* @param request 返航请求对象
* @return 返航响应
*/
@Operation(summary = "无人机返航", description = "控制指定机场的无人机执行返航操作")
@PostMapping("/return-home")
public R<String> returnHome(@RequestBody DroneReturnHomeRequest request)
{
log.info("收到无人机返航请求: sn={} ", request.getSn());
try {
Long currentTaskId = flightService.currentRunningTask(request.getSn());
java.util.Map<String, Object> params = new java.util.HashMap<>();
if(Objects.isNull(currentTaskId)){
params.put("messageID", UUID.randomUUID().toString());
}else {
params.put("messageID",currentTaskId);
}
params.put("taskId", 9074);
params.put("zhilin", "03");
CompletableFuture<CommandResult> future = machineCommandManager.executeCommand(request.getSn(), CommandType.RETURN_HOME, params);
CommandResult result = future.get();
if (result.isSuccess()) {
log.info("无人机返航成功: sn={}", request.getSn());
return R.ok("返航命令执行成功");
} else {
log.error("无人机返航失败: sn={}, reason={}", request.getSn(), result.getErrorMessage());
return R.fail("返航命令执行失败: " + result.getErrorMessage());
}
} catch (Exception e) {
log.error("无人机返航异常: sn={}", request.getSn(), e);
return R.fail("返航命令执行异常: " + e.getMessage());
}
}
}

View File

@ -0,0 +1,169 @@
package com.ruoyi.device.controller;
import com.ruoyi.common.core.constant.SecurityConstants;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.web.controller.BaseController;
import com.ruoyi.device.api.domain.AirTypeCategoryGroupVO;
import com.ruoyi.device.api.domain.AirTypeGeneralEnumVO;
import com.ruoyi.device.api.domain.AirTypeVendorGroupVO;
import com.ruoyi.device.controller.convert.DeviceAirTypeGeneralEnumVOConvert;
import com.ruoyi.device.service.api.IDeviceAirTypeGeneralEnumService;
import com.ruoyi.device.service.dto.DeviceAirTypeGeneralEnumDTO;
import com.ruoyi.system.api.RemoteDictService;
import com.ruoyi.system.api.domain.SysDictData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 无人机类型通用枚举Controller
*
* @author 拓恒
* @date 2026-01-28
*/
@RestController
@RequestMapping("/airType/generalEnum")
public class DeviceAirTypeGeneralEnumController extends BaseController
{
@Autowired
private IDeviceAirTypeGeneralEnumService airTypeGeneralEnumService;
@Autowired
private RemoteDictService remoteDictService;
/**
* 查询无人机类型通用枚举列表
*
* @param vo 无人机类型通用枚举VO
* @return 无人机类型通用枚举列表
*/
@GetMapping("/list")
public R<List<AirTypeGeneralEnumVO>> selectAirTypeGeneralEnumList(AirTypeGeneralEnumVO vo)
{
DeviceAirTypeGeneralEnumDTO dto = DeviceAirTypeGeneralEnumVOConvert.to(vo);
List<DeviceAirTypeGeneralEnumDTO> list = airTypeGeneralEnumService.selectAirTypeGeneralEnumList(dto);
List<AirTypeGeneralEnumVO> voList = DeviceAirTypeGeneralEnumVOConvert.fromList(list);
return R.ok(voList);
}
/**
* 按厂商分组查询无人机类型
*
* @param vo 无人机类型通用枚举VO
* @return 按厂商分组的无人机类型列表
*/
@GetMapping("/vendorGroup")
public R<List<AirTypeVendorGroupVO>> selectAirTypeGeneralEnumGroupByVendor(AirTypeGeneralEnumVO vo)
{
// 从数据字典获取无人机厂商类型
R<List<SysDictData>> dictResult = remoteDictService.getDictDataByType("air_vendor_type", SecurityConstants.INNER);
List<AirTypeVendorGroupVO> vendorGroupList = new ArrayList<>();
if (dictResult.getData() != null)
{
// 获取所有无人机类型数据
DeviceAirTypeGeneralEnumDTO dto = DeviceAirTypeGeneralEnumVOConvert.to(vo);
List<DeviceAirTypeGeneralEnumDTO> allList = airTypeGeneralEnumService.selectAirTypeGeneralEnumList(dto);
List<AirTypeGeneralEnumVO> allVoList = DeviceAirTypeGeneralEnumVOConvert.fromList(allList);
// 为每个字典项创建分组
for (SysDictData dictData : dictResult.getData())
{
AirTypeVendorGroupVO groupVO = new AirTypeVendorGroupVO();
groupVO.setLabel(dictData.getDictLabel());
groupVO.setValue(dictData.getDictValue());
// 筛选属于当前厂商的无人机类型
List<AirTypeGeneralEnumVO> vendorAirTypes = new ArrayList<>();
for (AirTypeGeneralEnumVO airTypeGeneralEnumVO : allVoList)
{
if (dictData.getDictValue().equals(airTypeGeneralEnumVO.getVendorId().toString()))
{
airTypeGeneralEnumVO.generateTypeCode();
vendorAirTypes.add(airTypeGeneralEnumVO);
}
}
groupVO.setAirTypeList(vendorAirTypes);
vendorGroupList.add(groupVO);
}
}
return R.ok(vendorGroupList);
}
/**
* 按厂商分组查询无人机类型厂商 -> 分类 -> 设备类型
*
* @param vo 无人机类型通用枚举VO
* @return 按厂商分组的无人机类型列表
*/
@GetMapping("/vendorGroupNew")
public R<List<AirTypeVendorGroupVO>> selectAirTypeGeneralEnumGroupByVendorNew(AirTypeGeneralEnumVO vo)
{
// 从数据字典获取无人机厂商类型
R<List<SysDictData>> dictResult = remoteDictService.getDictDataByType("air_vendor_type", SecurityConstants.INNER);
List<AirTypeVendorGroupVO> vendorGroupList = new ArrayList<>();
if (dictResult.getData() != null)
{
// 获取所有无人机类型数据包括生效和失效的
DeviceAirTypeGeneralEnumDTO dto = DeviceAirTypeGeneralEnumVOConvert.to(vo);
List<DeviceAirTypeGeneralEnumDTO> allList = airTypeGeneralEnumService.selectAirTypeGeneralEnumList(dto);
List<AirTypeGeneralEnumVO> allVoList = DeviceAirTypeGeneralEnumVOConvert.fromList(allList);
// 为每个字典项创建分组
for (SysDictData dictData : dictResult.getData())
{
AirTypeVendorGroupVO groupVO = new AirTypeVendorGroupVO();
groupVO.setLabel(dictData.getDictLabel());
groupVO.setValue(dictData.getDictValue());
// 筛选属于当前厂商的无人机类型
List<AirTypeGeneralEnumVO> vendorAirTypes = new ArrayList<>();
for (AirTypeGeneralEnumVO airTypeGeneralEnumVO : allVoList)
{
if (dictData.getDictValue().equals(airTypeGeneralEnumVO.getVendorId().toString()))
{
airTypeGeneralEnumVO.generateTypeCode();
vendorAirTypes.add(airTypeGeneralEnumVO);
}
}
// 按分类分组
Map<String, List<AirTypeGeneralEnumVO>> categoryMap = new HashMap<>();
for (AirTypeGeneralEnumVO airTypeGeneralEnumVO : vendorAirTypes)
{
String category = airTypeGeneralEnumVO.getCategory();
if (category == null || category.isEmpty()) {
category = "其他";
}
if (!categoryMap.containsKey(category)) {
categoryMap.put(category, new ArrayList<>());
}
categoryMap.get(category).add(airTypeGeneralEnumVO);
}
// 构建分类分组列表
List<AirTypeCategoryGroupVO> categoryGroups = new ArrayList<>();
for (Map.Entry<String, List<AirTypeGeneralEnumVO>> entry : categoryMap.entrySet()) {
AirTypeCategoryGroupVO categoryGroup = new AirTypeCategoryGroupVO();
categoryGroup.setCategory(entry.getKey());
categoryGroup.setAirTypeList(entry.getValue());
categoryGroups.add(categoryGroup);
}
groupVO.setCategoryGroups(categoryGroups);
vendorGroupList.add(groupVO);
}
}
return R.ok(vendorGroupList);
}
}

View File

@ -9,7 +9,7 @@ import com.ruoyi.device.api.domain.DockVO;
import com.ruoyi.device.api.domain.DockWithGPSVO; import com.ruoyi.device.api.domain.DockWithGPSVO;
import com.ruoyi.device.controller.convert.DockWithGPSVOConvert; import com.ruoyi.device.controller.convert.DockWithGPSVOConvert;
import com.ruoyi.device.service.api.IDockService; import com.ruoyi.device.service.api.IDockService;
import com.ruoyi.device.service.api.IBufferDeviceService; import com.ruoyi.device.service.impl.DefaultBufferDeviceImpl;
import com.ruoyi.device.service.dto.DockDetailDTO; import com.ruoyi.device.service.dto.DockDetailDTO;
import com.ruoyi.device.service.dto.DockDTO; import com.ruoyi.device.service.dto.DockDTO;
import com.ruoyi.device.controller.convert.DockVOConvert; import com.ruoyi.device.controller.convert.DockVOConvert;
@ -34,7 +34,7 @@ public class DockController extends BaseController
private IDockService dockService; private IDockService dockService;
@Autowired @Autowired
private IBufferDeviceService bufferDeviceService; private DefaultBufferDeviceImpl bufferDeviceService;
/** /**
* 搜索机场 * 搜索机场
@ -62,8 +62,11 @@ public class DockController extends BaseController
@GetMapping("/detail/{dockId}") @GetMapping("/detail/{dockId}")
public R<DockDetailVO> getDockDetail(@PathVariable("dockId") Long dockId) public R<DockDetailVO> getDockDetail(@PathVariable("dockId") Long dockId)
{ {
DockDetailDTO dockDetailDTO = bufferDeviceService.getDockDetailById(dockId); DockDetailDTO dockDetailDTO = bufferDeviceService.getDockDetailById(dockId);
if (dockDetailDTO == null) {
return R.fail("机场不存在: dockId=" + dockId);
}
DockDetailVO result = new DockDetailVO(); DockDetailVO result = new DockDetailVO();
BeanUtils.copyProperties(dockDetailDTO, result); BeanUtils.copyProperties(dockDetailDTO, result);
return R.ok(result); return R.ok(result);
@ -100,7 +103,9 @@ public class DockController extends BaseController
List<DockDetailDTO> dtoList = new ArrayList<>(); List<DockDetailDTO> dtoList = new ArrayList<>();
for (DockDTO dockDTO : dockDTOs) { for (DockDTO dockDTO : dockDTOs) {
DockDetailDTO dockDetailDTO = bufferDeviceService.getDockDetailById(dockDTO.getDockId()); DockDetailDTO dockDetailDTO = bufferDeviceService.getDockDetailById(dockDTO.getDockId());
dtoList.add(dockDetailDTO); if (dockDetailDTO != null) {
dtoList.add(dockDetailDTO);
}
} }
return R.ok(DockWithGPSVOConvert.fromList(dtoList)); return R.ok(DockWithGPSVOConvert.fromList(dtoList));
} }

View File

@ -6,16 +6,18 @@ import com.ruoyi.device.api.domain.*;
import com.ruoyi.device.controller.convert.DockVOConvert; import com.ruoyi.device.controller.convert.DockVOConvert;
import com.ruoyi.device.controller.convert.DockWithGPSVOConvert; import com.ruoyi.device.controller.convert.DockWithGPSVOConvert;
import com.ruoyi.device.controller.convert.GroupVOConvert; import com.ruoyi.device.controller.convert.GroupVOConvert;
import com.ruoyi.device.service.api.IBufferDeviceService; import com.ruoyi.device.domain.api.IDockDomain;
import com.ruoyi.device.domain.model.Dock;
import com.ruoyi.device.service.impl.DefaultBufferDeviceImpl;
import com.ruoyi.device.service.api.IGroupService; import com.ruoyi.device.service.api.IGroupService;
import com.ruoyi.device.service.dto.DockDetailDTO; import com.ruoyi.device.service.dto.DockDetailDTO;
import com.ruoyi.device.service.dto.DockGroupDTO; import com.ruoyi.device.service.dto.DockGroupDTO;
import com.ruoyi.device.service.dto.GroupDTO; import com.ruoyi.device.service.dto.GroupDTO;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.ArrayList; import java.util.*;
import java.util.List;
/** /**
* 分组Controller * 分组Controller
@ -31,7 +33,10 @@ public class GroupController extends BaseController
private IGroupService groupService; private IGroupService groupService;
@Autowired @Autowired
private IBufferDeviceService bufferDeviceService; private DefaultBufferDeviceImpl bufferDeviceService;
@Autowired
private IDockDomain dockDomain;
/** /**
* 创建分组 * 创建分组
* *
@ -42,10 +47,17 @@ public class GroupController extends BaseController
@PostMapping("/create") @PostMapping("/create")
public R<Long> createGroup(@RequestBody GroupCreateRequest request) public R<Long> createGroup(@RequestBody GroupCreateRequest request)
{ {
GroupDTO dto = new GroupDTO(); try
dto.setGroupName(request.getGroupName()); {
Long groupId = groupService.createGroup(dto); GroupDTO dto = new GroupDTO();
return R.ok(groupId); dto.setGroupName(request.getGroupName());
Long groupId = groupService.createGroup(dto);
return R.ok(groupId);
}
catch (RuntimeException e)
{
return R.fail("新增分组'" + request.getGroupName() + "'失败,分组名称已存在");
}
} }
/** /**
* 删除分组 * 删除分组
@ -71,11 +83,18 @@ public class GroupController extends BaseController
@PostMapping("/update") @PostMapping("/update")
public R<Void> updateGroup(@RequestBody GroupUpdateRequest request) public R<Void> updateGroup(@RequestBody GroupUpdateRequest request)
{ {
GroupDTO dto = new GroupDTO(); try
dto.setGroupId(request.getGroupId()); {
dto.setGroupName(request.getGroupName()); GroupDTO dto = new GroupDTO();
groupService.updateGroup(dto); dto.setGroupId(request.getGroupId());
return R.ok(); dto.setGroupName(request.getGroupName());
groupService.updateGroup(dto);
return R.ok();
}
catch (RuntimeException e)
{
return R.fail("修改分组'" + request.getGroupName() + "'失败,分组名称已存在");
}
} }
/** /**
@ -87,7 +106,12 @@ public class GroupController extends BaseController
@PostMapping("/switch") @PostMapping("/switch")
public R<Void> switchDockGroup(@RequestBody SwitchDockGroupRequest request) public R<Void> switchDockGroup(@RequestBody SwitchDockGroupRequest request)
{ {
groupService.switchDockGroup(request.getDockId(), request.getGroupId());
if(!CollectionUtils.isEmpty(request.getDockIds())){
for (Long dockId : request.getDockIds()) {
groupService.switchDockGroup(dockId, request.getGroupId());
}
}
return R.ok(); return R.ok();
} }
@ -102,14 +126,43 @@ public class GroupController extends BaseController
public R<List<DockWithGPSVO>> getDocksByGroupId(@PathVariable("groupId") Long groupId) public R<List<DockWithGPSVO>> getDocksByGroupId(@PathVariable("groupId") Long groupId)
{ {
logger.info("getDocksByGroupId {}", groupId); logger.info("getDocksByGroupId {}", groupId);
List<DockGroupDTO> groupDTOS = groupService.getDocksByGroupId(groupId);
List<DockDetailDTO> dtoList = new ArrayList<>(); if(Objects.equals(groupId, -1L)){
for (DockGroupDTO dockGroupDTO : groupDTOS) { // List<DockGroupDTO> groupDTOS = groupService.getDocksByGroupId(groupId);
DockDetailDTO dockDetailDTO = bufferDeviceService.getDockDetailById(dockGroupDTO.getDockId()); List<DockDetailDTO> dtoList = new ArrayList<>();
dtoList.add(dockDetailDTO); Dock queryDock = new Dock();
List<Dock> allDocks = dockDomain.selectDockList(queryDock);
if (allDocks != null) {
for (Dock dock : allDocks) {
if (dock.getLastActiveTime() != null) {
DockDetailDTO dockDetailDTO = bufferDeviceService.getDockDetailById(dock.getDockId());
if (dockDetailDTO != null) {
dockDetailDTO.setLastActiveTime(dock.getLastActiveTime());
dockDetailDTO.setCabinVideoUrl(dock.getCabinVideoUrl());
dockDetailDTO.setOutsideVideoUrl(dock.getOutsideVideoUrl());
dockDetailDTO.setLiveVideoUrl(dock.getLiveVideoUrl());
dtoList.add(dockDetailDTO);
}
}
}
}
dtoList.sort(Comparator.comparing(DockDetailDTO::getLastActiveTime).reversed());
return R.ok(DockWithGPSVOConvert.fromList(dtoList));
}else {
List<DockGroupDTO> groupDTOS = groupService.getDocksByGroupId(groupId);
List<DockDetailDTO> dtoList = new ArrayList<>();
for (DockGroupDTO dockGroupDTO : groupDTOS) {
DockDetailDTO dockDetailDTO = bufferDeviceService.getDockDetailById(dockGroupDTO.getDockId());
if (dockDetailDTO != null) {
dtoList.add(dockDetailDTO);
}
}
return R.ok(DockWithGPSVOConvert.fromList(dtoList));
} }
return R.ok(DockWithGPSVOConvert.fromList(dtoList));
} }
/** /**
@ -122,10 +175,29 @@ public class GroupController extends BaseController
public R<List<GroupVO>> getAllGroupIds() public R<List<GroupVO>> getAllGroupIds()
{ {
List<GroupDTO> groupDTOs = groupService.getAllGroupIds(); List<GroupDTO> groupDTOs = groupService.getAllGroupIds();
// Calculate aircraft count for each group
List<GroupVO> groupVOS = new ArrayList<>(); List<GroupVO> groupVOS = new ArrayList<>();
// 添加虚拟的"最近使用"分组groupId = -1
// 统计 last_active_time 不为空的机场数量
Dock queryDock = new Dock();
List<Dock> allDocks = dockDomain.selectDockList(queryDock);
int recentlyUsedCount = 0;
if (allDocks != null) {
for (Dock dock : allDocks) {
if (dock.getLastActiveTime() != null) {
recentlyUsedCount++;
}
}
}
// 创建"最近使用"虚拟分组
GroupVO recentlyUsedGroup = new GroupVO();
recentlyUsedGroup.setGroupId(-1L);
recentlyUsedGroup.setGroupName("最近使用");
recentlyUsedGroup.setDockCount(recentlyUsedCount);
groupVOS.add(recentlyUsedGroup);
// 添加其他真实分组
for(GroupDTO groupDTO : groupDTOs){ for(GroupDTO groupDTO : groupDTOs){
GroupVO groupVO = GroupVOConvert.from(groupDTO); GroupVO groupVO = GroupVOConvert.from(groupDTO);
List<DockGroupDTO> dockGroupDTOs = groupService.getDocksByGroupId(groupDTO.getGroupId()); List<DockGroupDTO> dockGroupDTOs = groupService.getDocksByGroupId(groupDTO.getGroupId());

View File

@ -7,20 +7,27 @@ import com.ruoyi.device.api.enums.AircraftStatusEnum;
import com.ruoyi.device.api.enums.DockStatusEnum; import com.ruoyi.device.api.enums.DockStatusEnum;
import com.ruoyi.device.api.enums.PayloadStatusEnum; import com.ruoyi.device.api.enums.PayloadStatusEnum;
import com.ruoyi.device.service.api.IAircraftService; import com.ruoyi.device.service.api.IAircraftService;
import com.ruoyi.device.service.api.IBufferDeviceService; import com.ruoyi.device.service.impl.DaJiangBufferDeviceImpl;
import com.ruoyi.device.service.impl.TuohengBufferDeviceImpl;
import com.ruoyi.device.service.api.IDockService; import com.ruoyi.device.service.api.IDockService;
import com.ruoyi.device.domain.api.IDeviceDomain;
import com.ruoyi.device.domain.model.Device;
import com.ruoyi.device.service.api.IPayloadService; import com.ruoyi.device.service.api.IPayloadService;
import com.ruoyi.device.service.dto.AircraftDTO; import com.ruoyi.device.service.dto.AircraftDTO;
import com.ruoyi.device.service.dto.AircraftDetailDTO; import com.ruoyi.device.service.dto.AircraftDetailDTO;
import com.ruoyi.device.service.dto.DockDTO; import com.ruoyi.device.service.dto.DockDTO;
import com.ruoyi.device.service.dto.DockDetailDTO; import com.ruoyi.device.service.dto.DockDetailDTO;
import com.ruoyi.device.service.dto.PayloadDTO; import com.ruoyi.device.service.dto.PayloadDTO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/** /**
* 统计Controller * 统计Controller
@ -32,6 +39,8 @@ import java.util.List;
@RequestMapping("/statistics") @RequestMapping("/statistics")
public class StaticsController extends BaseController public class StaticsController extends BaseController
{ {
private static final Logger log = LoggerFactory.getLogger(StaticsController.class);
@Autowired @Autowired
private IDockService dockService; private IDockService dockService;
@ -42,209 +51,392 @@ public class StaticsController extends BaseController
private IPayloadService payloadService; private IPayloadService payloadService;
@Autowired @Autowired
private IBufferDeviceService bufferDeviceService; private DaJiangBufferDeviceImpl daJiangBufferDeviceService;
@Autowired
private TuohengBufferDeviceImpl tuohengBufferDeviceService;
@Autowired
private IDeviceDomain deviceDomain;
/** /**
* 获取系统统计信息 * 获取系统统计信息合并大疆和拓恒
* *
* @return 统计信息 * @return 统计信息
*/ */
@GetMapping @GetMapping
public R<StatisticsVO> getStatistics() public R<StatisticsVO> getStatistics()
{ {
StatisticsVO vo = new StatisticsVO(); log.info("========== 开始统计所有设备信息(大疆+拓恒) ==========");
// 获取所有机场 // 获取大疆统计
List<DockDTO> docks = dockService.selectDockList(new DockDTO()); StatisticsVO djiStats = buildDjiStatisticsVO();
vo.setDockCount(docks != null ? docks.size() : 0);
// 统计各状态机场数量 // 获取拓恒统计
int idleCount = 0; StatisticsVO thStats = buildThStatisticsVO();
int workingCount = 0;
int debuggingCount = 0;
int offlineCount = 0;
if (docks != null) { // 合并统计结果
for (DockDTO dock : docks) { StatisticsVO totalStats = new StatisticsVO();
DockDetailDTO dockDetail = bufferDeviceService.getDockDetailById(dock.getDockId());
if (dockDetail != null && dockDetail.getDockStatus() != null) {
String status = dockDetail.getDockStatus();
if (DockStatusEnum.IDLE.getCode().equals(status)) {
idleCount++;
} else if (DockStatusEnum.WORKING.getCode().equals(status)) {
workingCount++;
} else if (DockStatusEnum.Debugging.getCode().equals(status)) {
debuggingCount++;
} else {
offlineCount++;
}
}
}
}
vo.setIdleDockCount(idleCount); // 机场统计
vo.setWorkingDockCount(workingCount); totalStats.setDockCount(djiStats.getDockCount() + thStats.getDockCount());
vo.setDebuggingDockCount(debuggingCount); totalStats.setIdleDockCount(djiStats.getIdleDockCount() + thStats.getIdleDockCount());
vo.setOfflineDockCount(offlineCount); totalStats.setWorkingDockCount(djiStats.getWorkingDockCount() + thStats.getWorkingDockCount());
totalStats.setDebuggingDockCount(djiStats.getDebuggingDockCount() + thStats.getDebuggingDockCount());
totalStats.setOfflineDockCount(djiStats.getOfflineDockCount() + thStats.getOfflineDockCount());
// 获取所有无人机 // 无人机统计
List<AircraftDTO> aircrafts = aircraftService.selectAircraftList(new AircraftDTO()); totalStats.setAircraftCount(djiStats.getAircraftCount() + thStats.getAircraftCount());
vo.setAircraftCount(aircrafts != null ? aircrafts.size() : 0); totalStats.setPowerOnInCabinCount(djiStats.getPowerOnInCabinCount() + thStats.getPowerOnInCabinCount());
totalStats.setPowerOffInCabinCount(djiStats.getPowerOffInCabinCount() + thStats.getPowerOffInCabinCount());
totalStats.setInMissionCount(djiStats.getInMissionCount() + thStats.getInMissionCount());
totalStats.setDebuggingAircraftCount(djiStats.getDebuggingAircraftCount() + thStats.getDebuggingAircraftCount());
totalStats.setOfflineAircraftCount(djiStats.getOfflineAircraftCount() + thStats.getOfflineAircraftCount());
// 统计各状态无人机数量 // 挂载统计
int powerOnInCabinCount = 0; totalStats.setPayloadCount(djiStats.getPayloadCount() + thStats.getPayloadCount());
int powerOffInCabinCount = 0; totalStats.setOfflinePayloadCount(djiStats.getOfflinePayloadCount() + thStats.getOfflinePayloadCount());
int inMissionCount = 0;
int debuggingAircraftCount = 0;
int offlineAircraftCount = 0;
if (aircrafts != null) { log.info("========== 所有设备统计完成 ==========");
for (AircraftDTO aircraft : aircrafts) { log.info("总计: 机场={}, 任务中机场={}, 无人机={}, 任务中无人机={}",
AircraftDetailDTO aircraftDetail = bufferDeviceService.getAircraftDetailById(aircraft.getAircraftId()); totalStats.getDockCount(), totalStats.getWorkingDockCount(),
if (aircraftDetail != null && aircraftDetail.getAircraftStatus() != null) { totalStats.getAircraftCount(), totalStats.getInMissionCount());
String status = aircraftDetail.getAircraftStatus();
if (AircraftStatusEnum.POWER_ON_IN_CABIN.getCode().equals(status)) {
powerOnInCabinCount++;
} else if (AircraftStatusEnum.POWER_OFF_IN_CABIN.getCode().equals(status)) {
powerOffInCabinCount++;
} else if (AircraftStatusEnum.IN_MISSION.getCode().equals(status)) {
inMissionCount++;
} else if (AircraftStatusEnum.DEBUGGING.getCode().equals(status)) {
debuggingAircraftCount++;
} else if (AircraftStatusEnum.OFFLINE.getCode().equals(status)) {
offlineAircraftCount++;
}
}
}
}
vo.setPowerOnInCabinCount(powerOnInCabinCount); return R.ok(totalStats);
vo.setPowerOffInCabinCount(powerOffInCabinCount);
vo.setInMissionCount(inMissionCount);
vo.setDebuggingAircraftCount(debuggingAircraftCount);
vo.setOfflineAircraftCount(offlineAircraftCount);
// 获取所有挂载
List<PayloadDTO> payloads = payloadService.selectPayloadList(new PayloadDTO());
vo.setPayloadCount(payloads != null ? payloads.size() : 0);
// 统计离线挂载数量暂时设置为0因为挂载状态需要从实时数据获取
vo.setOfflinePayloadCount(0);
return R.ok(vo);
} }
@GetMapping("/dji") @GetMapping("/dji")
public R<StatisticsVO> getDjiStatistics() public R<StatisticsVO> getDjiStatistics()
{ {
StatisticsVO vo = new StatisticsVO(); return R.ok(buildDjiStatisticsVO());
// 获取所有机场
List<DockDTO> docks = dockService.selectDockList(new DockDTO());
vo.setDockCount(docks != null ? docks.size() : 0);
// 统计各状态机场数量
int idleCount = 0;
int workingCount = 0;
int debuggingCount = 0;
int offlineCount = 0;
if (docks != null) {
for (DockDTO dock : docks) {
DockDetailDTO dockDetail = bufferDeviceService.getDockDetailById(dock.getDockId());
if (dockDetail != null && dockDetail.getDockStatus() != null) {
String status = dockDetail.getDockStatus();
if (DockStatusEnum.IDLE.getCode().equals(status)) {
idleCount++;
} else if (DockStatusEnum.WORKING.getCode().equals(status)) {
workingCount++;
} else if (DockStatusEnum.Debugging.getCode().equals(status)) {
debuggingCount++;
} else {
offlineCount++;
}
}
}
}
vo.setIdleDockCount(idleCount);
vo.setWorkingDockCount(workingCount);
vo.setDebuggingDockCount(debuggingCount);
vo.setOfflineDockCount(offlineCount);
// 获取所有无人机
List<AircraftDTO> aircrafts = aircraftService.selectAircraftList(new AircraftDTO());
vo.setAircraftCount(aircrafts != null ? aircrafts.size() : 0);
// 统计各状态无人机数量
int powerOnInCabinCount = 0;
int powerOffInCabinCount = 0;
int inMissionCount = 0;
int debuggingAircraftCount = 0;
int offlineAircraftCount = 0;
if (aircrafts != null) {
for (AircraftDTO aircraft : aircrafts) {
AircraftDetailDTO aircraftDetail = bufferDeviceService.getAircraftDetailById(aircraft.getAircraftId());
if (aircraftDetail != null && aircraftDetail.getAircraftStatus() != null) {
String status = aircraftDetail.getAircraftStatus();
if (AircraftStatusEnum.POWER_ON_IN_CABIN.getCode().equals(status)) {
powerOnInCabinCount++;
} else if (AircraftStatusEnum.POWER_OFF_IN_CABIN.getCode().equals(status)) {
powerOffInCabinCount++;
} else if (AircraftStatusEnum.IN_MISSION.getCode().equals(status)) {
inMissionCount++;
} else if (AircraftStatusEnum.DEBUGGING.getCode().equals(status)) {
debuggingAircraftCount++;
} else if (AircraftStatusEnum.OFFLINE.getCode().equals(status)) {
offlineAircraftCount++;
}
}
}
}
vo.setPowerOnInCabinCount(powerOnInCabinCount);
vo.setPowerOffInCabinCount(powerOffInCabinCount);
vo.setInMissionCount(inMissionCount);
vo.setDebuggingAircraftCount(debuggingAircraftCount);
vo.setOfflineAircraftCount(offlineAircraftCount);
// 获取所有挂载
List<PayloadDTO> payloads = payloadService.selectPayloadList(new PayloadDTO());
vo.setPayloadCount(payloads != null ? payloads.size() : 0);
// 统计离线挂载数量暂时设置为0因为挂载状态需要从实时数据获取
vo.setOfflinePayloadCount(0);
return R.ok(vo);
} }
@GetMapping("/th") @GetMapping("/th")
public R<StatisticsVO> getThStatistics() public R<StatisticsVO> getThStatistics()
{ {
return R.ok(buildThStatisticsVO());
}
private StatisticsVO buildDjiStatisticsVO (){
log.info("========== 开始统计DJI设备信息 ==========");
StatisticsVO vo = new StatisticsVO(); StatisticsVO vo = new StatisticsVO();
// 机场统计 // 获取所有机场
vo.setDockCount(0); List<DockDTO> allDocks = dockService.selectDockList(new DockDTO());
vo.setIdleDockCount(0);
vo.setWorkingDockCount(0);
vo.setDebuggingDockCount(0);
vo.setOfflineDockCount(0);
// 无人机统计 // 过滤出大疆机场
vo.setAircraftCount(0); List<DockDTO> docks = filterDocksByManufacturer(allDocks, "dajiang");
vo.setPowerOnInCabinCount(0); vo.setDockCount(docks.size());
vo.setPowerOffInCabinCount(0); log.info("大疆机场总数: {}", vo.getDockCount());
vo.setInMissionCount(0);
vo.setDebuggingAircraftCount(0);
vo.setOfflineAircraftCount(0);
// 挂载统计 // 批量获取机场详情 - 优化从N次查询减少到1次批量查询
Map<Long, DockDetailDTO> dockDetailsMap = null;
if (!docks.isEmpty()) {
List<Long> dockIds = docks.stream()
.map(DockDTO::getDockId)
.collect(Collectors.toList());
dockDetailsMap = daJiangBufferDeviceService.getDockDetailsByIds(dockIds);
}
// 统计各状态机场数量
int idleCount = 0;
int workingCount = 0;
int debuggingCount = 0;
int offlineCount = 0;
if (docks != null && dockDetailsMap != null) {
log.info("---------- 开始统计机场状态 ----------");
for (DockDTO dock : docks) {
DockDetailDTO dockDetail = dockDetailsMap.get(dock.getDockId());
if (dockDetail != null && dockDetail.getDockStatus() != null) {
String status = dockDetail.getDockStatus();
log.info("机场[ID:{}, Name:{}] 状态: {}", dock.getDockId(), dock.getDockName(), status);
if (DockStatusEnum.IDLE.getCode().equalsIgnoreCase(status)) {
idleCount++;
log.debug(" -> 匹配到IDLE状态");
} else if (DockStatusEnum.WORKING.getCode().equalsIgnoreCase(status)) {
workingCount++;
log.info(" -> 匹配到WORKING状态 (任务中)");
} else if (DockStatusEnum.Debugging.getCode().equalsIgnoreCase(status)) {
debuggingCount++;
log.debug(" -> 匹配到Debugging状态");
} else {
offlineCount++;
log.debug(" -> 其他状态,归类为离线");
}
}
}
}
vo.setIdleDockCount(idleCount);
vo.setWorkingDockCount(workingCount);
vo.setDebuggingDockCount(debuggingCount);
vo.setOfflineDockCount(offlineCount);
log.info("机场状态统计结果 -> 空闲:{}, 任务中:{}, 调试:{}, 离线:{}", idleCount, workingCount, debuggingCount, offlineCount);
// 获取所有无人机
List<AircraftDTO> allAircrafts = aircraftService.selectAircraftList(new AircraftDTO());
// 过滤出大疆无人机
List<AircraftDTO> aircrafts = filterAircraftsByManufacturer(allAircrafts, "dajiang");
vo.setAircraftCount(aircrafts.size());
log.info("大疆无人机总数: {}", vo.getAircraftCount());
// 批量获取无人机详情 - 优化从N次查询减少到1次批量查询
Map<Long, AircraftDetailDTO> aircraftDetailsMap = null;
if (!aircrafts.isEmpty()) {
List<Long> aircraftIds = aircrafts.stream()
.map(AircraftDTO::getAircraftId)
.collect(Collectors.toList());
aircraftDetailsMap = daJiangBufferDeviceService.getAircraftDetailsByIds(aircraftIds);
}
// 统计各状态无人机数量
int powerOnInCabinCount = 0;
int powerOffInCabinCount = 0;
int inMissionCount = 0;
int debuggingAircraftCount = 0;
int offlineAircraftCount = 0;
if (aircrafts != null && aircraftDetailsMap != null) {
log.info("---------- 开始统计大疆无人机状态 ----------");
for (AircraftDTO aircraft : aircrafts) {
AircraftDetailDTO aircraftDetail = aircraftDetailsMap.get(aircraft.getAircraftId());
if (aircraftDetail != null && aircraftDetail.getAircraftStatus() != null) {
String status = aircraftDetail.getAircraftStatus();
log.info("大疆无人机[ID:{}, Name:{}] 状态: {}", aircraft.getAircraftId(), aircraft.getAircraftName(), status);
if (AircraftStatusEnum.POWER_ON_IN_CABIN.getCode().equalsIgnoreCase(status)) {
powerOnInCabinCount++;
log.info(" -> 匹配到舱内开机状态");
} else if (AircraftStatusEnum.POWER_OFF_IN_CABIN.getCode().equalsIgnoreCase(status)) {
powerOffInCabinCount++;
log.info(" -> 匹配到舱内关机状态");
} else if (AircraftStatusEnum.POWER_ON_OUT_CABIN.getCode().equalsIgnoreCase(status)) {
// 舱外开机归类到舱内开机
powerOnInCabinCount++;
log.info(" -> 匹配到舱外开机状态,归类到舱内开机");
} else if (AircraftStatusEnum.POWER_OFF_OUT_CABIN.getCode().equalsIgnoreCase(status)) {
// 舱外关机归类到舱内关机
powerOffInCabinCount++;
log.info(" -> 匹配到舱外关机状态,归类到舱内关机");
} else if (AircraftStatusEnum.IN_MISSION.getCode().equalsIgnoreCase(status)) {
inMissionCount++;
log.info(" -> 匹配到IN_MISSION状态 (任务中)");
} else if (AircraftStatusEnum.DEBUGGING.getCode().equalsIgnoreCase(status)) {
debuggingAircraftCount++;
log.info(" -> 匹配到调试状态");
} else if (AircraftStatusEnum.OFFLINE.getCode().equalsIgnoreCase(status)) {
offlineAircraftCount++;
log.info(" -> 匹配到离线状态");
} else {
offlineAircraftCount++;
log.info(" -> 未知状态[{}],归类为离线", status);
}
} else {
log.warn("大疆无人机[ID:{}, Name:{}] 无法获取详情或状态为空",
aircraft.getAircraftId(), aircraft.getAircraftName());
}
}
}
vo.setPowerOnInCabinCount(powerOnInCabinCount);
vo.setPowerOffInCabinCount(powerOffInCabinCount);
vo.setInMissionCount(inMissionCount);
vo.setDebuggingAircraftCount(debuggingAircraftCount);
vo.setOfflineAircraftCount(offlineAircraftCount);
log.info("无人机状态统计结果 -> 舱内开机:{}, 舱内关机:{}, 任务中:{}, 调试:{}, 离线:{}",
powerOnInCabinCount, powerOffInCabinCount, inMissionCount, debuggingAircraftCount, offlineAircraftCount);
// 获取所有挂载
List<PayloadDTO> payloads = payloadService.selectPayloadList(new PayloadDTO());
vo.setPayloadCount(payloads != null ? payloads.size() : 0);
// 统计离线挂载数量暂时设置为0因为挂载状态需要从实时数据获取
vo.setOfflinePayloadCount(0);
log.info("========== DJI设备统计完成 ==========");
log.info("最终统计结果: 机场总数={}, 任务中机场={}, 无人机总数={}, 任务中无人机={}",
vo.getDockCount(), vo.getWorkingDockCount(), vo.getAircraftCount(), vo.getInMissionCount());
return vo;
}
/**
* 构建拓恒设备统计信息
*/
private StatisticsVO buildThStatisticsVO() {
log.info("========== 开始统计拓恒设备信息 ==========");
StatisticsVO vo = new StatisticsVO();
// 获取所有机场
List<DockDTO> allDocks = dockService.selectDockList(new DockDTO());
// 过滤出拓恒机场
List<DockDTO> thDocks = filterDocksByManufacturer(allDocks, "tuoheng");
vo.setDockCount(thDocks.size());
log.info("拓恒机场总数: {}", vo.getDockCount());
// 批量获取拓恒机场详情
Map<Long, DockDetailDTO> dockDetailsMap = null;
if (!thDocks.isEmpty()) {
List<Long> dockIds = thDocks.stream()
.map(DockDTO::getDockId)
.collect(Collectors.toList());
dockDetailsMap = tuohengBufferDeviceService.getDockDetailsByIds(dockIds);
}
// 统计各状态机场数量
int idleCount = 0;
int workingCount = 0;
int debuggingCount = 0;
int offlineCount = 0;
if (dockDetailsMap != null) {
log.info("---------- 开始统计拓恒机场状态 ----------");
for (DockDTO dock : thDocks) {
DockDetailDTO dockDetail = dockDetailsMap.get(dock.getDockId());
if (dockDetail != null && dockDetail.getDockStatus() != null) {
String status = dockDetail.getDockStatus();
log.info("拓恒机场[ID:{}, Name:{}] 状态: {}", dock.getDockId(), dock.getDockName(), status);
if (DockStatusEnum.IDLE.getCode().equalsIgnoreCase(status)) {
idleCount++;
} else if (DockStatusEnum.WORKING.getCode().equalsIgnoreCase(status)) {
workingCount++;
} else if (DockStatusEnum.Debugging.getCode().equalsIgnoreCase(status)) {
debuggingCount++;
} else {
offlineCount++;
}
}
}
}
vo.setIdleDockCount(idleCount);
vo.setWorkingDockCount(workingCount);
vo.setDebuggingDockCount(debuggingCount);
vo.setOfflineDockCount(offlineCount);
log.info("拓恒机场状态统计 -> 空闲:{}, 任务中:{}, 调试:{}, 离线:{}",
idleCount, workingCount, debuggingCount, offlineCount);
// 获取所有无人机
List<AircraftDTO> allAircrafts = aircraftService.selectAircraftList(new AircraftDTO());
// 过滤出拓恒无人机
List<AircraftDTO> thAircrafts = filterAircraftsByManufacturer(allAircrafts, "tuoheng");
vo.setAircraftCount(thAircrafts.size());
log.info("拓恒无人机总数: {}", vo.getAircraftCount());
// 批量获取拓恒无人机详情
Map<Long, AircraftDetailDTO> aircraftDetailsMap = null;
if (!thAircrafts.isEmpty()) {
List<Long> aircraftIds = thAircrafts.stream()
.map(AircraftDTO::getAircraftId)
.collect(Collectors.toList());
aircraftDetailsMap = tuohengBufferDeviceService.getAircraftDetailsByIds(aircraftIds);
}
// 统计各状态无人机数量
int powerOnInCabinCount = 0;
int powerOffInCabinCount = 0;
int inMissionCount = 0;
int debuggingAircraftCount = 0;
int offlineAircraftCount = 0;
if (aircraftDetailsMap != null) {
log.info("---------- 开始统计拓恒无人机状态 ----------");
for (AircraftDTO aircraft : thAircrafts) {
AircraftDetailDTO aircraftDetail = aircraftDetailsMap.get(aircraft.getAircraftId());
if (aircraftDetail != null && aircraftDetail.getAircraftStatus() != null) {
String status = aircraftDetail.getAircraftStatus();
log.info("拓恒无人机[ID:{}, Name:{}] 状态: {}",
aircraft.getAircraftId(), aircraft.getAircraftName(), status);
if (AircraftStatusEnum.POWER_ON_IN_CABIN.getCode().equalsIgnoreCase(status)) {
powerOnInCabinCount++;
log.info(" -> 匹配到舱内开机状态");
} else if (AircraftStatusEnum.POWER_OFF_IN_CABIN.getCode().equalsIgnoreCase(status)) {
powerOffInCabinCount++;
log.info(" -> 匹配到舱内关机状态");
} else if (AircraftStatusEnum.POWER_ON_OUT_CABIN.getCode().equalsIgnoreCase(status)) {
// 舱外开机归类到舱内开机
powerOnInCabinCount++;
log.info(" -> 匹配到舱外开机状态,归类到舱内开机");
} else if (AircraftStatusEnum.POWER_OFF_OUT_CABIN.getCode().equalsIgnoreCase(status)) {
// 舱外关机归类到舱内关机
powerOffInCabinCount++;
log.info(" -> 匹配到舱外关机状态,归类到舱内关机");
} else if (AircraftStatusEnum.IN_MISSION.getCode().equalsIgnoreCase(status)) {
inMissionCount++;
log.info(" -> 匹配到IN_MISSION状态 (任务中)");
} else if (AircraftStatusEnum.DEBUGGING.getCode().equalsIgnoreCase(status)) {
debuggingAircraftCount++;
log.info(" -> 匹配到调试状态");
} else if (AircraftStatusEnum.OFFLINE.getCode().equalsIgnoreCase(status)) {
offlineAircraftCount++;
log.info(" -> 匹配到离线状态");
} else {
offlineAircraftCount++;
log.info(" -> 未知状态[{}],归类为离线", status);
}
} else {
log.warn("拓恒无人机[ID:{}, Name:{}] 无法获取详情或状态为空",
aircraft.getAircraftId(), aircraft.getAircraftName());
}
}
}
vo.setPowerOnInCabinCount(powerOnInCabinCount);
vo.setPowerOffInCabinCount(powerOffInCabinCount);
vo.setInMissionCount(inMissionCount);
vo.setDebuggingAircraftCount(debuggingAircraftCount);
vo.setOfflineAircraftCount(offlineAircraftCount);
log.info("拓恒无人机状态统计 -> 舱内开机:{}, 舱内关机:{}, 任务中:{}, 调试:{}, 离线:{}",
powerOnInCabinCount, powerOffInCabinCount, inMissionCount,
debuggingAircraftCount, offlineAircraftCount);
// 挂载统计拓恒设备暂时设置为0
vo.setPayloadCount(0); vo.setPayloadCount(0);
vo.setOfflinePayloadCount(0); vo.setOfflinePayloadCount(0);
return R.ok(vo); log.info("========== 拓恒设备统计完成 ==========");
return vo;
}
/**
* 根据厂商过滤机场列表
*/
private List<DockDTO> filterDocksByManufacturer(List<DockDTO> docks, String manufacturer) {
if (docks == null || docks.isEmpty()) {
return List.of();
}
return docks.stream()
.filter(dock -> {
Device device = deviceDomain.selectDeviceByDeviceId(dock.getDeviceId());
return device != null && manufacturer.equals(device.getDeviceManufacturer());
})
.collect(Collectors.toList());
}
/**
* 根据厂商过滤无人机列表
*/
private List<AircraftDTO> filterAircraftsByManufacturer(List<AircraftDTO> aircrafts, String manufacturer) {
if (aircrafts == null || aircrafts.isEmpty()) {
return List.of();
}
return aircrafts.stream()
.filter(aircraft -> {
Device device = deviceDomain.selectDeviceByDeviceId(aircraft.getDeviceId());
return device != null && manufacturer.equals(device.getDeviceManufacturer());
})
.collect(Collectors.toList());
} }
} }

View File

@ -0,0 +1,86 @@
package com.ruoyi.device.controller;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.web.controller.BaseController;
import com.ruoyi.device.domain.api.IThingsBoardDomain;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.Data;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* ThingsBoard设备属性管理Controller
*
* @author ruoyi
* @date 2026-02-06
*/
@RestController
@RequestMapping("/thingsboard")
@Tag(name = "ThingsBoard设备属性管理", description = "提供设备属性的读写操作")
public class ThingsBoardController extends BaseController {
private static final Logger log = LoggerFactory.getLogger(ThingsBoardController.class);
@Autowired
private IThingsBoardDomain thingsBoardDomain;
/**
* 设置设备属性
*
* @param request 设置属性请求
* @return 操作结果
*/
@PostMapping("/attribute")
@Operation(summary = "设置设备属性", description = "设置ThingsBoard设备的属性值")
public R<Void> setDeviceAttribute(@RequestBody SetAttributeRequest request) {
log.info("收到设置设备属性请求: deviceIotId={}, key={}, value={}",
request.getDeviceIotId(), request.getKey(), request.getValue());
// 参数校验
if (request.getDeviceIotId() == null || request.getDeviceIotId().trim().isEmpty()) {
return R.fail("设备IOT ID不能为空");
}
if (request.getKey() == null || request.getKey().trim().isEmpty()) {
return R.fail("属性键不能为空");
}
if (request.getValue() == null) {
return R.fail("属性值不能为空");
}
// 调用Domain层设置属性
boolean success = thingsBoardDomain.setDeviceAttribute(
request.getDeviceIotId(),
request.getKey(),
request.getValue()
);
if (success) {
log.info("设备属性设置成功: deviceIotId={}, key={}",
request.getDeviceIotId(), request.getKey());
return R.ok();
} else {
log.error("设备属性设置失败: deviceIotId={}, key={}",
request.getDeviceIotId(), request.getKey());
return R.fail("设备属性设置失败");
}
}
/**
* 设置属性请求对象
*/
@Data
public static class SetAttributeRequest {
@Parameter(description = "设备IOT IDThingsBoard设备ID", required = true)
private String deviceIotId;
@Parameter(description = "属性键", required = true)
private String key;
@Parameter(description = "属性值", required = true)
private Object value;
}
}

View File

@ -0,0 +1,73 @@
package com.ruoyi.device.controller.convert;
import com.ruoyi.common.core.utils.BaseConvert;
import com.ruoyi.device.api.domain.AirLoadTypeVO;
import com.ruoyi.device.api.domain.AirTypeGeneralEnumVO;
import com.ruoyi.device.service.dto.DeviceAirLoadTypeDTO;
import com.ruoyi.device.service.dto.DeviceAirTypeGeneralEnumDTO;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 无人机类型通用枚举Controller转换器
*
* @author 拓恒
* @date 2026-01-20
*/
public class DeviceAirTypeGeneralEnumVOConvert extends BaseConvert<DeviceAirTypeGeneralEnumDTO, AirTypeGeneralEnumVO>
{
private static final DeviceAirTypeGeneralEnumVOConvert INSTANCE = new DeviceAirTypeGeneralEnumVOConvert();
private DeviceAirTypeGeneralEnumVOConvert() {
super(DeviceAirTypeGeneralEnumDTO.class, AirTypeGeneralEnumVO.class);
}
public static AirTypeGeneralEnumVO from(DeviceAirTypeGeneralEnumDTO dto)
{
AirTypeGeneralEnumVO vo = INSTANCE.innerFrom(dto);
// 手动转换负载列表按系列分组
if (dto.getLoadList() != null) {
Map<String, List<AirLoadTypeVO>> loadVOMap = new HashMap<>();
for (Map.Entry<String, List<DeviceAirLoadTypeDTO>> entry : dto.getLoadList().entrySet()) {
String series = entry.getKey();
List<DeviceAirLoadTypeDTO> loadDTOList = entry.getValue();
List<AirLoadTypeVO> loadVOList = new ArrayList<>();
for (DeviceAirLoadTypeDTO loadDTO : loadDTOList) {
AirLoadTypeVO loadVO = new AirLoadTypeVO();
loadVO.setLoadName(loadDTO.getLoadName());
loadVO.setLoadSeries(loadDTO.getLoadSeries());
loadVO.setLoadCategory(loadDTO.getLoadCategory());
loadVO.setSlot(loadDTO.getSlot());
loadVOList.add(loadVO);
}
loadVOMap.put(series, loadVOList);
}
vo.setLoadList(loadVOMap);
}
return vo;
}
public static DeviceAirTypeGeneralEnumDTO to(AirTypeGeneralEnumVO vo)
{
return INSTANCE.innerTo(vo);
}
public static List<AirTypeGeneralEnumVO> fromList(List<DeviceAirTypeGeneralEnumDTO> dtoList)
{
List<AirTypeGeneralEnumVO> voList = new ArrayList<>();
for (DeviceAirTypeGeneralEnumDTO dto : dtoList) {
voList.add(from(dto));
}
return voList;
}
public static List<DeviceAirTypeGeneralEnumDTO> toList(List<AirTypeGeneralEnumVO> voList)
{
return INSTANCE.innerToList(voList);
}
}

View File

@ -0,0 +1,38 @@
package com.ruoyi.device.domain;
import com.ruoyi.common.core.web.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 无人机负载槽位实体
*
* @author 拓恒
* @date 2026-03-04
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class DeviceAirLoadSlot extends BaseEntity {
private static final long serialVersionUID = 1L;
/** 主键 */
private Long id;
/** 无人机厂商ID */
private Long vendorId;
/** 无人机主类型 */
private Long type;
/** 无人机子类型 */
private Long subType;
/** 槽位数 */
private Integer slotCount;
/** 负载数量限制 */
private Integer loadLimit;
/** 配件限制数量 */
private Integer accessoryLimit;
}

View File

@ -0,0 +1,41 @@
package com.ruoyi.device.domain;
import com.ruoyi.common.core.web.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 无人机负载类型实体
*
* @author 拓恒
* @date 2026-03-04
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class DeviceAirLoadType extends BaseEntity {
private static final long serialVersionUID = 1L;
/** 主键 */
private Long id;
/** 无人机厂商ID */
private Long vendorId;
/** 无人机主类型 */
private Long type;
/** 无人机子类型 */
private Long subType;
/** 负载名称 */
private String loadName;
/** 负载系列 */
private String loadSeries;
/** 负载分类0-负载1-配件 */
private Integer loadCategory;
/** 槽1、2 或者-1-1代表全部槽位可用 */
private Integer slot;
}

View File

@ -0,0 +1,21 @@
package com.ruoyi.device.domain.api;
import com.ruoyi.device.domain.DeviceAirLoadSlot;
import java.util.List;
/**
* 无人机负载槽位领域接口
*
* @author 拓恒
* @date 2026-03-04
*/
public interface IDeviceAirLoadSlotDomain {
/**
* 根据厂商ID主类型子类型查询负载槽位
*
* @return 负载槽位信息
*/
DeviceAirLoadSlot selectDeviceAirLoadSlotByVendorAndType(DeviceAirLoadSlot deviceAirLoadSlot);
}

View File

@ -0,0 +1,23 @@
package com.ruoyi.device.domain.api;
import com.ruoyi.device.domain.DeviceAirLoadType;
import java.util.List;
import java.util.Map;
/**
* 无人机负载类型领域接口
*
* @author 拓恒
* @date 2026-03-04
*/
public interface IDeviceAirLoadTypeDomain {
/**
* 查询无人机负载类型列表
*
* @param deviceAirLoadType 无人机负载类型
* @return 无人机负载类型集合
*/
List<DeviceAirLoadType> selectDeviceAirLoadTypeList(DeviceAirLoadType deviceAirLoadType);
}

View File

@ -0,0 +1,61 @@
package com.ruoyi.device.domain.api;
import com.ruoyi.device.domain.model.DeviceAirTypeGeneralEnum;
import java.util.List;
/**
* 无人机类型通用枚举Domain接口
*
* @author ruoyi
* @date 2026-01-28
*/
public interface IDeviceAirTypeGeneralEnumDomain
{
/**
* 查询无人机类型通用枚举列表
*
* @param airTypeGeneralEnum 无人机类型通用枚举
* @return 无人机类型通用枚举集合
*/
List<DeviceAirTypeGeneralEnum> selectAirTypeGeneralEnumList(DeviceAirTypeGeneralEnum airTypeGeneralEnum);
/**
* 根据主键查询无人机类型通用枚举
*
* @param id 主键
* @return 无人机类型通用枚举
*/
DeviceAirTypeGeneralEnum selectAirTypeGeneralEnumById(Long id);
/**
* 新增无人机类型通用枚举
*
* @param airTypeGeneralEnum 无人机类型通用枚举
* @return 结果
*/
int insertAirTypeGeneralEnum(DeviceAirTypeGeneralEnum airTypeGeneralEnum);
/**
* 修改无人机类型通用枚举
*
* @param airTypeGeneralEnum 无人机类型通用枚举
* @return 结果
*/
int updateAirTypeGeneralEnum(DeviceAirTypeGeneralEnum airTypeGeneralEnum);
/**
* 删除无人机类型通用枚举
*
* @param id 主键
* @return 结果
*/
int deleteAirTypeGeneralEnumById(Long id);
/**
* 批量删除无人机类型通用枚举
*
* @param ids 需要删除的主键集合
* @return 影响行数
*/
int deleteAirTypeGeneralEnumByIds(Long[] ids);
}

View File

@ -35,6 +35,14 @@ public interface IDeviceDomain
*/ */
Device selectDeviceByIotDeviceId(String iotDeviceId); Device selectDeviceByIotDeviceId(String iotDeviceId);
/**
* 根据设备SN号查询设备
*
* @param deviceSn 设备SN号
* @return 设备
*/
Device selectDeviceByDeviceSn(String deviceSn);
/** /**
* 新增设备 * 新增设备
* *

View File

@ -58,4 +58,12 @@ public interface IGroupDomain
* @return 结果 * @return 结果
*/ */
int deleteGroupByGroupIds(Long[] groupIds); int deleteGroupByGroupIds(Long[] groupIds);
/**
* 根据分组名称查询分组
*
* @param groupName 分组名称
* @return 分组
*/
Group selectGroupByGroupName(String groupName);
} }

View File

@ -91,4 +91,67 @@ public interface IThingsBoardDomain {
* @return 子设备ID列表如果网关没有子设备则返回空列表 * @return 子设备ID列表如果网关没有子设备则返回空列表
*/ */
List<String> getGatewayChildDevices(String gatewayDeviceId); List<String> getGatewayChildDevices(String gatewayDeviceId);
/**
* 清除设备属性缓存
* 使 getDeviceAttributes 方法的缓存失效
*
* @param deviceId 设备ID
*/
void evictDeviceAttributesCache(String deviceId);
/**
* 清除设备遥测数据缓存
* 使 getDeviceTelemetry 方法的缓存失效
*
* @param deviceId 设备ID
*/
void evictDeviceTelemetryCache(String deviceId);
/**
* 根据设备ID获取拓恒设备的所有属性
* 只返回已注册的属性键对应的数据未注册的键会被忽略
*
* @param deviceId 设备ID
* @return 类型安全的属性映射
*/
AttributeMap getTuohengDeviceAttributes(String deviceId);
/**
* 根据设备ID获取拓恒设备的所有遥测数据
* 只返回已注册的遥测键对应的数据未注册的键会被忽略
*
* @param deviceId 设备ID
* @return 类型安全的遥测数据映射
*/
TelemetryMap getTuohengDeviceTelemetry(String deviceId);
/**
* 根据设备ID获取拓恒设备的预定义属性
* 只返回在 TuohengDeviceAttributes 中预定义的属性
*
* @param deviceId 设备ID
* @return 类型安全的属性映射只包含预定义的属性
*/
AttributeMap getPredefinedTuohengDeviceAttributes(String deviceId);
/**
* 根据设备ID获取拓恒设备的预定义遥测数据
* 只返回在 TuohengDeviceTelemetry 中预定义的遥测数据
*
* @param deviceId 设备ID
* @return 类型安全的遥测数据映射只包含预定义的遥测数据
*/
TelemetryMap getPredefinedTuohengDeviceTelemetry(String deviceId);
/**
* 设置设备属性
* 将指定的属性键值对保存到 ThingsBoard 设备的 SERVER_SCOPE 属性中
*
* @param deviceId 设备IDThingsBoard iotDeviceId
* @param key 属性键
* @param value 属性值
* @return 是否设置成功
*/
boolean setDeviceAttribute(String deviceId, String key, Object value);
} }

View File

@ -0,0 +1,7 @@
package com.ruoyi.device.domain.api;
import com.ruoyi.device.domain.model.weather.Weather;
public interface IWeatherDomain {
Weather weatherInfo(String lat, String lon,String dockerDeviceIotId);
}

View File

@ -0,0 +1,36 @@
package com.ruoyi.device.domain.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 天气API配置属性
*
* @author ruoyi
*/
@Data
@Component
@ConfigurationProperties(prefix = "weather.api")
public class WeatherProperties {
/**
* 天气API AppCode
*/
private String appcode = "6a152d74a3c249bfa6db6e664f2541f0";
/**
* 天气API Host
*/
private String host = "https://aliv8.data.moji.com";
/**
* 天气API Path
*/
private String path = "/whapi/json/aliweather/condition";
/**
* 天气API Token
*/
private String token = "ff826c205f8f4a59701e64e9e64e01c4";
}

View File

@ -0,0 +1,57 @@
package com.ruoyi.device.domain.convert;
import com.ruoyi.common.core.utils.BaseConvert;
import com.ruoyi.device.domain.DeviceAirLoadSlot;
import com.ruoyi.device.mapper.entity.DeviceAirLoadSlotEntity;
import java.util.List;
/**
* 无人机负载槽位领域层转换器
* Domain Entity Mapper Entity
*
* @author 拓恒
* @date 2026-03-04
*/
public class DeviceAirLoadSlotConvert extends BaseConvert<DeviceAirLoadSlotEntity, DeviceAirLoadSlot>
{
private static final DeviceAirLoadSlotConvert INSTANCE = new DeviceAirLoadSlotConvert();
private DeviceAirLoadSlotConvert() {
super(DeviceAirLoadSlotEntity.class, DeviceAirLoadSlot.class);
}
/**
* Entity Domain
*/
public static DeviceAirLoadSlot from(DeviceAirLoadSlotEntity entity)
{
return INSTANCE.innerFrom(entity);
}
/**
* Domain Entity
*/
public static DeviceAirLoadSlotEntity to(DeviceAirLoadSlot domain)
{
return INSTANCE.innerTo(domain);
}
/**
* Entity List Domain List
*/
public static List<DeviceAirLoadSlot> fromList(List<DeviceAirLoadSlotEntity> entityList)
{
return INSTANCE.innerFromList(entityList);
}
/**
* Domain List Entity List
*/
public static List<DeviceAirLoadSlotEntity> toList(List<DeviceAirLoadSlot> domainList)
{
return INSTANCE.innerToList(domainList);
}
}

View File

@ -0,0 +1,57 @@
package com.ruoyi.device.domain.convert;
import com.ruoyi.common.core.utils.BaseConvert;
import com.ruoyi.device.domain.DeviceAirLoadType;
import com.ruoyi.device.mapper.entity.DeviceAirLoadTypeEntity;
import java.util.List;
/**
* 无人机负载类型领域层转换器
* Domain Entity Mapper Entity
*
* @author 拓恒
* @date 2026-03-04
*/
public class DeviceAirLoadTypeConvert extends BaseConvert<DeviceAirLoadTypeEntity, DeviceAirLoadType>
{
private static final DeviceAirLoadTypeConvert INSTANCE = new DeviceAirLoadTypeConvert();
private DeviceAirLoadTypeConvert() {
super(DeviceAirLoadTypeEntity.class, DeviceAirLoadType.class);
}
/**
* Entity Domain
*/
public static DeviceAirLoadType from(DeviceAirLoadTypeEntity entity)
{
return INSTANCE.innerFrom(entity);
}
/**
* Domain Entity
*/
public static DeviceAirLoadTypeEntity to(DeviceAirLoadType domain)
{
return INSTANCE.innerTo(domain);
}
/**
* Entity List Domain List
*/
public static List<DeviceAirLoadType> fromList(List<DeviceAirLoadTypeEntity> entityList)
{
return INSTANCE.innerFromList(entityList);
}
/**
* Domain List Entity List
*/
public static List<DeviceAirLoadTypeEntity> toList(List<DeviceAirLoadType> domainList)
{
return INSTANCE.innerToList(domainList);
}
}

View File

@ -0,0 +1,57 @@
package com.ruoyi.device.domain.convert;
import com.ruoyi.common.core.utils.BaseConvert;
import com.ruoyi.device.domain.model.DeviceAirTypeGeneralEnum;
import com.ruoyi.device.mapper.entity.DeviceAirTypeGeneralEnumEntity;
import java.util.List;
/**
* 无人机类型通用枚举领域层转换器
* Domain Model Mapper Entity
*
* @author ruoyi
* @date 2026-01-28
*/
public class DeviceAirTypeGeneralEnumConvert extends BaseConvert<DeviceAirTypeGeneralEnum, DeviceAirTypeGeneralEnumEntity>
{
private static final DeviceAirTypeGeneralEnumConvert INSTANCE = new DeviceAirTypeGeneralEnumConvert();
private DeviceAirTypeGeneralEnumConvert() {
super(DeviceAirTypeGeneralEnum.class, DeviceAirTypeGeneralEnumEntity.class);
}
/**
* Model Entity
*/
public static DeviceAirTypeGeneralEnumEntity from(DeviceAirTypeGeneralEnum model)
{
return INSTANCE.innerFrom(model);
}
/**
* Entity Model
*/
public static DeviceAirTypeGeneralEnum to(DeviceAirTypeGeneralEnumEntity entity)
{
return INSTANCE.innerTo(entity);
}
/**
* Model List Entity List
*/
public static List<DeviceAirTypeGeneralEnumEntity> fromList(List<DeviceAirTypeGeneralEnum> modelList)
{
return INSTANCE.innerFromList(modelList);
}
/**
* Entity List Model List
*/
public static List<DeviceAirTypeGeneralEnum> toList(List<DeviceAirTypeGeneralEnumEntity> entityList)
{
return INSTANCE.innerToList(entityList);
}
}

View File

@ -1,10 +1,14 @@
package com.ruoyi.device.domain.impl; package com.ruoyi.device.domain.impl;
import com.ruoyi.device.config.DeviceCacheConfig;
import com.ruoyi.device.domain.api.IAircraftDomain; import com.ruoyi.device.domain.api.IAircraftDomain;
import com.ruoyi.device.domain.convert.AircraftConvert; import com.ruoyi.device.domain.convert.AircraftConvert;
import com.ruoyi.device.domain.model.Aircraft; import com.ruoyi.device.domain.model.Aircraft;
import com.ruoyi.device.mapper.AircraftMapper; import com.ruoyi.device.mapper.AircraftMapper;
import com.ruoyi.device.mapper.entity.AircraftEntity; import com.ruoyi.device.mapper.entity.AircraftEntity;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.List; import java.util.List;
@ -34,6 +38,7 @@ public class AircraftDomainImpl implements IAircraftDomain
} }
@Override @Override
@Cacheable(value = DeviceCacheConfig.AIRCRAFT_CACHE, key = "'id:' + #aircraftId", unless = "#result == null")
public Aircraft selectAircraftByAircraftId(Long aircraftId) public Aircraft selectAircraftByAircraftId(Long aircraftId)
{ {
AircraftEntity entity = aircraftMapper.selectAircraftByAircraftId(aircraftId); AircraftEntity entity = aircraftMapper.selectAircraftByAircraftId(aircraftId);
@ -41,6 +46,7 @@ public class AircraftDomainImpl implements IAircraftDomain
} }
@Override @Override
@Cacheable(value = DeviceCacheConfig.AIRCRAFT_CACHE, key = "'deviceId:' + #deviceId", unless = "#result == null")
public Aircraft selectAircraftByDeviceId(Long deviceId) public Aircraft selectAircraftByDeviceId(Long deviceId)
{ {
AircraftEntity entity = aircraftMapper.selectAircraftByDeviceId(deviceId); AircraftEntity entity = aircraftMapper.selectAircraftByDeviceId(deviceId);
@ -48,6 +54,7 @@ public class AircraftDomainImpl implements IAircraftDomain
} }
@Override @Override
@CacheEvict(value = DeviceCacheConfig.AIRCRAFT_CACHE, allEntries = true)
public int insertAircraft(Aircraft aircraft) public int insertAircraft(Aircraft aircraft)
{ {
AircraftEntity entity = AircraftConvert.to(aircraft); AircraftEntity entity = AircraftConvert.to(aircraft);
@ -58,6 +65,10 @@ public class AircraftDomainImpl implements IAircraftDomain
} }
@Override @Override
@Caching(evict = {
@CacheEvict(value = DeviceCacheConfig.AIRCRAFT_CACHE, key = "'id:' + #aircraft.aircraftId"),
@CacheEvict(value = DeviceCacheConfig.AIRCRAFT_CACHE, key = "'deviceId:' + #aircraft.deviceId")
})
public int updateAircraft(Aircraft aircraft) public int updateAircraft(Aircraft aircraft)
{ {
AircraftEntity entity = AircraftConvert.to(aircraft); AircraftEntity entity = AircraftConvert.to(aircraft);
@ -65,12 +76,14 @@ public class AircraftDomainImpl implements IAircraftDomain
} }
@Override @Override
@CacheEvict(value = DeviceCacheConfig.AIRCRAFT_CACHE, allEntries = true)
public int deleteAircraftByAircraftId(Long aircraftId) public int deleteAircraftByAircraftId(Long aircraftId)
{ {
return aircraftMapper.deleteAircraftByAircraftId(aircraftId); return aircraftMapper.deleteAircraftByAircraftId(aircraftId);
} }
@Override @Override
@CacheEvict(value = DeviceCacheConfig.AIRCRAFT_CACHE, allEntries = true)
public int deleteAircraftByAircraftIds(Long[] aircraftIds) public int deleteAircraftByAircraftIds(Long[] aircraftIds)
{ {
return aircraftMapper.deleteAircraftByAircraftIds(aircraftIds); return aircraftMapper.deleteAircraftByAircraftIds(aircraftIds);

View File

@ -1,10 +1,14 @@
package com.ruoyi.device.domain.impl; package com.ruoyi.device.domain.impl;
import com.ruoyi.device.config.DeviceCacheConfig;
import com.ruoyi.device.domain.api.IAircraftPayloadDomain; import com.ruoyi.device.domain.api.IAircraftPayloadDomain;
import com.ruoyi.device.domain.convert.AircraftPayloadConvert; import com.ruoyi.device.domain.convert.AircraftPayloadConvert;
import com.ruoyi.device.domain.model.AircraftPayload; import com.ruoyi.device.domain.model.AircraftPayload;
import com.ruoyi.device.mapper.AircraftPayloadMapper; import com.ruoyi.device.mapper.AircraftPayloadMapper;
import com.ruoyi.device.mapper.entity.AircraftPayloadEntity; import com.ruoyi.device.mapper.entity.AircraftPayloadEntity;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.List; import java.util.List;
@ -34,6 +38,7 @@ public class AircraftPayloadDomainImpl implements IAircraftPayloadDomain
} }
@Override @Override
@Cacheable(value = DeviceCacheConfig.AIRCRAFT_PAYLOAD_CACHE, key = "'id:' + #id", unless = "#result == null")
public AircraftPayload selectAircraftPayloadById(Long id) public AircraftPayload selectAircraftPayloadById(Long id)
{ {
AircraftPayloadEntity entity = aircraftPayloadMapper.selectAircraftPayloadById(id); AircraftPayloadEntity entity = aircraftPayloadMapper.selectAircraftPayloadById(id);
@ -41,6 +46,7 @@ public class AircraftPayloadDomainImpl implements IAircraftPayloadDomain
} }
@Override @Override
@Cacheable(value = DeviceCacheConfig.AIRCRAFT_PAYLOAD_CACHE, key = "'aircraftId:' + #aircraftId", unless = "#result == null")
public List<AircraftPayload> selectAircraftPayloadByAircraftId(Long aircraftId) public List<AircraftPayload> selectAircraftPayloadByAircraftId(Long aircraftId)
{ {
List<AircraftPayloadEntity> entityList = aircraftPayloadMapper.selectAircraftPayloadListByAircraftId(aircraftId); List<AircraftPayloadEntity> entityList = aircraftPayloadMapper.selectAircraftPayloadListByAircraftId(aircraftId);
@ -48,6 +54,7 @@ public class AircraftPayloadDomainImpl implements IAircraftPayloadDomain
} }
@Override @Override
@Cacheable(value = DeviceCacheConfig.AIRCRAFT_PAYLOAD_CACHE, key = "'payloadId:' + #payloadId", unless = "#result == null")
public List<AircraftPayload> selectAircraftPayloadByPayloadId(Long payloadId) public List<AircraftPayload> selectAircraftPayloadByPayloadId(Long payloadId)
{ {
List<AircraftPayloadEntity> entityList = aircraftPayloadMapper.selectAircraftPayloadListByPayloadId(payloadId); List<AircraftPayloadEntity> entityList = aircraftPayloadMapper.selectAircraftPayloadListByPayloadId(payloadId);
@ -55,6 +62,7 @@ public class AircraftPayloadDomainImpl implements IAircraftPayloadDomain
} }
@Override @Override
@Cacheable(value = DeviceCacheConfig.AIRCRAFT_PAYLOAD_CACHE, key = "'dockId:' + #dockId", unless = "#result == null")
public List<AircraftPayload> selectAircraftPayloadByDockId(Long dockId) public List<AircraftPayload> selectAircraftPayloadByDockId(Long dockId)
{ {
List<AircraftPayloadEntity> entityList = aircraftPayloadMapper.selectAircraftPayloadListByDockId(dockId); List<AircraftPayloadEntity> entityList = aircraftPayloadMapper.selectAircraftPayloadListByDockId(dockId);
@ -62,6 +70,7 @@ public class AircraftPayloadDomainImpl implements IAircraftPayloadDomain
} }
@Override @Override
@CacheEvict(value = DeviceCacheConfig.AIRCRAFT_PAYLOAD_CACHE, allEntries = true)
public int insertAircraftPayload(AircraftPayload aircraftPayload) public int insertAircraftPayload(AircraftPayload aircraftPayload)
{ {
AircraftPayloadEntity entity = AircraftPayloadConvert.to(aircraftPayload); AircraftPayloadEntity entity = AircraftPayloadConvert.to(aircraftPayload);
@ -72,6 +81,7 @@ public class AircraftPayloadDomainImpl implements IAircraftPayloadDomain
} }
@Override @Override
@CacheEvict(value = DeviceCacheConfig.AIRCRAFT_PAYLOAD_CACHE, allEntries = true)
public int updateAircraftPayload(AircraftPayload aircraftPayload) public int updateAircraftPayload(AircraftPayload aircraftPayload)
{ {
AircraftPayloadEntity entity = AircraftPayloadConvert.to(aircraftPayload); AircraftPayloadEntity entity = AircraftPayloadConvert.to(aircraftPayload);
@ -79,12 +89,14 @@ public class AircraftPayloadDomainImpl implements IAircraftPayloadDomain
} }
@Override @Override
@CacheEvict(value = DeviceCacheConfig.AIRCRAFT_PAYLOAD_CACHE, allEntries = true)
public int deleteAircraftPayloadById(Long id) public int deleteAircraftPayloadById(Long id)
{ {
return aircraftPayloadMapper.deleteAircraftPayloadById(id); return aircraftPayloadMapper.deleteAircraftPayloadById(id);
} }
@Override @Override
@CacheEvict(value = DeviceCacheConfig.AIRCRAFT_PAYLOAD_CACHE, allEntries = true)
public int deleteAircraftPayloadByIds(Long[] ids) public int deleteAircraftPayloadByIds(Long[] ids)
{ {
return aircraftPayloadMapper.deleteAircraftPayloadByIds(ids); return aircraftPayloadMapper.deleteAircraftPayloadByIds(ids);

View File

@ -0,0 +1,41 @@
package com.ruoyi.device.domain.impl;
import com.ruoyi.device.domain.DeviceAirLoadSlot;
import com.ruoyi.device.domain.api.IDeviceAirLoadSlotDomain;
import com.ruoyi.device.domain.convert.DeviceAirLoadSlotConvert;
import com.ruoyi.device.domain.convert.DeviceAirLoadTypeConvert;
import com.ruoyi.device.mapper.DeviceAirLoadSlotMapper;
import com.ruoyi.device.mapper.entity.DeviceAirLoadSlotEntity;
import com.ruoyi.device.mapper.entity.DeviceAirLoadTypeEntity;
import com.ruoyi.device.service.dto.DeviceAirLoadSlotDTO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* 无人机负载槽位Domain实现类
*
* @author 拓恒
* @date 2026-03-04
*/
@Repository
public class DeviceAirLoadSlotDomainImpl implements IDeviceAirLoadSlotDomain {
@Autowired
private DeviceAirLoadSlotMapper deviceAirLoadSlotMapper;
@Override
public DeviceAirLoadSlot selectDeviceAirLoadSlotByVendorAndType(DeviceAirLoadSlot deviceAirLoadSlot) {
DeviceAirLoadSlotEntity entity = DeviceAirLoadSlotConvert.to(deviceAirLoadSlot);
DeviceAirLoadSlotEntity result = deviceAirLoadSlotMapper.selectDeviceAirLoadSlotByVendorAndType(entity);
if (result == null) {
// 设置默认值
result = new DeviceAirLoadSlotEntity();
result.setSlotCount(0);
result.setLoadLimit(0);
result.setAccessoryLimit(0);
}
return DeviceAirLoadSlotConvert.from(result);
}
}

View File

@ -0,0 +1,35 @@
package com.ruoyi.device.domain.impl;
import com.ruoyi.device.domain.DeviceAirLoadType;
import com.ruoyi.device.domain.api.IDeviceAirLoadTypeDomain;
import com.ruoyi.device.domain.convert.DeviceAirLoadTypeConvert;
import com.ruoyi.device.mapper.DeviceAirLoadTypeMapper;
import com.ruoyi.device.mapper.entity.DeviceAirLoadTypeEntity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 无人机负载类型Domain实现类
*
* @author 拓恒
* @date 2026-03-04
*/
@Repository
public class DeviceAirLoadTypeDomainImpl implements IDeviceAirLoadTypeDomain {
@Autowired
private DeviceAirLoadTypeMapper deviceAirLoadTypeMapper;
@Override
public List<DeviceAirLoadType> selectDeviceAirLoadTypeList(DeviceAirLoadType deviceAirLoadType) {
DeviceAirLoadTypeEntity entity = DeviceAirLoadTypeConvert.to(deviceAirLoadType);
List<DeviceAirLoadTypeEntity> entityList = deviceAirLoadTypeMapper.selectDeviceAirLoadTypeList(entity);
return DeviceAirLoadTypeConvert.fromList(entityList);
}
}

View File

@ -0,0 +1,65 @@
package com.ruoyi.device.domain.impl;
import com.ruoyi.device.domain.api.IDeviceAirTypeGeneralEnumDomain;
import com.ruoyi.device.domain.convert.DeviceAirTypeGeneralEnumConvert;
import com.ruoyi.device.domain.model.DeviceAirTypeGeneralEnum;
import com.ruoyi.device.mapper.DeviceAirTypeGeneralEnumMapper;
import com.ruoyi.device.mapper.entity.DeviceAirTypeGeneralEnumEntity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* 无人机类型通用枚举Domain实现类
*
* @author ruoyi
* @date 2026-01-28
*/
@Repository
public class DeviceAirTypeGeneralEnumDomainImpl implements IDeviceAirTypeGeneralEnumDomain
{
@Autowired
private DeviceAirTypeGeneralEnumMapper airTypeGeneralEnumMapper;
@Override
public List<DeviceAirTypeGeneralEnum> selectAirTypeGeneralEnumList(DeviceAirTypeGeneralEnum airTypeGeneralEnum)
{
DeviceAirTypeGeneralEnumEntity entity = DeviceAirTypeGeneralEnumConvert.from(airTypeGeneralEnum);
List<DeviceAirTypeGeneralEnumEntity> entityList = airTypeGeneralEnumMapper.selectAirTypeGeneralEnumList(entity);
return DeviceAirTypeGeneralEnumConvert.toList(entityList);
}
@Override
public DeviceAirTypeGeneralEnum selectAirTypeGeneralEnumById(Long id)
{
DeviceAirTypeGeneralEnumEntity entity = airTypeGeneralEnumMapper.selectAirTypeGeneralEnumById(id);
return DeviceAirTypeGeneralEnumConvert.to(entity);
}
@Override
public int insertAirTypeGeneralEnum(DeviceAirTypeGeneralEnum airTypeGeneralEnum)
{
DeviceAirTypeGeneralEnumEntity entity = DeviceAirTypeGeneralEnumConvert.from(airTypeGeneralEnum);
return airTypeGeneralEnumMapper.insertAirTypeGeneralEnum(entity);
}
@Override
public int updateAirTypeGeneralEnum(DeviceAirTypeGeneralEnum airTypeGeneralEnum)
{
DeviceAirTypeGeneralEnumEntity entity = DeviceAirTypeGeneralEnumConvert.from(airTypeGeneralEnum);
return airTypeGeneralEnumMapper.updateAirTypeGeneralEnum(entity);
}
@Override
public int deleteAirTypeGeneralEnumById(Long id)
{
return airTypeGeneralEnumMapper.deleteAirTypeGeneralEnumById(id);
}
@Override
public int deleteAirTypeGeneralEnumByIds(Long[] ids)
{
return airTypeGeneralEnumMapper.deleteAirTypeGeneralEnumByIds(ids);
}
}

View File

@ -1,10 +1,15 @@
package com.ruoyi.device.domain.impl; package com.ruoyi.device.domain.impl;
import com.ruoyi.device.config.DeviceCacheConfig;
import com.ruoyi.device.domain.api.IDeviceDomain; import com.ruoyi.device.domain.api.IDeviceDomain;
import com.ruoyi.device.domain.convert.DeviceConvert; import com.ruoyi.device.domain.convert.DeviceConvert;
import com.ruoyi.device.domain.model.Device; import com.ruoyi.device.domain.model.Device;
import com.ruoyi.device.mapper.DeviceMapper; import com.ruoyi.device.mapper.DeviceMapper;
import com.ruoyi.device.mapper.entity.DeviceEntity; import com.ruoyi.device.mapper.entity.DeviceEntity;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.List; import java.util.List;
@ -15,6 +20,7 @@ import java.util.List;
* @author ruoyi * @author ruoyi
* @date 2026-01-16 * @date 2026-01-16
*/ */
@Slf4j
@Component @Component
public class DeviceDomainImpl implements IDeviceDomain public class DeviceDomainImpl implements IDeviceDomain
{ {
@ -34,6 +40,7 @@ public class DeviceDomainImpl implements IDeviceDomain
} }
@Override @Override
@Cacheable(value = DeviceCacheConfig.DEVICE_CACHE, key = "'id:' + #deviceId", unless = "#result == null")
public Device selectDeviceByDeviceId(Long deviceId) public Device selectDeviceByDeviceId(Long deviceId)
{ {
DeviceEntity entity = deviceMapper.selectDeviceByDeviceId(deviceId); DeviceEntity entity = deviceMapper.selectDeviceByDeviceId(deviceId);
@ -41,6 +48,7 @@ public class DeviceDomainImpl implements IDeviceDomain
} }
@Override @Override
@Cacheable(value = DeviceCacheConfig.DEVICE_CACHE, key = "'iotId:' + #iotDeviceId", unless = "#result == null")
public Device selectDeviceByIotDeviceId(String iotDeviceId) public Device selectDeviceByIotDeviceId(String iotDeviceId)
{ {
DeviceEntity entity = deviceMapper.selectDeviceByIotDeviceId(iotDeviceId); DeviceEntity entity = deviceMapper.selectDeviceByIotDeviceId(iotDeviceId);
@ -48,6 +56,18 @@ public class DeviceDomainImpl implements IDeviceDomain
} }
@Override @Override
@Cacheable(value = DeviceCacheConfig.DEVICE_CACHE, key = "'sn:' + #deviceSn", unless = "#result == null")
public Device selectDeviceByDeviceSn(String deviceSn)
{
log.info("查询设备 by device_sn: {}", deviceSn);
DeviceEntity entity = deviceMapper.selectDeviceByDeviceSn(deviceSn);
Device result = DeviceConvert.from(entity);
log.info("查询设备结果: device_sn={}, result={}", deviceSn, result != null ? result.getDeviceId() : "null");
return result;
}
@Override
@CacheEvict(value = DeviceCacheConfig.DEVICE_CACHE, allEntries = true)
public int insertDevice(Device device) public int insertDevice(Device device)
{ {
DeviceEntity entity = DeviceConvert.to(device); DeviceEntity entity = DeviceConvert.to(device);
@ -58,6 +78,11 @@ public class DeviceDomainImpl implements IDeviceDomain
} }
@Override @Override
@Caching(evict = {
@CacheEvict(value = DeviceCacheConfig.DEVICE_CACHE, key = "'id:' + #device.deviceId"),
@CacheEvict(value = DeviceCacheConfig.DEVICE_CACHE, key = "'iotId:' + #device.iotDeviceId"),
@CacheEvict(value = DeviceCacheConfig.DEVICE_CACHE, key = "'sn:' + #device.deviceSn")
})
public int updateDevice(Device device) public int updateDevice(Device device)
{ {
DeviceEntity entity = DeviceConvert.to(device); DeviceEntity entity = DeviceConvert.to(device);
@ -65,12 +90,14 @@ public class DeviceDomainImpl implements IDeviceDomain
} }
@Override @Override
@CacheEvict(value = DeviceCacheConfig.DEVICE_CACHE, allEntries = true)
public int deleteDeviceByDeviceId(Long deviceId) public int deleteDeviceByDeviceId(Long deviceId)
{ {
return deviceMapper.deleteDeviceByDeviceId(deviceId); return deviceMapper.deleteDeviceByDeviceId(deviceId);
} }
@Override @Override
@CacheEvict(value = DeviceCacheConfig.DEVICE_CACHE, allEntries = true)
public int deleteDeviceByDeviceIds(Long[] deviceIds) public int deleteDeviceByDeviceIds(Long[] deviceIds)
{ {
return deviceMapper.deleteDeviceByDeviceIds(deviceIds); return deviceMapper.deleteDeviceByDeviceIds(deviceIds);

View File

@ -1,10 +1,14 @@
package com.ruoyi.device.domain.impl; package com.ruoyi.device.domain.impl;
import com.ruoyi.device.config.DeviceCacheConfig;
import com.ruoyi.device.domain.api.IDockAircraftDomain; import com.ruoyi.device.domain.api.IDockAircraftDomain;
import com.ruoyi.device.domain.convert.DockAircraftConvert; import com.ruoyi.device.domain.convert.DockAircraftConvert;
import com.ruoyi.device.domain.model.DockAircraft; import com.ruoyi.device.domain.model.DockAircraft;
import com.ruoyi.device.mapper.DockAircraftMapper; import com.ruoyi.device.mapper.DockAircraftMapper;
import com.ruoyi.device.mapper.entity.DockAircraftEntity; import com.ruoyi.device.mapper.entity.DockAircraftEntity;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.List; import java.util.List;
@ -34,6 +38,7 @@ public class DockAircraftDomainImpl implements IDockAircraftDomain
} }
@Override @Override
@Cacheable(value = DeviceCacheConfig.DOCK_AIRCRAFT_CACHE, key = "'id:' + #id", unless = "#result == null")
public DockAircraft selectDockAircraftById(Long id) public DockAircraft selectDockAircraftById(Long id)
{ {
DockAircraftEntity entity = dockAircraftMapper.selectDockAircraftById(id); DockAircraftEntity entity = dockAircraftMapper.selectDockAircraftById(id);
@ -41,6 +46,7 @@ public class DockAircraftDomainImpl implements IDockAircraftDomain
} }
@Override @Override
@Cacheable(value = DeviceCacheConfig.DOCK_AIRCRAFT_CACHE, key = "'dockId:' + #dockId", unless = "#result == null")
public List<DockAircraft> selectDockAircraftByDockId(Long dockId) public List<DockAircraft> selectDockAircraftByDockId(Long dockId)
{ {
List<DockAircraftEntity> entityList = dockAircraftMapper.selectDockAircraftListByDockId(dockId); List<DockAircraftEntity> entityList = dockAircraftMapper.selectDockAircraftListByDockId(dockId);
@ -48,6 +54,7 @@ public class DockAircraftDomainImpl implements IDockAircraftDomain
} }
@Override @Override
@Cacheable(value = DeviceCacheConfig.DOCK_AIRCRAFT_CACHE, key = "'aircraftId:' + #aircraftId", unless = "#result == null")
public List<DockAircraft> selectDockAircraftByAircraftId(Long aircraftId) public List<DockAircraft> selectDockAircraftByAircraftId(Long aircraftId)
{ {
List<DockAircraftEntity> entityList = dockAircraftMapper.selectDockAircraftListByAircraftId(aircraftId); List<DockAircraftEntity> entityList = dockAircraftMapper.selectDockAircraftListByAircraftId(aircraftId);
@ -55,6 +62,7 @@ public class DockAircraftDomainImpl implements IDockAircraftDomain
} }
@Override @Override
@CacheEvict(value = DeviceCacheConfig.DOCK_AIRCRAFT_CACHE, allEntries = true)
public int insertDockAircraft(DockAircraft dockAircraft) public int insertDockAircraft(DockAircraft dockAircraft)
{ {
DockAircraftEntity entity = DockAircraftConvert.to(dockAircraft); DockAircraftEntity entity = DockAircraftConvert.to(dockAircraft);
@ -65,6 +73,11 @@ public class DockAircraftDomainImpl implements IDockAircraftDomain
} }
@Override @Override
@Caching(evict = {
@CacheEvict(value = DeviceCacheConfig.DOCK_AIRCRAFT_CACHE, key = "'id:' + #dockAircraft.id"),
@CacheEvict(value = DeviceCacheConfig.DOCK_AIRCRAFT_CACHE, key = "'dockId:' + #dockAircraft.dockId"),
@CacheEvict(value = DeviceCacheConfig.DOCK_AIRCRAFT_CACHE, key = "'aircraftId:' + #dockAircraft.aircraftId")
})
public int updateDockAircraft(DockAircraft dockAircraft) public int updateDockAircraft(DockAircraft dockAircraft)
{ {
DockAircraftEntity entity = DockAircraftConvert.to(dockAircraft); DockAircraftEntity entity = DockAircraftConvert.to(dockAircraft);
@ -72,12 +85,14 @@ public class DockAircraftDomainImpl implements IDockAircraftDomain
} }
@Override @Override
@CacheEvict(value = DeviceCacheConfig.DOCK_AIRCRAFT_CACHE, allEntries = true)
public int deleteDockAircraftById(Long id) public int deleteDockAircraftById(Long id)
{ {
return dockAircraftMapper.deleteDockAircraftById(id); return dockAircraftMapper.deleteDockAircraftById(id);
} }
@Override @Override
@CacheEvict(value = DeviceCacheConfig.DOCK_AIRCRAFT_CACHE, allEntries = true)
public int deleteDockAircraftByIds(Long[] ids) public int deleteDockAircraftByIds(Long[] ids)
{ {
return dockAircraftMapper.deleteDockAircraftByIds(ids); return dockAircraftMapper.deleteDockAircraftByIds(ids);

View File

@ -1,10 +1,14 @@
package com.ruoyi.device.domain.impl; package com.ruoyi.device.domain.impl;
import com.ruoyi.device.config.DeviceCacheConfig;
import com.ruoyi.device.domain.api.IDockDomain; import com.ruoyi.device.domain.api.IDockDomain;
import com.ruoyi.device.domain.convert.DockConvert; import com.ruoyi.device.domain.convert.DockConvert;
import com.ruoyi.device.domain.model.Dock; import com.ruoyi.device.domain.model.Dock;
import com.ruoyi.device.mapper.DockMapper; import com.ruoyi.device.mapper.DockMapper;
import com.ruoyi.device.mapper.entity.DockEntity; import com.ruoyi.device.mapper.entity.DockEntity;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.List; import java.util.List;
@ -34,6 +38,7 @@ public class DockDomainImpl implements IDockDomain
} }
@Override @Override
@Cacheable(value = DeviceCacheConfig.DOCK_CACHE, key = "'id:' + #dockId", unless = "#result == null")
public Dock selectDockByDockId(Long dockId) public Dock selectDockByDockId(Long dockId)
{ {
DockEntity entity = dockMapper.selectDockByDockId(dockId); DockEntity entity = dockMapper.selectDockByDockId(dockId);
@ -41,6 +46,7 @@ public class DockDomainImpl implements IDockDomain
} }
@Override @Override
@Cacheable(value = DeviceCacheConfig.DOCK_CACHE, key = "'deviceId:' + #deviceId", unless = "#result == null")
public Dock selectDockByDeviceId(Long deviceId) public Dock selectDockByDeviceId(Long deviceId)
{ {
DockEntity entity = dockMapper.selectDockByDeviceId(deviceId); DockEntity entity = dockMapper.selectDockByDeviceId(deviceId);
@ -48,6 +54,7 @@ public class DockDomainImpl implements IDockDomain
} }
@Override @Override
@CacheEvict(value = DeviceCacheConfig.DOCK_CACHE, allEntries = true)
public int insertDock(Dock dock) public int insertDock(Dock dock)
{ {
DockEntity entity = DockConvert.to(dock); DockEntity entity = DockConvert.to(dock);
@ -58,6 +65,10 @@ public class DockDomainImpl implements IDockDomain
} }
@Override @Override
@Caching(evict = {
@CacheEvict(value = DeviceCacheConfig.DOCK_CACHE, key = "'id:' + #dock.dockId"),
@CacheEvict(value = DeviceCacheConfig.DOCK_CACHE, key = "'deviceId:' + #dock.deviceId")
})
public int updateDock(Dock dock) public int updateDock(Dock dock)
{ {
DockEntity entity = DockConvert.to(dock); DockEntity entity = DockConvert.to(dock);
@ -65,12 +76,14 @@ public class DockDomainImpl implements IDockDomain
} }
@Override @Override
@CacheEvict(value = DeviceCacheConfig.DOCK_CACHE, allEntries = true)
public int deleteDockByDockId(Long dockId) public int deleteDockByDockId(Long dockId)
{ {
return dockMapper.deleteDockByDockId(dockId); return dockMapper.deleteDockByDockId(dockId);
} }
@Override @Override
@CacheEvict(value = DeviceCacheConfig.DOCK_CACHE, allEntries = true)
public int deleteDockByDockIds(Long[] dockIds) public int deleteDockByDockIds(Long[] dockIds)
{ {
return dockMapper.deleteDockByDockIds(dockIds); return dockMapper.deleteDockByDockIds(dockIds);

View File

@ -68,4 +68,11 @@ public class GroupDomainImpl implements IGroupDomain
{ {
return groupMapper.deleteGroupByGroupIds(groupIds); return groupMapper.deleteGroupByGroupIds(groupIds);
} }
@Override
public Group selectGroupByGroupName(String groupName)
{
GroupEntity entity = groupMapper.selectGroupByGroupName(groupName);
return GroupConvert.from(entity);
}
} }

View File

@ -1,10 +1,13 @@
package com.ruoyi.device.domain.impl; package com.ruoyi.device.domain.impl;
import com.ruoyi.device.config.DeviceCacheConfig;
import com.ruoyi.device.domain.api.IPayloadDomain; import com.ruoyi.device.domain.api.IPayloadDomain;
import com.ruoyi.device.domain.convert.PayloadConvert; import com.ruoyi.device.domain.convert.PayloadConvert;
import com.ruoyi.device.domain.model.Payload; import com.ruoyi.device.domain.model.Payload;
import com.ruoyi.device.mapper.PayloadMapper; import com.ruoyi.device.mapper.PayloadMapper;
import com.ruoyi.device.mapper.entity.PayloadEntity; import com.ruoyi.device.mapper.entity.PayloadEntity;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.List; import java.util.List;
@ -34,6 +37,7 @@ public class PayloadDomainImpl implements IPayloadDomain
} }
@Override @Override
@Cacheable(value = DeviceCacheConfig.PAYLOAD_CACHE, key = "'id:' + #payloadId", unless = "#result == null")
public Payload selectPayloadByPayloadId(Long payloadId) public Payload selectPayloadByPayloadId(Long payloadId)
{ {
PayloadEntity entity = payloadMapper.selectPayloadByPayloadId(payloadId); PayloadEntity entity = payloadMapper.selectPayloadByPayloadId(payloadId);
@ -41,6 +45,7 @@ public class PayloadDomainImpl implements IPayloadDomain
} }
@Override @Override
@CacheEvict(value = DeviceCacheConfig.PAYLOAD_CACHE, allEntries = true)
public int insertPayload(Payload payload) public int insertPayload(Payload payload)
{ {
PayloadEntity entity = PayloadConvert.to(payload); PayloadEntity entity = PayloadConvert.to(payload);
@ -51,6 +56,7 @@ public class PayloadDomainImpl implements IPayloadDomain
} }
@Override @Override
@CacheEvict(value = DeviceCacheConfig.PAYLOAD_CACHE, key = "'id:' + #payload.payloadId")
public int updatePayload(Payload payload) public int updatePayload(Payload payload)
{ {
PayloadEntity entity = PayloadConvert.to(payload); PayloadEntity entity = PayloadConvert.to(payload);
@ -58,12 +64,14 @@ public class PayloadDomainImpl implements IPayloadDomain
} }
@Override @Override
@CacheEvict(value = DeviceCacheConfig.PAYLOAD_CACHE, allEntries = true)
public int deletePayloadByPayloadId(Long payloadId) public int deletePayloadByPayloadId(Long payloadId)
{ {
return payloadMapper.deletePayloadByPayloadId(payloadId); return payloadMapper.deletePayloadByPayloadId(payloadId);
} }
@Override @Override
@CacheEvict(value = DeviceCacheConfig.PAYLOAD_CACHE, allEntries = true)
public int deletePayloadByPayloadIds(Long[] payloadIds) public int deletePayloadByPayloadIds(Long[] payloadIds)
{ {
return payloadMapper.deletePayloadByPayloadIds(payloadIds); return payloadMapper.deletePayloadByPayloadIds(payloadIds);

View File

@ -1,13 +1,18 @@
package com.ruoyi.device.domain.impl; package com.ruoyi.device.domain.impl;
import com.ruoyi.device.config.DeviceCacheConfig;
import com.ruoyi.device.domain.api.IThingsBoardDomain; import com.ruoyi.device.domain.api.IThingsBoardDomain;
import com.ruoyi.device.domain.model.thingsboard.*; import com.ruoyi.device.domain.model.thingsboard.*;
import com.ruoyi.device.domain.model.thingsboard.constants.DeviceAttributes; import com.ruoyi.device.domain.model.thingsboard.constants.DeviceAttributes;
import com.ruoyi.device.domain.model.thingsboard.constants.DeviceTelemetry; import com.ruoyi.device.domain.model.thingsboard.constants.DeviceTelemetry;
import com.ruoyi.device.domain.model.thingsboard.tuoheng.constants.TuohengDeviceAttributes;
import com.ruoyi.device.domain.model.thingsboard.tuoheng.constants.TuohengDeviceTelemetry;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.rest.client.RestClient; import org.thingsboard.rest.client.RestClient;
@ -87,27 +92,25 @@ public class ThingsBoardDomainImpl implements IThingsBoardDomain {
} }
@Override @Override
@Cacheable(value = DeviceCacheConfig.THINGSBOARD_ATTRIBUTES_CACHE, key = "#deviceId", unless = "#result == null || #result.isEmpty()")
public AttributeMap getDeviceAttributes(String deviceId) { public AttributeMap getDeviceAttributes(String deviceId) {
AttributeMap attributeMap = new AttributeMap(); AttributeMap attributeMap = new AttributeMap();
try { try {
DeviceId id = new DeviceId(UUID.fromString(deviceId)); DeviceId deviceIdObj = new DeviceId(UUID.fromString(deviceId));
// 获取所有属性键 List<String> attributeKeys = client.getAttributeKeys(deviceIdObj);
List<String> attributeKeys = client.getAttributeKeys(id);
if (attributeKeys == null || attributeKeys.isEmpty()) { if (attributeKeys == null || attributeKeys.isEmpty()) {
log.debug("设备 {} 没有属性", deviceId); log.debug("设备 {} 没有属性", deviceId);
return attributeMap; return attributeMap;
} }
// 获取属性值 List<AttributeKvEntry> attributeKvEntries = client.getAttributeKvEntries(deviceIdObj, attributeKeys);
List<AttributeKvEntry> attributeKvEntries = client.getAttributeKvEntries(id, attributeKeys);
if (attributeKvEntries == null || attributeKvEntries.isEmpty()) { if (attributeKvEntries == null || attributeKvEntries.isEmpty()) {
log.debug("设备 {} 的属性值为空", deviceId); log.debug("设备 {} 的属性值为空", deviceId);
return attributeMap; return attributeMap;
} }
// 解析并填充到AttributeMap
for (AttributeKvEntry entry : attributeKvEntries) { for (AttributeKvEntry entry : attributeKvEntries) {
parseAndPutAttribute(attributeMap, entry); parseAndPutAttribute(attributeMap, entry);
} }
@ -120,27 +123,25 @@ public class ThingsBoardDomainImpl implements IThingsBoardDomain {
} }
@Override @Override
@Cacheable(value = DeviceCacheConfig.THINGSBOARD_TELEMETRY_CACHE, key = "#deviceId", unless = "#result == null || #result.isEmpty()")
public TelemetryMap getDeviceTelemetry(String deviceId) { public TelemetryMap getDeviceTelemetry(String deviceId) {
TelemetryMap telemetryMap = new TelemetryMap(); TelemetryMap telemetryMap = new TelemetryMap();
try { try {
DeviceId id = new DeviceId(UUID.fromString(deviceId)); DeviceId deviceIdObj = new DeviceId(UUID.fromString(deviceId));
// 获取所有遥测键 List<String> timeseriesKeys = client.getTimeseriesKeys(deviceIdObj);
List<String> timeseriesKeys = client.getTimeseriesKeys(id);
if (timeseriesKeys == null || timeseriesKeys.isEmpty()) { if (timeseriesKeys == null || timeseriesKeys.isEmpty()) {
log.debug("设备 {} 没有遥测数据", deviceId); log.debug("设备 {} 没有遥测数据", deviceId);
return telemetryMap; return telemetryMap;
} }
// 获取最新的遥测数据 List<TsKvEntry> latestTimeseries = client.getLatestTimeseries(deviceIdObj, timeseriesKeys);
List<TsKvEntry> latestTimeseries = client.getLatestTimeseries(id, timeseriesKeys);
if (latestTimeseries == null || latestTimeseries.isEmpty()) { if (latestTimeseries == null || latestTimeseries.isEmpty()) {
log.debug("设备 {} 的遥测数据为空", deviceId); log.debug("设备 {} 的遥测数据为空", deviceId);
return telemetryMap; return telemetryMap;
} }
// 解析并填充到TelemetryMap
for (TsKvEntry entry : latestTimeseries) { for (TsKvEntry entry : latestTimeseries) {
parseAndPutTelemetry(telemetryMap, entry); parseAndPutTelemetry(telemetryMap, entry);
} }
@ -330,5 +331,179 @@ public class ThingsBoardDomainImpl implements IThingsBoardDomain {
} }
} }
@Override
@CacheEvict(value = DeviceCacheConfig.THINGSBOARD_ATTRIBUTES_CACHE, key = "#deviceId")
public void evictDeviceAttributesCache(String deviceId) {
// 空实现仅用于清除缓存
}
@Override
@CacheEvict(value = DeviceCacheConfig.THINGSBOARD_TELEMETRY_CACHE, key = "#deviceId")
public void evictDeviceTelemetryCache(String deviceId) {
// 空实现仅用于清除缓存
}
@Override
@Cacheable(value = DeviceCacheConfig.THINGSBOARD_ATTRIBUTES_CACHE, key = "#deviceId", unless = "#result == null || #result.isEmpty()")
public AttributeMap getTuohengDeviceAttributes(String deviceId) {
AttributeMap attributeMap = new AttributeMap();
try {
DeviceId deviceIdObj = new DeviceId(UUID.fromString(deviceId));
List<String> attributeKeys = client.getAttributeKeys(deviceIdObj);
if (attributeKeys == null || attributeKeys.isEmpty()) {
log.debug("拓恒设备 {} 没有属性", deviceId);
return attributeMap;
}
List<AttributeKvEntry> attributeKvEntries = client.getAttributeKvEntries(deviceIdObj, attributeKeys);
if (attributeKvEntries == null || attributeKvEntries.isEmpty()) {
log.debug("拓恒设备 {} 的属性值为空", deviceId);
return attributeMap;
}
for (AttributeKvEntry entry : attributeKvEntries) {
parseAndPutAttribute(attributeMap, entry);
}
} catch (Exception e) {
log.error("获取拓恒设备属性失败: deviceId={}, error={}", deviceId, e.getMessage(), e);
}
return attributeMap;
}
@Override
@Cacheable(value = DeviceCacheConfig.THINGSBOARD_TELEMETRY_CACHE, key = "#deviceId", unless = "#result == null || #result.isEmpty()")
public TelemetryMap getTuohengDeviceTelemetry(String deviceId) {
TelemetryMap telemetryMap = new TelemetryMap();
try {
DeviceId deviceIdObj = new DeviceId(UUID.fromString(deviceId));
List<String> timeseriesKeys = client.getTimeseriesKeys(deviceIdObj);
if (timeseriesKeys == null || timeseriesKeys.isEmpty()) {
log.debug("拓恒设备 {} 没有遥测数据", deviceId);
return telemetryMap;
}
List<TsKvEntry> latestTimeseries = client.getLatestTimeseries(deviceIdObj, timeseriesKeys);
if (latestTimeseries == null || latestTimeseries.isEmpty()) {
log.debug("拓恒设备 {} 的遥测数据为空", deviceId);
return telemetryMap;
}
for (TsKvEntry entry : latestTimeseries) {
parseAndPutTelemetry(telemetryMap, entry);
}
} catch (Exception e) {
log.error("获取拓恒设备遥测数据失败: deviceId={}, error={}", deviceId, e.getMessage(), e);
}
return telemetryMap;
}
@Override
public AttributeMap getPredefinedTuohengDeviceAttributes(String deviceId) {
// 先获取所有属性已经处理了异常情况
AttributeMap allAttributes = getTuohengDeviceAttributes(deviceId);
// 创建新的 AttributeMap 只包含预定义的键
AttributeMap predefinedAttributes = new AttributeMap();
// 获取预定义的键名称集合
List<String> predefinedKeyNames = TuohengDeviceAttributes.getPredefinedKeys()
.stream()
.map(AttributeKey::getName)
.toList();
// 过滤只保留预定义的键
for (AttributeKey<?> key : allAttributes.keySet()) {
if (predefinedKeyNames.contains(key.getName())) {
// 复制到新的 map
allAttributes.get(key).ifPresent(value -> {
@SuppressWarnings("unchecked")
AttributeKey<Object> objKey = (AttributeKey<Object>) key;
predefinedAttributes.put(objKey, value);
});
}
}
return predefinedAttributes;
}
@Override
public TelemetryMap getPredefinedTuohengDeviceTelemetry(String deviceId) {
// 先获取所有遥测数据已经处理了 null 值问题
TelemetryMap allTelemetry = getTuohengDeviceTelemetry(deviceId);
// 创建新的 TelemetryMap 只包含预定义的键
TelemetryMap predefinedTelemetry = new TelemetryMap();
// 获取预定义的键名称集合
List<String> predefinedKeyNames = TuohengDeviceTelemetry.getPredefinedKeys()
.stream()
.map(TelemetryKey::getName)
.toList();
// 过滤只保留预定义的键
for (TelemetryKey<?> key : allTelemetry.keySet()) {
if (predefinedKeyNames.contains(key.getName())) {
// 复制到新的 map
allTelemetry.get(key).ifPresent(telemetryValue -> {
@SuppressWarnings("unchecked")
TelemetryKey<Object> objKey = (TelemetryKey<Object>) key;
predefinedTelemetry.put(objKey, telemetryValue.getValue(), telemetryValue.getTimestamp());
});
}
}
return predefinedTelemetry;
}
@Override
@CacheEvict(value = DeviceCacheConfig.THINGSBOARD_ATTRIBUTES_CACHE,
allEntries = false,
key = "#deviceId")
public boolean setDeviceAttribute(String deviceId, String key, Object value) {
try {
log.info("设置设备属性: deviceId={}, key={}, value={}", deviceId, key, value);
// deviceId 字符串转换为 DeviceId 对象
DeviceId deviceIdObj = new DeviceId(UUID.fromString(deviceId));
// 构建 JsonNode 对象
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
com.fasterxml.jackson.databind.node.ObjectNode jsonNode = mapper.createObjectNode();
// 根据值的类型设置到 JsonNode
if (value instanceof String) {
jsonNode.put(key, (String) value);
} else if (value instanceof Integer) {
jsonNode.put(key, (Integer) value);
} else if (value instanceof Long) {
jsonNode.put(key, (Long) value);
} else if (value instanceof Double) {
jsonNode.put(key, (Double) value);
} else if (value instanceof Boolean) {
jsonNode.put(key, (Boolean) value);
} else {
jsonNode.put(key, value.toString());
}
// 调用 ThingsBoard REST API 保存属性
// saveDeviceAttributes 方法会将属性保存到 SERVER_SCOPE
client.saveDeviceAttributes(deviceIdObj, "SERVER_SCOPE", jsonNode);
log.info("设备属性设置成功: deviceId={}, key={}", deviceId, key);
return true;
} catch (Exception e) {
log.error("设置设备属性失败: deviceId=, key={}, error={}",
deviceId, key, e.getMessage(), e);
return false;
}
}
} }

View File

@ -0,0 +1,209 @@
package com.ruoyi.device.domain.impl;
import com.alibaba.fastjson2.JSON;
import com.ruoyi.device.config.DeviceCacheConfig;
import com.ruoyi.device.domain.api.IWeatherDomain;
import com.ruoyi.device.domain.impl.weather.HttpUtils;
import com.ruoyi.device.domain.model.weather.Weather;
import com.ruoyi.device.domain.model.weather.WeatherResponse;
import com.ruoyi.device.domain.config.WeatherProperties;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpResponse;
import org.apache.http.util.EntityUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
@Component
@Slf4j
public class WeatherDomainImpl implements IWeatherDomain {
@Autowired
private WeatherProperties weatherProperties;
@Override
@Cacheable(value = DeviceCacheConfig.WEATHER_CACHE, key = "#dockerDeviceIotId", unless = "#result == null")
public Weather weatherInfo(String lat, String lon, String dockerDeviceIotId) {
log.info("开始获取天气信息 - lat: {}, lon: {}", lat, lon);
String host = weatherProperties.getHost();
String path = weatherProperties.getPath();
String method = "POST";
Map<String, String> headers = new HashMap<>();
headers.put("Authorization", "APPCODE " + weatherProperties.getAppcode());
headers.put("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
Map<String, String> querys = new HashMap<>();
Map<String, String> bodys = new HashMap<>();
bodys.put("lat", lat);
bodys.put("lon", lon);
bodys.put("token", weatherProperties.getToken());
log.info("天气API配置 - host: {}, path: {}, token: {}", host, path, weatherProperties.getToken());
Weather weather = new Weather();
try {
HttpResponse response = HttpUtils.doPost(host, path, method, headers, querys, bodys);
String json = EntityUtils.toString(response.getEntity());
log.info("天气API原始响应 - json: {}", json);
WeatherResponse weatherResponse = JSON.parseObject(json, WeatherResponse.class);
log.info("天气API解析结果 - weatherResponse: {}, code: {}",
weatherResponse != null ? "not null" : "null",
weatherResponse != null ? weatherResponse.getCode() : "N/A");
if (weatherResponse != null && weatherResponse.getCode() == 0) {
String windLevel = weatherResponse.getData().getCondition().getWindLevel();
weather.setWindSpeed(convertWindLevelToSpeed(windLevel));
weather.setEnvironmentTemperature(Double.valueOf(weatherResponse.getData().getCondition().getTemp()));
weather.setEnvironmentHumidity(Double.valueOf(weatherResponse.getData().getCondition().getHumidity()));
String conditionId = weatherResponse.getData().getCondition().getConditionId();
weather.setRainfall(convertConditionIdToRainfall(conditionId));
log.info("天气数据解析成功 - windSpeed: {}, temp: {}, humidity: {}, rainfall: {}",
weather.getWindSpeed(), weather.getEnvironmentTemperature(),
weather.getEnvironmentHumidity(), weather.getRainfall());
} else {
log.warn("天气API返回异常 - weatherResponse: {}, code: {}",
JSON.toJSONString(weatherResponse),
weatherResponse != null ? weatherResponse.getCode() : "null");
}
log.info("lat {} log {} weather {}", lat, lon, JSON.toJSONString(weather));
return weather;
} catch (Exception e) {
log.error("获取天气信息异常 - lat: {}, lon: {}", lat, lon, e);
return null;
}
}
/**
* 根据天气状况ID转换为降雨量等级
* @param conditionId 天气状况ID
* @return 降雨量等级0-无雨1-小雨/小雪2-中雨/中雪3-大雨/大雪
*/
private static Double convertConditionIdToRainfall(String conditionId) {
if (conditionId == null || conditionId.isEmpty()) {
return 0.0; // 默认无雨
}
try {
int id = Integer.parseInt(conditionId);
// 小雨相关阵雨小阵雨局部阵雨小雨小雨夹雪小到中雨
if (id == 15 || id == 16 || id == 17 || id == 18 || id == 19 ||
id == 20 || id == 21 || id == 22 ||
id == 51 || id == 52 || id == 66 ||
id == 86 || id == 91) {
return 1.0; // 小雨
}
// 小雪相关阵雪小阵雪小雪小到中雪
if (id == 24 || id == 25 ||
id == 58 || id == 59 || id == 71 || id == 72 || id == 73 ||
id == 94) {
return 1.0; // 小雪
}
// 中雨相关中雨中到大雨
if (id == 53 || id == 67 || id == 92) {
return 2.0; // 中雨
}
// 中雪相关中雪
if (id == 60 || id == 61) {
return 2.0; // 中雪
}
// 大雨相关强阵雨大雨暴雨大暴雨特大暴雨大到暴雨
if (id == 23 || id == 54 || id == 55 || id == 56 || id == 57 ||
id == 68 || id == 69 || id == 70 || id == 93) {
return 3.0; // 大雨
}
// 大雪相关大雪暴雪
if (id == 62 || id == 63 || id == 74 || id == 75 || id == 76) {
return 3.0; // 大雪
}
// 雷雨相关雷阵雨雷电雷暴雷阵雨伴有冰雹
if (id == 37 || id == 38 || id == 39 || id == 40 || id == 41 ||
id == 42 || id == 43 || id == 44 || id == 45 ||
id == 87 || id == 88 || id == 89 || id == 90 || id == 599) {
return 1.0; // 雷阵雨按小雨处理
}
// 雨夹雪冻雨
if (id == 49 || id == 50 || id == 64 || id == 65) {
return 1.0; // 雨夹雪按小雨处理
}
// 冰雹冰针冰粒
if (id == 46 || id == 47 || id == 48) {
return 1.0; // 冰雹按小雨处理
}
// 通用通用
if (id == 78 || id == 77) {
return 1.0; // 通用雨/雪按小雨处理
}
// 其他情况晴天多云阴天沙尘等
return 0.0; // 无雨
} catch (NumberFormatException e) {
return 0.0; // 解析失败默认无雨
}
}
/**
* 根据风力等级转换为风速中间值单位m/s
* @param windLevel 风力等级
* @return 风速中间值
*/
private static Double convertWindLevelToSpeed(String windLevel) {
if (windLevel == null || windLevel.isEmpty()) {
return 0.1; // 默认无风的中间值
}
try {
int level = Integer.parseInt(windLevel);
switch (level) {
case 0: // 0-0.2 m/s
return 0.1;
case 1: // 0.3-1.5 m/s
return 0.9;
case 2: // 1.6-3.3 m/s
return 2.45;
case 3: // 3.4-5.4 m/s
return 4.4;
case 4: // 5.5-7.9 m/s
return 6.7;
case 5: // 8-10.7 m/s
return 9.35;
case 6: // 10.8-13.8 m/s
return 12.3;
case 7: // 13.9-17.1 m/s
return 15.5;
case 8: // 17.2-20.7 m/s
return 18.95;
case 9: // 20.8-24.4 m/s
return 22.6;
case 10: // 24.5-28.4 m/s
return 26.45;
case 11: // 28.5-32.6 m/s
return 30.55;
case 12: // 32.6-999.9 m/s (飓风)
return 516.25; // (32.6 + 999.9) / 2
default:
return 0.1; // 未知等级默认无风
}
} catch (NumberFormatException e) {
return 0.1; // 解析失败默认无风
}
}
}

View File

@ -0,0 +1,211 @@
# DJI MQTT 模块使用说明(支持多客户端)
## 概述
本模块实现了大疆MQTT消息的接收和处理功能支持动态创建多个MQTT客户端每个客户端可以连接到不同的服务器。
## 设备分类
| SN 前缀 | 设备类型 |
|---------|---------|
| `7C` 开头 | 大疆设备 |
| `158` 开头 | 大疆设备 |
**注意本模块只处理大疆设备7C、158开头会自动过滤拓恒设备TH开头包括THJS。**
## 核心特性
**多客户端支持** - 可以同时创建多个MQTT客户端
**动态配置** - 每个客户端可以独立配置IP、端口、用户名等
**独立消息处理** - 每个客户端有独立的消息处理器
**共享订阅可选** - 支持开启/关闭共享订阅
**完整数据模型** - 包含所有无人机和机场字段
**自动区分设备** - 自动识别无人机和机场数据
## 架构设计
```
DjiMqttClientManager (管理器)
DjiMqttClientService (客户端实例)
DjiMqttMessageHandler (消息处理器)
IDroneDataCallback / IDockDataCallback (回调接口)
```
## 快速开始
### 1. 注入管理器
```java
@Autowired
private DjiMqttClientManager clientManager;
```
### 2. 创建客户端
```java
// 构建配置
DjiMqttClientConfig config = DjiMqttClientConfig.builder()
.host("mqtt.t-aaron.com") // MQTT服务器地址
.port(10883) // 端口
.clientId("my_client_1") // 客户端ID必须唯一
.username("admin") // 用户名
.password("admin") // 密码
.useSharedSubscription(true) // 是否使用共享订阅
.sharedGroupName("dji-group") // 共享订阅组名
.build();
// 创建并连接客户端
String clientId = clientManager.createClient(config);
```
### 3. 注册回调
```java
// 获取消息处理器
DjiMqttMessageHandler handler = clientManager.getHandler(clientId);
// 注册无人机数据回调
handler.registerDroneDataCallback(droneData -> {
System.out.println("无人机SN: " + droneData.getDeviceSn());
System.out.println("位置: " + droneData.getLatitude() + ", " + droneData.getLongitude());
});
// 注册机场数据回调
handler.registerDockDataCallback(dockData -> {
System.out.println("机场SN: " + dockData.getDeviceSn());
System.out.println("温度: " + dockData.getTemperature());
});
```
## 多客户端示例
### 场景同时连接多个MQTT服务器
```java
@Component
public class MyMqttService {
@Autowired
private DjiMqttClientManager clientManager;
public void init() {
// 客户端1连接到服务器A
DjiMqttClientConfig config1 = DjiMqttClientConfig.builder()
.host("mqtt.server-a.com")
.port(10883)
.clientId("client_a")
.username("admin")
.password("admin")
.useSharedSubscription(true)
.build();
String clientId1 = clientManager.createClient(config1);
DjiMqttMessageHandler handler1 = clientManager.getHandler(clientId1);
handler1.registerDroneDataCallback(data -> processServerA(data));
// 客户端2连接到服务器B
DjiMqttClientConfig config2 = DjiMqttClientConfig.builder()
.host("mqtt.server-b.com")
.port(1883)
.clientId("client_b")
.username("user2")
.password("pass2")
.useSharedSubscription(false)
.build();
String clientId2 = clientManager.createClient(config2);
DjiMqttMessageHandler handler2 = clientManager.getHandler(clientId2);
handler2.registerDroneDataCallback(data -> processServerB(data));
}
}
```
## 管理器API
### 创建客户端
```java
String clientId = clientManager.createClient(config);
```
### 获取消息处理器
```java
DjiMqttMessageHandler handler = clientManager.getHandler(clientId);
```
### 获取客户端
```java
DjiMqttClientService client = clientManager.getClient(clientId);
boolean isConnected = client.isConnected();
```
### 移除客户端
```java
clientManager.removeClient(clientId);
```
### 断开所有客户端
```java
clientManager.disconnectAll();
```
### 获取所有客户端ID
```java
Set<String> clientIds = clientManager.getAllClientIds();
```
## 配置参数说明
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| host | String | 是 | - | MQTT服务器地址 |
| port | Integer | 是 | - | MQTT服务器端口 |
| clientId | String | 是 | - | 客户端ID必须唯一 |
| username | String | 是 | - | 用户名 |
| password | String | 是 | - | 密码 |
| connectionTimeout | Integer | 否 | 30 | 连接超时时间(秒) |
| keepAliveInterval | Integer | 否 | 60 | 保持连接时间(秒) |
| autoReconnect | Boolean | 否 | true | 自动重连 |
| cleanSession | Boolean | 否 | false | 清除会话 |
| useSharedSubscription | Boolean | 否 | true | 是否使用共享订阅 |
| sharedGroupName | String | 否 | dji-group | 共享订阅组名 |
## 数据模型
### DroneData无人机数据
包含100+字段,主要分类:
- **基础信息**:固件版本、飞行器状态、档位等
- **位置信息**经纬度、高度、Home点等
- **姿态信息**:偏航角、横滚角、俯仰角
- **速度信息**:水平速度、垂直速度、风速
- **电池信息**:电量、剩余飞行时间、电池详情
- **相机信息**:拍照录像状态、变焦、红外测温等
- **避障信息**:水平/上视/下视避障状态
- **保养信息**:保养状态、累计飞行时间/架次
### DockData机场数据
包含60+字段,主要分类:
- **基础信息**:固件版本、机场状态、任务状态
- **位置信息**:经纬度、高度、朝向角
- **环境信息**:温度、湿度、风速、降雨量
- **设备状态**:舱盖状态、飞行器在舱状态、补光灯等
- **电池信息**:充电状态、备用电池、电池保养
- **网络信息**:网络类型、质量、速率
- **图传信息**4G/SDR链路状态、信号质量
## 注意事项
1. **clientId必须唯一**每个MQTT客户端的clientId必须全局唯一
2. **部分字段推送**每次MQTT消息可能只包含部分字段使用时需要判空
3. **原始数据访问**:所有字段都保存在`rawData`中可以通过Map访问
4. **共享订阅**:多实例部署时建议开启共享订阅,避免重复消费
5. **独立处理器**:每个客户端有独立的消息处理器,互不影响
6. **自动重连**:连接断开后会自动重连(可配置)
## 完整示例
参考 `DjiMqttUsageExample.java` 获取完整示例代码。

View File

@ -0,0 +1,18 @@
package com.ruoyi.device.domain.impl.djimqtt.callback;
import com.ruoyi.device.domain.impl.djimqtt.model.DockData;
/**
* 机场数据回调接口
*
* @author ruoyi
*/
public interface IDockDataCallback {
/**
* 处理机场数据
*
* @param dockData 机场数据
*/
void onDockData(DockData dockData);
}

View File

@ -0,0 +1,18 @@
package com.ruoyi.device.domain.impl.djimqtt.callback;
import com.ruoyi.device.domain.impl.djimqtt.model.DroneData;
/**
* 无人机数据回调接口
*
* @author ruoyi
*/
public interface IDroneDataCallback {
/**
* 处理无人机数据
*
* @param droneData 无人机数据
*/
void onDroneData(DroneData droneData);
}

View File

@ -0,0 +1,76 @@
package com.ruoyi.device.domain.impl.djimqtt.config;
import lombok.Builder;
import lombok.Data;
/**
* DJI MQTT客户端配置
* 用于动态创建MQTT客户端
*
* @author ruoyi
*/
@Data
@Builder
public class DjiMqttClientConfig {
/**
* MQTT服务器地址
*/
private String host;
/**
* MQTT服务器端口
*/
private Integer port;
/**
* 客户端ID必须唯一
*/
private String clientId;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 连接超时时间
*/
@Builder.Default
private Integer connectionTimeout = 30;
/**
* 保持连接时间
*/
@Builder.Default
private Integer keepAliveInterval = 60;
/**
* 自动重连
*/
@Builder.Default
private Boolean autoReconnect = true;
/**
* 清除会话
*/
@Builder.Default
private Boolean cleanSession = false;
/**
* 是否使用共享订阅
*/
@Builder.Default
private Boolean useSharedSubscription = true;
/**
* 共享订阅组名
*/
@Builder.Default
private String sharedGroupName = "dji-group";
}

View File

@ -0,0 +1,153 @@
package com.ruoyi.device.domain.impl.djimqtt.example;
import com.ruoyi.device.domain.impl.djimqtt.callback.IDockDataCallback;
import com.ruoyi.device.domain.impl.djimqtt.callback.IDroneDataCallback;
import com.ruoyi.device.domain.impl.djimqtt.config.DjiMqttClientConfig;
import com.ruoyi.device.domain.impl.djimqtt.handler.DjiMqttMessageHandler;
import com.ruoyi.device.domain.impl.djimqtt.manager.DjiMqttClientManager;
import com.ruoyi.device.domain.impl.djimqtt.model.DockData;
import com.ruoyi.device.domain.impl.djimqtt.model.DroneData;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
/**
* DJI MQTT使用示例支持多客户端
*
* 使用说明
* 1. 注入 DjiMqttClientManager
* 2. 使用 DjiMqttClientConfig.builder() 创建配置
* 3. 调用 manager.createClient(config) 创建客户端
* 4. 通过 manager.getHandler(clientId) 获取消息处理器
* 5. 注册回调处理数据
*
* @author ruoyi
*/
@Slf4j
@Component
public class DjiMqttUsageExample {
// @Autowired
// private DjiMqttClientManager clientManager;
//
// /**
// * 应用启动后创建MQTT客户端
// */
// @EventListener(ApplicationReadyEvent.class)
// public void onApplicationReady() {
// // 示例1创建第一个MQTT客户端
// createClient1();
//
// // 示例2创建第二个MQTT客户端不同的服务器
// createClient2();
// }
//
// /**
// * 创建第一个MQTT客户端
// */
// private void createClient1() {
// // 构建配置
// DjiMqttClientConfig config = DjiMqttClientConfig.builder()
// .host("mqtt.t-aaron.com")
// .port(10883)
// .clientId("client_1")
// .username("admin")
// .password("admin")
// .useSharedSubscription(true)
// .sharedGroupName("dji-group-1")
// .build();
//
// // 创建客户端
// String clientId = clientManager.createClient(config);
//
// // 获取消息处理器
// DjiMqttMessageHandler handler = clientManager.getHandler(clientId);
//
// // 注册无人机数据回调
// handler.registerDroneDataCallback(new IDroneDataCallback() {
// @Override
// public void onDroneData(DroneData droneData) {
// handleDroneDataForClient1(droneData);
// }
// });
//
// // 注册机场数据回调
// handler.registerDockDataCallback(new IDockDataCallback() {
// @Override
// public void onDockData(DockData dockData) {
// handleDockDataForClient1(dockData);
// }
// });
//
// log.info("客户端1已创建并注册回调");
// }
//
// /**
// * 创建第二个MQTT客户端连接到不同的服务器
// */
// private void createClient2() {
// // 构建配置
// DjiMqttClientConfig config = DjiMqttClientConfig.builder()
// .host("mqtt.another-server.com")
// .port(1883)
// .clientId("client_2")
// .username("user2")
// .password("pass2")
// .useSharedSubscription(false) // 不使用共享订阅
// .build();
//
// // 创建客户端
// String clientId = clientManager.createClient(config);
//
// // 获取消息处理器
// DjiMqttMessageHandler handler = clientManager.getHandler(clientId);
//
// // 注册回调
// handler.registerDroneDataCallback(droneData -> handleDroneDataForClient2(droneData));
// handler.registerDockDataCallback(dockData -> handleDockDataForClient2(dockData));
//
// log.info("客户端2已创建并注册回调");
// }
//
// /**
// * 处理客户端1的无人机数据
// */
// private void handleDroneDataForClient1(DroneData droneData) {
// log.info("[客户端1] 收到无人机数据 - SN: {}, Type: {}",
// droneData.getDeviceSn(),
// droneData.getMessageType());
//
// // 处理位置信息
// if (droneData.getLatitude() != null && droneData.getLongitude() != null) {
// log.info("[客户端1] 无人机位置 - 纬度: {}, 经度: {}, 高度: {}",
// droneData.getLatitude(),
// droneData.getLongitude(),
// droneData.getElevation());
// }
// }
//
// /**
// * 处理客户端1的机场数据
// */
// private void handleDockDataForClient1(DockData dockData) {
// log.info("[客户端1] 收到机场数据 - SN: {}, Type: {}",
// dockData.getDeviceSn(),
// dockData.getMessageType());
// }
//
// /**
// * 处理客户端2的无人机数据
// */
// private void handleDroneDataForClient2(DroneData droneData) {
// log.info("[客户端2] 收到无人机数据 - SN: {}", droneData.getDeviceSn());
// }
//
// /**
// * 处理客户端2的机场数据
// */
// private void handleDockDataForClient2(DockData dockData) {
// log.info("[客户端2] 收到机场数据 - SN: {}", dockData.getDeviceSn());
// }
}

View File

@ -0,0 +1,243 @@
package com.ruoyi.device.domain.impl.djimqtt.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.device.domain.impl.djimqtt.callback.IDockDataCallback;
import com.ruoyi.device.domain.impl.djimqtt.callback.IDroneDataCallback;
import com.ruoyi.device.domain.impl.djimqtt.model.DjiMqttMessage;
import com.ruoyi.device.domain.impl.djimqtt.model.DockData;
import com.ruoyi.device.domain.impl.djimqtt.model.DroneData;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* DJI MQTT消息处理器
* 订阅 thing/product/+ 格式的topic
* 只处理大疆设备7C158 开头的SN
* 过滤拓恒设备TH 开头的SN包括THJS
*
* @author ruoyi
*/
@Slf4j
@Component
public class DjiMqttMessageHandler {
private final ObjectMapper objectMapper = new ObjectMapper();
/**
* 无人机数据回调列表
*/
private final List<IDroneDataCallback> droneDataCallbacks = new ArrayList<>();
/**
* 机场数据回调列表
*/
private final List<IDockDataCallback> dockDataCallbacks = new ArrayList<>();
/**
* 无人机SN正则表达式只匹配大疆7C158开头的SN
* 示例7CXXXXXXXXXX, 158XXXXXXXXXX
*/
private static final Pattern DRONE_SN_PATTERN = Pattern.compile("^(7C|158)[0-9A-Z]+");
/**
* 机场SN正则表达式只匹配大疆7C158开头的SN
* 示例7CXXXXXXXXXX, 158XXXXXXXXXX
*/
private static final Pattern DOCK_SN_PATTERN = Pattern.compile("^(7C|158)[0-9A-Z]+");
/**
* 拓恒设备SN前缀需要过滤
* TH开头的设备是拓恒设备需要跳过
*/
private static final Pattern TUOHENG_SN_PATTERN = Pattern.compile("^TH[0-9A-Z]+");
/**
* 注册无人机数据回调
*
* @param callback 回调接口
*/
public void registerDroneDataCallback(IDroneDataCallback callback) {
if (callback != null && !droneDataCallbacks.contains(callback)) {
droneDataCallbacks.add(callback);
log.info("注册无人机数据回调: {}", callback.getClass().getSimpleName());
}
}
/**
* 注册机场数据回调
*
* @param callback 回调接口
*/
public void registerDockDataCallback(IDockDataCallback callback) {
if (callback != null && !dockDataCallbacks.contains(callback)) {
dockDataCallbacks.add(callback);
log.info("注册机场数据回调: {}", callback.getClass().getSimpleName());
}
}
/**
* 处理MQTT消息
*
* @param topic 主题
* @param payload 消息内容
*/
public void handleMessage(String topic, String payload) {
try {
log.debug("收到MQTT消息 - Topic: {}, Payload: {}", topic, payload);
// 解析设备SN和消息类型
String deviceSn = extractDeviceSnFromTopic(topic);
String messageType = extractMessageTypeFromTopic(topic);
if (deviceSn == null || messageType == null) {
log.warn("无法从Topic解析设备SN或消息类型: {}", topic);
return;
}
if (isTuohengSn(deviceSn)) {
log.debug("跳过拓恒设备消息 - SN: {}", deviceSn);
return;
}
// 解析JSON消息
@SuppressWarnings("unchecked")
DjiMqttMessage<Map<String, Object>> message = objectMapper.readValue(
payload,
objectMapper.getTypeFactory().constructParametricType(
DjiMqttMessage.class,
Map.class
)
);
// 判断是无人机还是机场
if (isDroneSn(deviceSn)) {
handleDroneMessage(deviceSn, messageType, message);
} else if (isDockSn(deviceSn)) {
handleDockMessage(deviceSn, messageType, message);
} else {
// log.warn("未知的设备SN格式: {}", deviceSn);
}
} catch (Exception e) {
log.error("处理MQTT消息失败 - Topic: {}, Error: {}", topic, e.getMessage(), e);
}
}
/**
* 处理无人机消息
*/
private void handleDroneMessage(String deviceSn, String messageType, DjiMqttMessage<Map<String, Object>> message) {
try {
DroneData droneData = objectMapper.convertValue(message.getData(), DroneData.class);
droneData.setDeviceSn(deviceSn);
droneData.setMessageType(messageType);
droneData.setRawData(message.getData());
log.debug("处理无人机数据 - SN: {}, Type: {}", deviceSn, messageType);
// 通知所有回调
for (IDroneDataCallback callback : droneDataCallbacks) {
try {
callback.onDroneData(droneData);
} catch (Exception e) {
log.error("无人机数据回调执行失败: {}", e.getMessage(), e);
}
}
} catch (Exception e) {
log.error("处理无人机消息失败 - SN: {}, Error: {}", deviceSn, e.getMessage(), e);
}
}
/**
* 处理机场消息
*/
private void handleDockMessage(String deviceSn, String messageType, DjiMqttMessage<Map<String, Object>> message) {
try {
DockData dockData = objectMapper.convertValue(message.getData(), DockData.class);
dockData.setDeviceSn(deviceSn);
dockData.setMessageType(messageType);
dockData.setRawData(message.getData());
log.debug("处理机场数据 - SN: {}, Type: {}", deviceSn, messageType);
// 通知所有回调
for (IDockDataCallback callback : dockDataCallbacks) {
try {
callback.onDockData(dockData);
} catch (Exception e) {
log.error("机场数据回调执行失败: {}", e.getMessage(), e);
}
}
} catch (Exception e) {
log.error("处理机场消息失败 - SN: {}, Error: {}", deviceSn, e.getMessage(), e);
}
}
/**
* 从Topic中提取设备SN
* Topic格式: thing/product/{deviceSn}/osd thing/product/{deviceSn}/state
*/
private String extractDeviceSnFromTopic(String topic) {
if (topic == null) {
return null;
}
String[] parts = topic.split("/");
if (parts.length >= 3) {
return parts[2];
}
return null;
}
/**
* 从Topic中提取消息类型
*/
private String extractMessageTypeFromTopic(String topic) {
if (topic == null) {
return null;
}
String[] parts = topic.split("/");
if (parts.length >= 4) {
return parts[3]; // osd state
}
return null;
}
/**
* 判断是否为无人机SN
*/
private boolean isDroneSn(String sn) {
if (sn == null) {
return false;
}
Matcher matcher = DRONE_SN_PATTERN.matcher(sn);
return matcher.matches();
}
/**
* 判断是否为机场SN
*/
private boolean isDockSn(String sn) {
if (sn == null) {
return false;
}
Matcher matcher = DOCK_SN_PATTERN.matcher(sn);
return matcher.matches();
}
/**
* 判断是否为拓恒设备SN
*/
private boolean isTuohengSn(String sn) {
if (sn == null) {
return false;
}
Matcher matcher = TUOHENG_SN_PATTERN.matcher(sn);
return matcher.matches();
}
}

View File

@ -0,0 +1,125 @@
package com.ruoyi.device.domain.impl.djimqtt.manager;
import com.ruoyi.device.domain.impl.djimqtt.config.DjiMqttClientConfig;
import com.ruoyi.device.domain.impl.djimqtt.handler.DjiMqttMessageHandler;
import com.ruoyi.device.domain.impl.djimqtt.service.DjiMqttClientService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* DJI MQTT客户端管理器
* 管理多个MQTT客户端实例
*
* @author ruoyi
*/
@Slf4j
@Component
public class DjiMqttClientManager {
/**
* 客户端映射表 clientId -> DjiMqttClientService
*/
private final Map<String, DjiMqttClientService> clients = new ConcurrentHashMap<>();
/**
* 消息处理器映射表 clientId -> DjiMqttMessageHandler
*/
private final Map<String, DjiMqttMessageHandler> handlers = new ConcurrentHashMap<>();
/**
* 创建并连接MQTT客户端
*
* @param config 客户端配置
* @return 客户端ID
*/
public String createClient(DjiMqttClientConfig config) {
String clientId = config.getClientId();
if (clients.containsKey(clientId)) {
log.warn("MQTT客户端[{}]已存在", clientId);
return clientId;
}
// 为每个客户端创建独立的消息处理器
DjiMqttMessageHandler handler = new DjiMqttMessageHandler();
handlers.put(clientId, handler);
// 创建客户端
DjiMqttClientService client = new DjiMqttClientService(config, handler);
clients.put(clientId, client);
// 连接
client.connect();
log.info("成功创建MQTT客户端[{}]", clientId);
return clientId;
}
/**
* 获取消息处理器
*
* @param clientId 客户端ID
* @return 消息处理器
*/
public DjiMqttMessageHandler getHandler(String clientId) {
return handlers.get(clientId);
}
/**
* 获取客户端
*
* @param clientId 客户端ID
* @return 客户端服务
*/
public DjiMqttClientService getClient(String clientId) {
return clients.get(clientId);
}
/**
* 断开并移除客户端
*
* @param clientId 客户端ID
*/
public void removeClient(String clientId) {
DjiMqttClientService client = clients.remove(clientId);
if (client != null) {
client.disconnect();
log.info("已移除MQTT客户端[{}]", clientId);
}
handlers.remove(clientId);
}
/**
* 断开所有客户端
*/
public void disconnectAll() {
clients.forEach((clientId, client) -> {
try {
client.disconnect();
log.info("已断开MQTT客户端[{}]", clientId);
} catch (Exception e) {
log.error("断开MQTT客户端[{}]失败: {}", clientId, e.getMessage(), e);
}
});
clients.clear();
handlers.clear();
}
/**
* 获取所有客户端ID
*/
public java.util.Set<String> getAllClientIds() {
return clients.keySet();
}
/**
* 检查客户端是否存在
*/
public boolean hasClient(String clientId) {
return clients.containsKey(clientId);
}
}

View File

@ -0,0 +1,57 @@
package com.ruoyi.device.domain.impl.djimqtt.model;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* DJI MQTT消息基础结构
*
* @author ruoyi
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class DjiMqttMessage<T> {
/**
* 事务ID
*/
@JsonProperty("tid")
private String tid;
/**
* 业务ID
*/
@JsonProperty("bid")
private String bid;
/**
* 时间戳
*/
@JsonProperty("timestamp")
private Long timestamp;
/**
* 数据内容
*/
@JsonProperty("data")
private T data;
/**
* 网关设备SN
*/
@JsonProperty("gateway")
private String gateway;
/**
* 消息来源
*/
@JsonProperty("source")
private String source;
/**
* 是否需要回复
*/
@JsonProperty("need_reply")
private Integer needReply;
}

View File

@ -0,0 +1,291 @@
package com.ruoyi.device.domain.impl.djimqtt.model;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.ruoyi.device.domain.impl.djimqtt.model.dock.*;
import com.ruoyi.device.domain.impl.djimqtt.model.drone.MaintainStatus;
import com.ruoyi.device.domain.impl.djimqtt.model.drone.PositionState;
import com.ruoyi.device.domain.impl.djimqtt.model.drone.StorageInfo;
import lombok.Data;
import java.util.Map;
/**
* 机场完整数据从osd和state主题接收
* 注意每次推送可能只包含部分字段
*
* @author ruoyi
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class DockData {
// ========== 元数据 ==========
/**
* 设备SN从topic中解析
*/
private String deviceSn;
/**
* 消息类型osd或state
*/
private String messageType;
/**
* 原始数据用于存储所有字段
*/
private Map<String, Object> rawData;
// ========== 基础信息 ==========
/**
* Home 点有效性0-无效1-有效
*/
@JsonProperty("home_position_is_valid")
private Integer homePositionIsValid;
/**
* 机场朝向角
*/
@JsonProperty("heading")
private Double heading;
/**
* 固件版本
*/
@JsonProperty("firmware_version")
private String firmwareVersion;
/**
* 机场状态
*/
@JsonProperty("mode_code")
private Integer modeCode;
/**
* 机场任务状态
*/
@JsonProperty("flighttask_step_code")
private Integer flighttaskStepCode;
// ========== 位置信息 ==========
/**
* 纬度
*/
@JsonProperty("latitude")
private Double latitude;
/**
* 经度
*/
@JsonProperty("longitude")
private Double longitude;
/**
* 椭球高度
*/
@JsonProperty("height")
private Double height;
// ========== 环境信息 ==========
/**
* 舱内温度摄氏度
*/
@JsonProperty("temperature")
private Float temperature;
/**
* 舱内湿度%RH
*/
@JsonProperty("humidity")
private Float humidity;
/**
* 环境温度摄氏度
*/
@JsonProperty("environment_temperature")
private Float environmentTemperature;
/**
* 风速/
*/
@JsonProperty("wind_speed")
private Float windSpeed;
/**
* 降雨量0-无雨1-小雨2-中雨3-大雨
*/
@JsonProperty("rainfall")
private Integer rainfall;
// ========== 设备状态 ==========
/**
* 舱盖状态0-关闭1-打开2-半开3-舱盖状态异常
*/
@JsonProperty("cover_state")
private Integer coverState;
/**
* 飞行器是否在舱0-舱外1-舱内
*/
@JsonProperty("drone_in_dock")
private Integer droneInDock;
/**
* DRC链路状态0-未连接1-连接中2-已连接
*/
@JsonProperty("drc_state")
private Integer drcState;
/**
* 紧急停止按钮状态0-关闭1-开启
*/
@JsonProperty("emergency_stop_state")
private Integer emergencyStopState;
/**
* 补光灯状态0-关闭1-打开
*/
@JsonProperty("supplement_light_state")
private Integer supplementLightState;
/**
* 机场声光报警状态0-关闭1-开启
*/
@JsonProperty("alarm_state")
private Integer alarmState;
// ========== 电池和电源 ==========
/**
* 飞行器电池保养信息
*/
@JsonProperty("drone_battery_maintenance_info")
private DroneBatteryMaintenanceInfo droneBatteryMaintenanceInfo;
/**
* 飞行器充电状态
*/
@JsonProperty("drone_charge_state")
private DroneChargeState droneChargeState;
/**
* 机场备用电池信息
*/
@JsonProperty("backup_battery")
private BackupBattery backupBattery;
/**
* 电池运行模式1-计划模式2-待命模式
*/
@JsonProperty("battery_store_mode")
private Integer batteryStoreMode;
/**
* 工作电流毫安
*/
@JsonProperty("working_current")
private Float workingCurrent;
/**
* 工作电压毫伏
*/
@JsonProperty("working_voltage")
private Integer workingVoltage;
// ========== 网络和存储 ==========
/**
* 网络状态
*/
@JsonProperty("network_state")
private NetworkState networkState;
/**
* 图传链路
*/
@JsonProperty("wireless_link")
private WirelessLink wirelessLink;
/**
* 存储容量
*/
@JsonProperty("storage")
private StorageInfo storage;
// ========== 空调和配置 ==========
/**
* 机场空调工作状态信息
*/
@JsonProperty("air_conditioner")
private AirConditioner airConditioner;
/**
* 空中回传false-关闭true-开启
*/
@JsonProperty("air_transfer_enable")
private Boolean airTransferEnable;
/**
* 机场静音模式0-非静音模式1-静音模式
*/
@JsonProperty("silent_mode")
private Integer silentMode;
// ========== 保养和固件 ==========
/**
* 保养信息
*/
@JsonProperty("maintain_status")
private MaintainStatus maintainStatus;
/**
* 搜星状态
*/
@JsonProperty("position_state")
private PositionState positionState;
/**
* 机场激活时间(unix 时间戳)
*/
@JsonProperty("activation_time")
private Long activationTime;
/**
* 固件升级状态0-未升级1-升级中
*/
@JsonProperty("firmware_upgrade_status")
private Integer firmwareUpgradeStatus;
/**
* 固件一致性0-不需要一致性升级1-需要一致性升级
*/
@JsonProperty("compatible_status")
private Integer compatibleStatus;
// ========== 统计信息 ==========
/**
* 机场累计运行时长
*/
@JsonProperty("acc_time")
private Integer accTime;
/**
* 机场累计作业次数
*/
@JsonProperty("job_number")
private Integer jobNumber;
/**
* 首次上电时间毫秒
*/
@JsonProperty("first_power_on")
private Long firstPowerOn;
}

View File

@ -0,0 +1,329 @@
package com.ruoyi.device.domain.impl.djimqtt.model;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.ruoyi.device.domain.impl.djimqtt.model.drone.*;
import lombok.Data;
import java.util.List;
import java.util.Map;
/**
* 无人机完整数据从osd和state主题接收
* 注意每次推送可能只包含部分字段
*
* @author ruoyi
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class DroneData {
// ========== 元数据 ==========
/**
* 设备SN从topic中解析
*/
private String deviceSn;
/**
* 消息类型osd或state
*/
private String messageType;
/**
* 原始数据用于存储所有字段
*/
private Map<String, Object> rawData;
// ========== 基础信息 ==========
/**
* 飞行器图传连接质量最好的网关SN
*/
@JsonProperty("best_link_gateway")
private String bestLinkGateway;
/**
* 图传连接拓扑
*/
@JsonProperty("wireless_link_topo")
private WirelessLinkTopo wirelessLinkTopo;
/**
* 飞行器相机信息
*/
@JsonProperty("cameras")
private List<CameraInfo> cameras;
/**
* 飞行安全数据库版本
*/
@JsonProperty("flysafe_database_version")
private String flysafeDatabaseVersion;
/**
* 离线地图开关false-关闭true-开启
*/
@JsonProperty("offline_map_enable")
private Boolean offlineMapEnable;
// ========== 返航和限制 ==========
/**
* 返航高度模式当前值0-智能高度1-设定高度
*/
@JsonProperty("current_rth_mode")
private Integer currentRthMode;
/**
* 返航高度模式设置值0-智能高度1-设定高度
*/
@JsonProperty("rth_mode")
private Integer rthMode;
/**
* 飞行器避障状态
*/
@JsonProperty("obstacle_avoidance")
private ObstacleAvoidance obstacleAvoidance;
/**
* 是否接近限飞区0-未达到1-接近
*/
@JsonProperty("is_near_area_limit")
private Integer isNearAreaLimit;
/**
* 是否接近设定的限制高度0-未达到1-接近
*/
@JsonProperty("is_near_height_limit")
private Integer isNearHeightLimit;
/**
* 飞行器限高
*/
@JsonProperty("height_limit")
private Integer heightLimit;
/**
* 飞行器限远状态
*/
@JsonProperty("distance_limit_status")
private DistanceLimitStatus distanceLimitStatus;
// ========== 设备状态 ==========
/**
* 飞行器夜航灯状态0-关闭1-打开
*/
@JsonProperty("night_lights_state")
private Integer nightLightsState;
/**
* 飞行器激活时间(unix 时间戳)
*/
@JsonProperty("activation_time")
private Long activationTime;
/**
* 保养信息
*/
@JsonProperty("maintain_status")
private MaintainStatus maintainStatus;
/**
* 飞行器累计飞行总架次
*/
@JsonProperty("total_flight_sorties")
private Integer totalFlightSorties;
/**
* 飞行器累计飞行总里程
*/
@JsonProperty("total_flight_distance")
private Float totalFlightDistance;
/**
* 飞行器累计飞行航时
*/
@JsonProperty("total_flight_time")
private Float totalFlightTime;
// ========== 位置信息 ==========
/**
* 搜星状态
*/
@JsonProperty("position_state")
private PositionState positionState;
/**
* 存储容量
*/
@JsonProperty("storage")
private StorageInfo storage;
/**
* 当前位置纬度
*/
@JsonProperty("latitude")
private Double latitude;
/**
* 当前位置经度
*/
@JsonProperty("longitude")
private Double longitude;
/**
* 相对起飞点高度
*/
@JsonProperty("elevation")
private Float elevation;
/**
* 绝对高度
*/
@JsonProperty("height")
private Double height;
/**
* Home点纬度
*/
@JsonProperty("home_latitude")
private Double homeLatitude;
/**
* Home点经度
*/
@JsonProperty("home_longitude")
private Double homeLongitude;
/**
* 距离Home点的距离
*/
@JsonProperty("home_distance")
private Float homeDistance;
// ========== 姿态信息 ==========
/**
* 偏航轴角度
*/
@JsonProperty("attitude_head")
private Integer attitudeHead;
/**
* 横滚轴角度
*/
@JsonProperty("attitude_roll")
private Float attitudeRoll;
/**
* 俯仰轴角度
*/
@JsonProperty("attitude_pitch")
private Float attitudePitch;
// ========== 速度和风速 ==========
/**
* 水平速度/
*/
@JsonProperty("horizontal_speed")
private Float horizontalSpeed;
/**
* 垂直速度/
*/
@JsonProperty("vertical_speed")
private Float verticalSpeed;
/**
* 当前风向
*/
@JsonProperty("wind_direction")
private Integer windDirection;
/**
* 风速/
*/
@JsonProperty("wind_speed")
private Float windSpeed;
// ========== 电池信息 ==========
/**
* 飞行器电池信息
*/
@JsonProperty("battery")
private DroneBatteryInfo battery;
/**
* 严重低电量告警百分比
*/
@JsonProperty("serious_low_battery_warning_threshold")
private Integer seriousLowBatteryWarningThreshold;
/**
* 低电量告警百分比
*/
@JsonProperty("low_battery_warning_threshold")
private Integer lowBatteryWarningThreshold;
/**
* 返航预留电量百分比
*/
@JsonProperty("remaining_power_for_return_home")
private Integer remainingPowerForReturnHome;
// ========== 控制和状态 ==========
/**
* 当前控制源
*/
@JsonProperty("control_source")
private String controlSource;
/**
* 固件升级状态0-未升级1-升级中
*/
@JsonProperty("firmware_upgrade_status")
private Integer firmwareUpgradeStatus;
/**
* 固件一致性0-不需要一致性升级1-需要一致性升级
*/
@JsonProperty("compatible_status")
private Integer compatibleStatus;
/**
* 固件版本
*/
@JsonProperty("firmware_version")
private String firmwareVersion;
/**
* 档位
*/
@JsonProperty("gear")
private Integer gear;
/**
* 飞行器状态
*/
@JsonProperty("mode_code")
private Integer modeCode;
/**
* 飞行器进入当前状态的原因
*/
@JsonProperty("mode_code_reason")
private Integer modeCodeReason;
/**
* 航迹ID
*/
@JsonProperty("track_id")
private String trackId;
}

View File

@ -0,0 +1,27 @@
package com.ruoyi.device.domain.impl.djimqtt.model.dock;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* 机场空调工作状态信息
*
* @author ruoyi
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class AirConditioner {
/**
* 机场空调状态
*/
@JsonProperty("air_conditioner_state")
private Integer airConditionerState;
/**
* 剩余等待可切换时间
*/
@JsonProperty("switch_time")
private Integer switchTime;
}

View File

@ -0,0 +1,33 @@
package com.ruoyi.device.domain.impl.djimqtt.model.dock;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* 机场备用电池信息
*
* @author ruoyi
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class BackupBattery {
/**
* 备用电池开关0-关闭1-开启
*/
@JsonProperty("switch")
private Integer switchState;
/**
* 备用电池电压毫伏
*/
@JsonProperty("voltage")
private Integer voltage;
/**
* 备用电池温度摄氏度
*/
@JsonProperty("temperature")
private Float temperature;
}

View File

@ -0,0 +1,39 @@
package com.ruoyi.device.domain.impl.djimqtt.model.dock;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* 机场电池详细信息
*
* @author ruoyi
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class DockBatteryDetail {
/**
* 电池剩余电量
*/
@JsonProperty("capacity_percent")
private Integer capacityPercent;
/**
* 电池序号0-左电池1-右电池
*/
@JsonProperty("index")
private Integer index;
/**
* 电压毫伏
*/
@JsonProperty("voltage")
private Integer voltage;
/**
* 温度摄氏度
*/
@JsonProperty("temperature")
private Float temperature;
}

View File

@ -0,0 +1,41 @@
package com.ruoyi.device.domain.impl.djimqtt.model.dock;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.List;
/**
* 飞行器电池保养信息
*
* @author ruoyi
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class DroneBatteryMaintenanceInfo {
/**
* 保养状态0-无需保养1-待保养2-正在保养
*/
@JsonProperty("maintenance_state")
private Integer maintenanceState;
/**
* 电池保养剩余时间小时
*/
@JsonProperty("maintenance_time_left")
private Integer maintenanceTimeLeft;
/**
* 电池加热保温状态0-未开启1-加热中2-保温中
*/
@JsonProperty("heat_state")
private Integer heatState;
/**
* 电池详细信息
*/
@JsonProperty("batteries")
private List<DockBatteryDetail> batteries;
}

View File

@ -0,0 +1,27 @@
package com.ruoyi.device.domain.impl.djimqtt.model.dock;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* 飞行器充电状态
*
* @author ruoyi
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class DroneChargeState {
/**
* 电量百分比
*/
@JsonProperty("capacity_percent")
private Integer capacityPercent;
/**
* 充电状态0-空闲1-充电中
*/
@JsonProperty("state")
private Integer state;
}

View File

@ -0,0 +1,33 @@
package com.ruoyi.device.domain.impl.djimqtt.model.dock;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* 网络状态
*
* @author ruoyi
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class NetworkState {
/**
* 网络类型1-4G2-以太网
*/
@JsonProperty("type")
private Integer type;
/**
* 网络质量0-无信号1-2-较差3-一般4-较好5-
*/
@JsonProperty("quality")
private Integer quality;
/**
* 网络速率KB/s
*/
@JsonProperty("rate")
private Float rate;
}

View File

@ -0,0 +1,75 @@
package com.ruoyi.device.domain.impl.djimqtt.model.dock;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* 图传链路
*
* @author ruoyi
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class WirelessLink {
/**
* 飞行器上 Dongle 数量
*/
@JsonProperty("dongle_number")
private Integer dongleNumber;
/**
* 4G 链路连接状态0-断开1-连接
*/
@JsonProperty("4g_link_state")
private Integer fourGLinkState;
/**
* SDR 链路连接状态0-断开1-连接
*/
@JsonProperty("sdr_link_state")
private Integer sdrLinkState;
/**
* 机场的图传链路模式0-SDR 模式1-4G 融合模式
*/
@JsonProperty("link_workmode")
private Integer linkWorkmode;
/**
* SDR 信号质量0-5
*/
@JsonProperty("sdr_quality")
private Integer sdrQuality;
/**
* 总体 4G 信号质量0-5
*/
@JsonProperty("4g_quality")
private Integer fourGQuality;
/**
* 天端 4G 信号质量0-5
*/
@JsonProperty("4g_uav_quality")
private Integer fourGUavQuality;
/**
* 地端 4G 信号质量0-5
*/
@JsonProperty("4g_gnd_quality")
private Integer fourGGndQuality;
/**
* SDR 频段
*/
@JsonProperty("sdr_freq_band")
private Float sdrFreqBand;
/**
* 4G 频段
*/
@JsonProperty("4g_freq_band")
private Float fourGFreqBand;
}

View File

@ -0,0 +1,75 @@
package com.ruoyi.device.domain.impl.djimqtt.model.drone;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* 电池详细信息
*
* @author ruoyi
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class BatteryDetail {
/**
* 电池剩余电量
*/
@JsonProperty("capacity_percent")
private Integer capacityPercent;
/**
* 电池序号
*/
@JsonProperty("index")
private Integer index;
/**
* 电池序列号SN
*/
@JsonProperty("sn")
private String sn;
/**
* 电池类型
*/
@JsonProperty("type")
private Integer type;
/**
* 电池子类型
*/
@JsonProperty("sub_type")
private Integer subType;
/**
* 固件版本
*/
@JsonProperty("firmware_version")
private String firmwareVersion;
/**
* 电池循环次数
*/
@JsonProperty("loop_times")
private Integer loopTimes;
/**
* 电压毫伏
*/
@JsonProperty("voltage")
private Integer voltage;
/**
* 温度摄氏度
*/
@JsonProperty("temperature")
private Float temperature;
/**
* 高电压存储天数
*/
@JsonProperty("high_voltage_storage_days")
private Integer highVoltageStorageDays;
}

View File

@ -0,0 +1,167 @@
package com.ruoyi.device.domain.impl.djimqtt.model.drone;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.List;
/**
* 飞行器相机信息
*
* @author ruoyi
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class CameraInfo {
/**
* 剩余拍照张数
*/
@JsonProperty("remain_photo_num")
private Integer remainPhotoNum;
/**
* 剩余录像时间
*/
@JsonProperty("remain_record_duration")
private Integer remainRecordDuration;
/**
* 视频录制时长
*/
@JsonProperty("record_time")
private Integer recordTime;
/**
* 负载编号
*/
@JsonProperty("payload_index")
private String payloadIndex;
/**
* 相机模式0-拍照1-录像2-智能低光3-全景拍照
*/
@JsonProperty("camera_mode")
private Integer cameraMode;
/**
* 拍照状态0-空闲1-拍照中
*/
@JsonProperty("photo_state")
private Integer photoState;
/**
* 录像状态0-空闲1-录像中
*/
@JsonProperty("recording_state")
private Integer recordingState;
/**
* 变焦倍数
*/
@JsonProperty("zoom_factor")
private Float zoomFactor;
/**
* 红外变焦倍数
*/
@JsonProperty("ir_zoom_factor")
private Float irZoomFactor;
/**
* 视场角FOV liveview 中的区域
*/
@JsonProperty("liveview_world_region")
private LiveviewWorldRegion liveviewWorldRegion;
/**
* 照片存储设置集合
*/
@JsonProperty("photo_storage_settings")
private List<String> photoStorageSettings;
/**
* 视频存储设置集合
*/
@JsonProperty("video_storage_settings")
private List<String> videoStorageSettings;
/**
* 广角镜头曝光模式
*/
@JsonProperty("wide_exposure_mode")
private Integer wideExposureMode;
/**
* 广角镜头感光度
*/
@JsonProperty("wide_iso")
private Integer wideIso;
/**
* 广角镜头快门速度
*/
@JsonProperty("wide_shutter_speed")
private Integer wideShutterSpeed;
/**
* 广角镜头曝光值
*/
@JsonProperty("wide_exposure_value")
private Integer wideExposureValue;
/**
* 变焦镜头曝光模式
*/
@JsonProperty("zoom_exposure_mode")
private Integer zoomExposureMode;
/**
* 变焦镜头感光度
*/
@JsonProperty("zoom_iso")
private Integer zoomIso;
/**
* 变焦镜头快门速度
*/
@JsonProperty("zoom_shutter_speed")
private Integer zoomShutterSpeed;
/**
* 变焦镜头曝光值
*/
@JsonProperty("zoom_exposure_value")
private Integer zoomExposureValue;
/**
* 变焦镜头对焦模式
*/
@JsonProperty("zoom_focus_mode")
private Integer zoomFocusMode;
/**
* 变焦镜头对焦值
*/
@JsonProperty("zoom_focus_value")
private Integer zoomFocusValue;
/**
* 红外测温模式
*/
@JsonProperty("ir_metering_mode")
private Integer irMeteringMode;
/**
* 红外测温点
*/
@JsonProperty("ir_metering_point")
private IrMeteringPoint irMeteringPoint;
/**
* 红外测温区域
*/
@JsonProperty("ir_metering_area")
private IrMeteringArea irMeteringArea;
}

View File

@ -0,0 +1,27 @@
package com.ruoyi.device.domain.impl.djimqtt.model.drone;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* 飞行器对频信息中心节点
*
* @author ruoyi
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class CenterNode {
/**
* 扰码信息
*/
@JsonProperty("sdr_id")
private Integer sdrId;
/**
* 设备sn
*/
@JsonProperty("sn")
private String sn;
}

View File

@ -0,0 +1,33 @@
package com.ruoyi.device.domain.impl.djimqtt.model.drone;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* 飞行器限远状态
*
* @author ruoyi
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class DistanceLimitStatus {
/**
* 是否开启限远0-未设置1-已设置
*/
@JsonProperty("state")
private Integer state;
/**
* 限远距离
*/
@JsonProperty("distance_limit")
private Integer distanceLimit;
/**
* 是否接近设定的限制距离0-未达到1-接近
*/
@JsonProperty("is_near_distance_limit")
private Integer isNearDistanceLimit;
}

View File

@ -0,0 +1,47 @@
package com.ruoyi.device.domain.impl.djimqtt.model.drone;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.List;
/**
* 飞行器电池信息
*
* @author ruoyi
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class DroneBatteryInfo {
/**
* 电池的总剩余电量
*/
@JsonProperty("capacity_percent")
private Integer capacityPercent;
/**
* 剩余飞行时间
*/
@JsonProperty("remain_flight_time")
private Integer remainFlightTime;
/**
* 返航所需电量百分比
*/
@JsonProperty("return_home_power")
private Integer returnHomePower;
/**
* 强制降落电量百分比
*/
@JsonProperty("landing_power")
private Integer landingPower;
/**
* 电池详细信息
*/
@JsonProperty("batteries")
private List<BatteryDetail> batteries;
}

View File

@ -0,0 +1,57 @@
package com.ruoyi.device.domain.impl.djimqtt.model.drone;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* 红外测温区域
*
* @author ruoyi
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class IrMeteringArea {
/**
* 测温区域左上角点坐标 x
*/
@JsonProperty("x")
private Double x;
/**
* 测温区域左上角点坐标 y
*/
@JsonProperty("y")
private Double y;
/**
* 测温区域宽度
*/
@JsonProperty("width")
private Double width;
/**
* 测温区域高度
*/
@JsonProperty("height")
private Double height;
/**
* 测温区域平均温度
*/
@JsonProperty("aver_temperature")
private Double averTemperature;
/**
* 测温区域最低温度点
*/
@JsonProperty("min_temperature_point")
private TemperaturePoint minTemperaturePoint;
/**
* 测温区域最高温度点
*/
@JsonProperty("max_temperature_point")
private TemperaturePoint maxTemperaturePoint;
}

View File

@ -0,0 +1,33 @@
package com.ruoyi.device.domain.impl.djimqtt.model.drone;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* 红外测温点
*
* @author ruoyi
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class IrMeteringPoint {
/**
* 测温点坐标 x
*/
@JsonProperty("x")
private Double x;
/**
* 测温点坐标 y
*/
@JsonProperty("y")
private Double y;
/**
* 测温点的温度
*/
@JsonProperty("temperature")
private Double temperature;
}

View File

@ -0,0 +1,33 @@
package com.ruoyi.device.domain.impl.djimqtt.model.drone;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* 机场或遥控器对频信息叶子节点
*
* @author ruoyi
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class LeafNode {
/**
* 扰码信息
*/
@JsonProperty("sdr_id")
private Integer sdrId;
/**
* 设备sn
*/
@JsonProperty("sn")
private String sn;
/**
* 控制源序号
*/
@JsonProperty("control_source_index")
private Integer controlSourceIndex;
}

View File

@ -0,0 +1,39 @@
package com.ruoyi.device.domain.impl.djimqtt.model.drone;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* 视场角FOV liveview 中的区域
*
* @author ruoyi
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class LiveviewWorldRegion {
/**
* 左上角的 x 轴起始点
*/
@JsonProperty("left")
private Float left;
/**
* 左上角的 y 轴起始点
*/
@JsonProperty("top")
private Float top;
/**
* 右下角的 x 轴起始点
*/
@JsonProperty("right")
private Float right;
/**
* 右下角的 y 轴起始点
*/
@JsonProperty("bottom")
private Float bottom;
}

View File

@ -0,0 +1,23 @@
package com.ruoyi.device.domain.impl.djimqtt.model.drone;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.List;
/**
* 保养信息
*
* @author ruoyi
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class MaintainStatus {
/**
* 保养信息数组
*/
@JsonProperty("maintain_status_array")
private List<MaintainStatusItem> maintainStatusArray;
}

View File

@ -0,0 +1,45 @@
package com.ruoyi.device.domain.impl.djimqtt.model.drone;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* 保养信息项
*
* @author ruoyi
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class MaintainStatusItem {
/**
* 保养状态0-无保养1-有保养
*/
@JsonProperty("state")
private Integer state;
/**
* 上一次保养类型1-飞行器基础保养2-飞行器常规保养3-飞行器深度保养
*/
@JsonProperty("last_maintain_type")
private Integer lastMaintainType;
/**
* 上一次保养时间
*/
@JsonProperty("last_maintain_time")
private Long lastMaintainTime;
/**
* 上一次保养时飞行航时小时
*/
@JsonProperty("last_maintain_flight_time")
private Integer lastMaintainFlightTime;
/**
* 上一次保养时飞行架次
*/
@JsonProperty("last_maintain_flight_sorties")
private Integer lastMaintainFlightSorties;
}

View File

@ -0,0 +1,33 @@
package com.ruoyi.device.domain.impl.djimqtt.model.drone;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* 飞行器避障状态
*
* @author ruoyi
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class ObstacleAvoidance {
/**
* 水平避障状态0-关闭1-开启
*/
@JsonProperty("horizon")
private Integer horizon;
/**
* 上视避障状态0-关闭1-开启
*/
@JsonProperty("upside")
private Integer upside;
/**
* 下视避障状态0-关闭1-开启
*/
@JsonProperty("downside")
private Integer downside;
}

View File

@ -0,0 +1,39 @@
package com.ruoyi.device.domain.impl.djimqtt.model.drone;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* 搜星状态
*
* @author ruoyi
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class PositionState {
/**
* 是否收敛0-未开始1-收敛中2-收敛成功3-收敛失败
*/
@JsonProperty("is_fixed")
private Integer isFixed;
/**
* 搜星档位1-1档2-2档3-3档4-4档5-5档10-RTK fixed
*/
@JsonProperty("quality")
private Integer quality;
/**
* GPS 搜星数量
*/
@JsonProperty("gps_number")
private Integer gpsNumber;
/**
* RTK 搜星数量
*/
@JsonProperty("rtk_number")
private Integer rtkNumber;
}

View File

@ -0,0 +1,27 @@
package com.ruoyi.device.domain.impl.djimqtt.model.drone;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* 存储容量
*
* @author ruoyi
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class StorageInfo {
/**
* 总容量KB
*/
@JsonProperty("total")
private Integer total;
/**
* 已使用容量KB
*/
@JsonProperty("used")
private Integer used;
}

View File

@ -0,0 +1,33 @@
package com.ruoyi.device.domain.impl.djimqtt.model.drone;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* 温度点
*
* @author ruoyi
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class TemperaturePoint {
/**
* 温度点坐标 x
*/
@JsonProperty("x")
private Double x;
/**
* 温度点坐标 y
*/
@JsonProperty("y")
private Double y;
/**
* 温度点的温度
*/
@JsonProperty("temperature")
private Double temperature;
}

View File

@ -0,0 +1,35 @@
package com.ruoyi.device.domain.impl.djimqtt.model.drone;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.List;
/**
* 图传连接拓扑
*
* @author ruoyi
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class WirelessLinkTopo {
/**
* 加密编码
*/
@JsonProperty("secret_code")
private List<Integer> secretCode;
/**
* 飞行器对频信息
*/
@JsonProperty("center_node")
private CenterNode centerNode;
/**
* 当前连接的机场或遥控器对频信息
*/
@JsonProperty("leaf_nodes")
private List<LeafNode> leafNodes;
}

View File

@ -0,0 +1,190 @@
package com.ruoyi.device.domain.impl.djimqtt.service;
import com.ruoyi.device.domain.impl.djimqtt.config.DjiMqttClientConfig;
import com.ruoyi.device.domain.impl.djimqtt.handler.DjiMqttMessageHandler;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.paho.mqttv5.client.IMqttToken;
import org.eclipse.paho.mqttv5.client.MqttCallback;
import org.eclipse.paho.mqttv5.client.MqttClient;
import org.eclipse.paho.mqttv5.client.MqttConnectionOptions;
import org.eclipse.paho.mqttv5.client.MqttDisconnectResponse;
import org.eclipse.paho.mqttv5.client.persist.MemoryPersistence;
import org.eclipse.paho.mqttv5.common.MqttException;
import org.eclipse.paho.mqttv5.common.MqttMessage;
import org.eclipse.paho.mqttv5.common.packet.MqttProperties;
/**
* DJI MQTT客户端服务基于Eclipse Paho MQTT v5
* 支持MQTT 5.0协议动态创建多个客户端
*
* @author ruoyi
*/
@Slf4j
public class DjiMqttClientService {
private final DjiMqttClientConfig config;
private final DjiMqttMessageHandler messageHandler;
private MqttClient mqttClient;
/**
* 无人机OSD主题
*/
private static final String DRONE_OSD_TOPIC = "thing/product/+/osd";
/**
* 无人机State主题
*/
private static final String DRONE_STATE_TOPIC = "thing/product/+/state";
/**
* 构造函数
*
* @param config 客户端配置
* @param messageHandler 消息处理器
*/
public DjiMqttClientService(DjiMqttClientConfig config, DjiMqttMessageHandler messageHandler) {
this.config = config;
this.messageHandler = messageHandler;
}
/**
* 连接到MQTT服务器
*/
public void connect() {
try {
if (mqttClient != null && mqttClient.isConnected()) {
log.info("MQTT客户端[{}]已连接,无需重复连接", config.getClientId());
return;
}
String broker = String.format("tcp://%s:%d", config.getHost(), config.getPort());
log.info("开始连接DJI MQTT服务器[{}]: {}", config.getClientId(), broker);
mqttClient = new MqttClient(broker, config.getClientId(), new MemoryPersistence());
MqttConnectionOptions options = new MqttConnectionOptions();
options.setUserName(config.getUsername());
options.setPassword(config.getPassword().getBytes());
options.setConnectionTimeout(config.getConnectionTimeout());
options.setKeepAliveInterval(config.getKeepAliveInterval());
options.setAutomaticReconnect(config.getAutoReconnect());
options.setCleanStart(config.getCleanSession());
mqttClient.setCallback(new MqttCallback() {
@Override
public void disconnected(MqttDisconnectResponse disconnectResponse) {
log.error("MQTT客户端[{}]连接丢失: {}", config.getClientId(),
disconnectResponse.getReasonString());
if (config.getAutoReconnect()) {
log.info("MQTT客户端[{}]将自动重连...", config.getClientId());
}
}
@Override
public void mqttErrorOccurred(MqttException exception) {
log.error("MQTT客户端[{}]发生错误: {}", config.getClientId(),
exception.getMessage(), exception);
}
@Override
public void messageArrived(String topic, MqttMessage message) {
try {
String payload = new String(message.getPayload());
messageHandler.handleMessage(topic, payload);
} catch (Exception e) {
log.error("MQTT客户端[{}]处理消息失败: {}", config.getClientId(),
e.getMessage(), e);
}
}
@Override
public void deliveryComplete(IMqttToken token) {
// 不需要处理
}
@Override
public void connectComplete(boolean reconnect, String serverURI) {
if (reconnect) {
log.info("MQTT客户端[{}]重连成功: {}", config.getClientId(), serverURI);
// 重连后重新订阅
subscribe();
} else {
log.info("MQTT客户端[{}]首次连接成功: {}", config.getClientId(), serverURI);
}
}
@Override
public void authPacketArrived(int reasonCode, MqttProperties properties) {
// 不需要处理
}
});
mqttClient.connect(options);
log.info("MQTT客户端[{}]成功连接到服务器", config.getClientId());
subscribe();
} catch (Exception e) {
log.error("MQTT客户端[{}]连接失败: {}", config.getClientId(), e.getMessage(), e);
}
}
/**
* 订阅主题
*/
private void subscribe() {
try {
if (mqttClient == null || !mqttClient.isConnected()) {
log.warn("MQTT客户端[{}]未连接,无法订阅主题", config.getClientId());
return;
}
String osdTopic = config.getUseSharedSubscription()
? String.format("$share/%s/%s", config.getSharedGroupName(), DRONE_OSD_TOPIC)
: DRONE_OSD_TOPIC;
String stateTopic = config.getUseSharedSubscription()
? String.format("$share/%s/%s", config.getSharedGroupName(), DRONE_STATE_TOPIC)
: DRONE_STATE_TOPIC;
mqttClient.subscribe(osdTopic, 1);
log.info("MQTT客户端[{}]成功订阅主题: {}", config.getClientId(), osdTopic);
mqttClient.subscribe(stateTopic, 1);
log.info("MQTT客户端[{}]成功订阅主题: {}", config.getClientId(), stateTopic);
} catch (Exception e) {
log.error("MQTT客户端[{}]订阅主题失败: {}", config.getClientId(), e.getMessage(), e);
}
}
/**
* 断开连接
*/
public void disconnect() {
try {
if (mqttClient != null && mqttClient.isConnected()) {
mqttClient.disconnect();
mqttClient.close();
log.info("MQTT客户端[{}]已断开连接", config.getClientId());
}
} catch (Exception e) {
log.error("MQTT客户端[{}]断开连接失败: {}", config.getClientId(), e.getMessage(), e);
}
}
/**
* 检查连接状态
*/
public boolean isConnected() {
return mqttClient != null && mqttClient.isConnected();
}
/**
* 获取客户端ID
*/
public String getClientId() {
return config.getClientId();
}
}

View File

@ -0,0 +1,272 @@
package com.ruoyi.device.domain.impl.machine;
import com.ruoyi.device.domain.impl.machine.command.*;
import com.ruoyi.device.domain.impl.machine.instruction.InstructionContext;
import com.ruoyi.device.domain.impl.machine.mqtt.MqttClient;
import com.ruoyi.device.domain.impl.machine.state.MachineStates;
import com.ruoyi.device.domain.impl.machine.statemachine.MachineStateManager;
import com.ruoyi.device.domain.impl.machine.statemachine.StateChangeListener;
import com.ruoyi.device.domain.impl.machine.vendor.VendorConfig;
import com.ruoyi.device.domain.impl.machine.vendor.VendorRegistry;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
/**
* 设备命令管理器框架使用者的主要入口
*/
@Slf4j
@Component
public class MachineCommandManager {
private final VendorRegistry vendorRegistry;
private final MachineStateManager stateManager;
private final TransactionExecutor transactionExecutor;
private final MqttClient mqttClient;
/**
* SN -> 当前正在执行的命令
*/
private final Map<String, CommandExecution> executingCommands = new ConcurrentHashMap<>();
/**
* 命令执行监听器
*/
private final Map<String, CommandExecutionListener> commandListeners = new ConcurrentHashMap<>();
public MachineCommandManager(VendorRegistry vendorRegistry,
MachineStateManager stateManager,
TransactionExecutor transactionExecutor,
MqttClient mqttClient) {
this.vendorRegistry = vendorRegistry;
this.stateManager = stateManager;
this.transactionExecutor = transactionExecutor;
this.mqttClient = mqttClient;
}
/**
* 获取设备当前状态
*
* @param sn 设备SN号
* @return 设备状态
*/
public MachineStates getMachineStates(String sn) {
return stateManager.getStates(sn);
}
/**
* 更新设备状态通常在心跳中调用
*
* @param sn 设备SN号
* @param newStates 新状态
*/
public void updateMachineStates(String sn, MachineStates newStates,Boolean force) {
stateManager.updateStates(sn, newStates,force);
}
/**
* 判断设备是否正在执行命令
*
* @param sn 设备SN号
* @return 是否正在执行命令
*/
public boolean isExecutingCommand(String sn) {
CommandExecution execution = executingCommands.get(sn);
return execution != null && !execution.getFuture().isDone();
}
/**
* 获取设备当前正在执行的命令类型
*
* @param sn 设备SN号
* @return 命令类型如果没有正在执行的命令则返回null
*/
public CommandType getExecutingCommandType(String sn) {
CommandExecution execution = executingCommands.get(sn);
if (execution != null && !execution.getFuture().isDone()) {
return execution.getCommandType();
}
return null;
}
/**
* 获取设备在当前状态下可以执行的命令列表
*
* @param sn 设备SN号
* @return 可执行的命令列表
*/
public List<CommandType> getAvailableCommands(String sn) {
VendorConfig vendorConfig = vendorRegistry.getVendorConfig(sn);
if (vendorConfig == null) {
log.warn("设备未绑定厂家: sn={}", sn);
return List.of();
}
MachineStates currentStates = stateManager.getStates(sn);
return vendorConfig.getAvailableCommands(currentStates);
}
/**
* 执行命令
*
* @param sn 设备SN号
* @param commandType 命令类型
* @return 命令执行结果的Future
*/
public CompletableFuture<CommandResult> executeCommand(String sn, CommandType commandType) {
return executeCommand(sn, commandType, Map.of());
}
/**
* 执行命令带参数
*
* @param sn 设备SN号
* @param commandType 命令类型
* @param params 命令参数
* @return 命令执行结果的Future
*/
public CompletableFuture<CommandResult> executeCommand(String sn, CommandType commandType, Map<String, Object> params) {
log.info("收到命令执行请求: sn={}, commandType={}, params={}", sn, commandType, params);
// 1. 检查设备是否已绑定厂家
VendorConfig vendorConfig = vendorRegistry.getVendorConfig(sn);
if (vendorConfig == null) {
String error = "设备未绑定厂家";
log.error("{}: sn={}", error, sn);
return CompletableFuture.completedFuture(CommandResult.failure(commandType, error));
}
// 2. 检查是否正在执行其他命令
if (isExecutingCommand(sn)) {
String error = "设备正在执行其他命令: " + getExecutingCommandType(sn);
log.warn("{}: sn={}", error, sn);
return CompletableFuture.completedFuture(CommandResult.failure(commandType, error));
}
// 3. 检查当前状态是否可以执行该命令
MachineStates currentStates = stateManager.getStates(sn);
if (!vendorConfig.canExecuteCommand(currentStates, commandType)) {
String error = "当前状态不允许执行该命令";
log.warn("{}: sn={}, commandType={}, currentStates={}", error, sn, commandType, currentStates);
return CompletableFuture.completedFuture(CommandResult.failure(commandType, error));
}
// 4. 获取事务定义
Transaction transaction = vendorConfig.getTransaction(commandType);
if (transaction == null) {
String error = "厂家不支持该命令";
log.error("{}: sn={}, commandType={}, vendorType={}", error, sn, commandType, vendorConfig.getVendorType());
return CompletableFuture.completedFuture(CommandResult.failure(commandType, error));
}
// 5. 创建指令上下文
InstructionContext context = new InstructionContext(sn, vendorConfig.getVendorType(), mqttClient);
params.forEach(context::putCommandParam);
// 6. 执行事务
CompletableFuture<CommandResult> future = transactionExecutor.executeTransaction(transaction, context);
// 7. 记录正在执行的命令
executingCommands.put(sn, new CommandExecution(commandType, future, System.currentTimeMillis()));
// 8. 添加完成回调
future.whenComplete((result, throwable) -> {
executingCommands.remove(sn);
if (throwable != null) {
log.error("命令执行异常: sn={}, commandType={}", sn, commandType, throwable);
notifyCommandComplete(sn, CommandResult.failure(commandType, "命令执行异常: " + throwable.getMessage()));
} else {
log.info("命令执行完成: sn={}, commandType={}, success={}", sn, commandType, result.isSuccess());
notifyCommandComplete(sn, result);
}
});
return future;
}
/**
* 注册命令执行监听器
*
* @param listenerId 监听器ID
* @param listener 监听器
*/
public void registerCommandListener(String listenerId, CommandExecutionListener listener) {
commandListeners.put(listenerId, listener);
log.debug("注册命令执行监听器: listenerId={}", listenerId);
}
/**
* 取消注册命令执行监听器
*
* @param listenerId 监听器ID
*/
public void unregisterCommandListener(String listenerId) {
commandListeners.remove(listenerId);
log.debug("取消注册命令执行监听器: listenerId={}", listenerId);
}
/**
* 注册状态变化监听器
*
* @param listenerId 监听器ID
* @param listener 监听器
*/
public void registerStateChangeListener(String listenerId, StateChangeListener listener) {
stateManager.registerStateChangeListener(listenerId, listener);
}
/**
* 取消注册状态变化监听器
*
* @param listenerId 监听器ID
*/
public void unregisterStateChangeListener(String listenerId) {
stateManager.unregisterStateChangeListener(listenerId);
}
/**
* 通知命令执行完成
*/
private void notifyCommandComplete(String sn, CommandResult result) {
for (CommandExecutionListener listener : commandListeners.values()) {
try {
listener.onCommandComplete(sn, result);
} catch (Exception e) {
log.error("命令执行监听器执行失败: sn={}, commandType={}", sn, result.getCommandType(), e);
}
}
}
/**
* 命令执行信息
*/
private static class CommandExecution {
private final CommandType commandType;
private final CompletableFuture<CommandResult> future;
private final long startTime;
public CommandExecution(CommandType commandType, CompletableFuture<CommandResult> future, long startTime) {
this.commandType = commandType;
this.future = future;
this.startTime = startTime;
}
public CommandType getCommandType() {
return commandType;
}
public CompletableFuture<CommandResult> getFuture() {
return future;
}
public long getStartTime() {
return startTime;
}
}
}

View File

@ -0,0 +1,15 @@
package com.ruoyi.device.domain.impl.machine.command;
/**
* 命令执行监听器
*/
@FunctionalInterface
public interface CommandExecutionListener {
/**
* 命令执行完成回调
*
* @param sn 设备SN号
* @param result 命令执行结果
*/
void onCommandComplete(String sn, CommandResult result);
}

View File

@ -0,0 +1,58 @@
package com.ruoyi.device.domain.impl.machine.command;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 命令执行结果
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CommandResult {
/**
* 命令类型
*/
private CommandType commandType;
/**
* 是否成功
*/
private boolean success;
/**
* 错误信息
*/
private String errorMessage;
/**
* 失败的指令名称
*/
private String failedInstructionName;
/**
* 结果数据
*/
private Object data;
public static CommandResult success(CommandType commandType) {
return new CommandResult(commandType, true, null, null, null);
}
public static CommandResult success(CommandType commandType, Object data) {
return new CommandResult(commandType, true, null, null, data);
}
public static CommandResult failure(CommandType commandType, String errorMessage) {
return new CommandResult(commandType, false, errorMessage, null, null);
}
public static CommandResult failure(CommandType commandType, String errorMessage, String failedInstructionName) {
return new CommandResult(commandType, false, errorMessage, failedInstructionName, null);
}
public static CommandResult timeout(CommandType commandType) {
return new CommandResult(commandType, false, "命令执行超时", null, null);
}
}

View File

@ -0,0 +1,196 @@
package com.ruoyi.device.domain.impl.machine.command;
/**
* 命令类型枚举
*/
public enum CommandType {
/**
* 开机
*/
POWER_ON,
/**
* 关机
*/
POWER_OFF,
/**
* 起飞
*/
TAKE_OFF,
/**
* 返航
*/
RETURN_HOME,
/**
* 急停
*/
EMERGENCY_STOP,
/**
* 继续飞行
*/
RESUME_FLIGHT,
/**
* 指点飞行
*/
POINT_FLY,
/**
* 取消指点
*/
CANCEL_POINT,
/**
* 开始航线任务
*/
START_MISSION,
/**
* 暂停航线任务
*/
PAUSE_MISSION,
/**
* 恢复航线任务
*/
RESUME_MISSION,
/**
* 打开舱门
*/
OPEN_COVER,
/**
* 关闭舱门
*/
CLOSE_COVER,
/**
* 进入调试模式
*/
ENTER_DEBUG_MODE,
/**
* 退出调试模式
*/
EXIT_DEBUG_MODE,
/**
* 进入DRC模式
*/
ENTER_DRC_MODE,
/**
* 退出DRC模式
*/
EXIT_DRC_MODE,
/**
* 重启机巢
*/
REBOOT_AIRPORT,
/**
* 前进
*/
FORWARD,
/**
* 后退
*/
BACKWARD,
/**
* 左移
*/
LEFT,
/**
* 右移
*/
RIGHT,
/**
* 左旋
*/
ROTATE_LEFT,
/**
* 右旋
*/
ROTATE_RIGHT,
/**
* 上升
*/
UP,
/**
* 下降
*/
DOWN,
/**
* 切换可见光
*/
SWITCH_VISIBLE_LIGHT,
/**
* 云台变焦
*/
GIMBAL_ZOOM,
/**
* 切换红外
*/
SWITCH_IR,
/**
* 切换广角
*/
SWITCH_WIDE_ANGLE,
/**
* 云台右移
*/
GIMBAL_MOVE_RIGHT,
/**
* 云台左移
*/
GIMBAL_MOVE_LEFT,
/**
* 云台俯仰
*/
GIMBAL_PITCH_UP,
/**
* 云台俯仰
*/
GIMBAL_PITCH_DOWN,
/**
* 云台复位
*/
GIMBAL_RESET,
/**
* 航线飞行
*/
AIRLINE_FLIGHT,
/**
* 悬停
*/
HOVER,
/**
* 继续任务
*/
CONTINUE_TASK
}

View File

@ -0,0 +1,62 @@
package com.ruoyi.device.domain.impl.machine.command;
import com.ruoyi.device.domain.impl.machine.instruction.Instruction;
import lombok.Data;
/**
* 事务由多个指令组成的树状结构支持条件分支
*/
@Data
public class Transaction {
/**
* 事务名称
*/
private String name;
/**
* 命令类型
*/
private CommandType commandType;
/**
* 根指令事务的起始指令
*/
private Instruction rootInstruction;
/**
* 事务超时时间毫秒
*/
private long timeoutMs = 20000; // 默认10秒
public Transaction(String name, CommandType commandType) {
this.name = name;
this.commandType = commandType;
}
/**
* 设置根指令
*
* @param instruction 根指令
* @return Transaction 支持链式调用
*/
public Transaction root(Instruction instruction) {
this.rootInstruction = instruction;
return this;
}
/**
* 获取根指令
*/
public Instruction getRootInstruction() {
return rootInstruction;
}
/**
* 设置超时时间
*/
public Transaction setTimeout(long timeoutMs) {
this.timeoutMs = timeoutMs;
return this;
}
}

View File

@ -0,0 +1,337 @@
package com.ruoyi.device.domain.impl.machine.command;
import com.ruoyi.device.domain.impl.machine.instruction.CallbackConfig;
import com.ruoyi.device.domain.impl.machine.instruction.Instruction;
import com.ruoyi.device.domain.impl.machine.instruction.InstructionContext;
import com.ruoyi.device.domain.impl.machine.instruction.InstructionResult;
import com.ruoyi.device.domain.impl.machine.mqtt.MqttCallbackRegistry;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* 事务执行器完全异步化版本
*
* 设计说明
* 1. 完全异步不阻塞任何线程所有操作都通过 CompletableFuture 链式调用
* 2. 高并发可以同时处理数万个命令不会创建大量线程
* 3. 资源高效线程只在真正需要执行任务时才使用不会浪费在等待上
*
* 性能优势
* - 传统方式10万个命令 = 10万个阻塞线程 = 系统崩溃
* - 异步方式10万个命令 = 200个工作线程 + 10万个 CompletableFuture = 正常运行
*/
@Slf4j
@Component
public class TransactionExecutor {
private final MqttCallbackRegistry callbackRegistry;
private final Executor commandExecutor;
private final ScheduledExecutorService timeoutScheduler;
public TransactionExecutor(
MqttCallbackRegistry callbackRegistry,
@Qualifier("commandExecutor") Executor commandExecutor) {
this.callbackRegistry = callbackRegistry;
this.commandExecutor = commandExecutor;
// 创建一个专门用于超时检查的调度器核心线程数较小
this.timeoutScheduler = new ScheduledThreadPoolExecutor(
2,
r -> {
Thread t = new Thread(r, "timeout-scheduler");
t.setDaemon(true);
return t;
}
);
log.info("事务执行器初始化完成(完全异步模式)");
}
/**
* 执行事务完全异步
*
* @param transaction 事务定义
* @param context 执行上下文
* @return CompletableFuture不会阻塞调用线程
*/
public CompletableFuture<CommandResult> executeTransaction(Transaction transaction, InstructionContext context) {
log.info("开始执行事务: transaction={}, sn={}", transaction.getName(), context.getSn());
long startTime = System.currentTimeMillis();
// 直接返回异步执行的结果不创建新线程
return executeInstructionTreeAsync(transaction, context, startTime, transaction.getRootInstruction());
}
/**
* 异步执行指令树
*
* @param transaction 事务定义
* @param context 执行上下文
* @param startTime 事务开始时间
* @param currentInstruction 当前要执行的指令
* @return CompletableFuture
*/
private CompletableFuture<CommandResult> executeInstructionTreeAsync(
Transaction transaction,
InstructionContext context,
long startTime,
Instruction currentInstruction) {
// 检查根指令
if (currentInstruction == null) {
log.error("事务没有根指令: transaction={}", transaction.getName());
return CompletableFuture.completedFuture(
CommandResult.failure(transaction.getCommandType(), "事务没有根指令")
);
}
// 检查事务是否超时
if (System.currentTimeMillis() - startTime > transaction.getTimeoutMs()) {
log.warn("事务执行超时: transaction={}, sn={}", transaction.getName(), context.getSn());
return CompletableFuture.completedFuture(
CommandResult.timeout(transaction.getCommandType())
);
}
log.debug("执行指令: instruction={}", currentInstruction.getName());
// 异步执行当前指令
Instruction finalCurrentInstruction = currentInstruction;
return executeInstructionAsync(currentInstruction, context)
.thenCompose(result -> {
// 根据执行结果获取下游指令
Instruction nextInstruction = finalCurrentInstruction.getNextInstruction(result.isSuccess());
if (nextInstruction != null) {
// 有下游指令递归执行
log.debug("根据执行结果选择下游指令: success={}, nextInstruction={}",
result.isSuccess(), nextInstruction.getName());
return executeInstructionTreeAsync(transaction, context, startTime, nextInstruction);
} else {
// 没有下游指令当前指令的结果就是事务的结果
if (!result.isSuccess()) {
log.error("指令执行失败(无下游指令): instruction={}, error={}",
finalCurrentInstruction.getName(), result.getErrorMessage());
return CompletableFuture.completedFuture(
CommandResult.failure(
transaction.getCommandType(),
result.getErrorMessage(),
finalCurrentInstruction.getName()
)
);
} else {
log.info("指令执行成功(无下游指令),事务完成: instruction={}, sn={}",
finalCurrentInstruction.getName(), context.getSn());
return CompletableFuture.completedFuture(
CommandResult.success(transaction.getCommandType())
);
}
}
});
}
/**
* 异步执行单个指令
*
* @param instruction 指令
* @param context 执行上下文
* @return CompletableFuture<InstructionResult>
*/
private CompletableFuture<InstructionResult> executeInstructionAsync(
Instruction instruction,
InstructionContext context) {
log.debug("开始执行指令: instruction={}, sn={}", instruction.getName(), context.getSn());
// a. 判断是否可以执行
if (!instruction.canExecute(context)) {
String error = "指令被拒绝";
log.warn("指令不满足执行条件: instruction={}, sn={}", instruction.getName(), context.getSn());
InstructionResult result = InstructionResult.failure(error);
instruction.onComplete(context, result);
return CompletableFuture.completedFuture(result);
}
// b. 预先获取回调配置在发送命令前
CallbackConfig methodCallback = instruction.getMethodCallbackConfig(context);
CallbackConfig stateCallback = instruction.getStateCallbackConfig(context);
// 设置回调类型
if (methodCallback != null) {
methodCallback.setCallbackType(CallbackConfig.CallbackType.METHOD);
}
if (stateCallback != null) {
stateCallback.setCallbackType(CallbackConfig.CallbackType.STATE);
}
// c. 预先注册回调在发送命令前避免竞态条件
CompletableFuture<InstructionResult> methodFuture = null;
CompletableFuture<InstructionResult> stateFuture = null;
if (methodCallback != null) {
log.info("【预注册方法回调】instruction={}, topic={}",
instruction.getName(), methodCallback.getTopic());
methodFuture = waitForCallbackAsync(methodCallback, context);
}
if (stateCallback != null) {
log.info("【预注册状态回调】instruction={}, topic={}",
instruction.getName(), stateCallback.getTopic());
stateFuture = waitForCallbackAsync(stateCallback, context);
}
// d. 在线程池中执行远程调用回调已经注册好了
CompletableFuture<InstructionResult> finalMethodFuture = methodFuture;
CompletableFuture<InstructionResult> finalStateFuture = stateFuture;
return CompletableFuture.supplyAsync(() -> {
try {
instruction.executeRemoteCall(context);
log.debug("远程调用已发送: instruction={}", instruction.getName());
return true;
} catch (Exception e) {
log.error("远程调用失败: instruction={}, sn={}", instruction.getName(), context.getSn(), e);
return false;
}
}, commandExecutor).thenCompose(remoteCallSuccess -> {
if (!remoteCallSuccess) {
// 命令发送失败取消已注册的回调避免资源泄漏
if (finalMethodFuture != null) {
log.warn("命令发送失败,取消方法回调");
finalMethodFuture.cancel(true);
}
if (finalStateFuture != null) {
log.warn("命令发送失败,取消状态回调");
finalStateFuture.cancel(true);
}
InstructionResult result = InstructionResult.failure("远程调用失败");
instruction.onComplete(context, result);
return CompletableFuture.completedFuture(result);
}
// e. 等待方法回调已经预注册
if (finalMethodFuture != null) {
return finalMethodFuture.thenCompose(methodResult -> {
if (!methodResult.isSuccess()) {
instruction.onComplete(context, methodResult);
return CompletableFuture.completedFuture(methodResult);
}
// f. 等待状态回调已经预注册
if (finalStateFuture != null) {
return finalStateFuture.thenApply(stateResult -> {
instruction.onComplete(context, stateResult);
return stateResult;
});
}
// 没有状态回调直接成功
InstructionResult result = InstructionResult.success();
instruction.onComplete(context, result);
return CompletableFuture.completedFuture(result);
});
}
// 没有方法回调检查是否有状态回调
if (finalStateFuture != null) {
return finalStateFuture.thenApply(stateResult -> {
instruction.onComplete(context, stateResult);
return stateResult;
});
}
// 没有任何回调直接成功
InstructionResult result = InstructionResult.success();
instruction.onComplete(context, result);
return CompletableFuture.completedFuture(result);
});
}
/**
* 异步等待回调不阻塞线程
*
* 关键改进
* 1. 不使用 future.get() 阻塞线程
* 2. 使用 ScheduledExecutorService 实现超时
* 3. 完全基于回调机制
*
* @param callbackConfig 回调配置
* @param context 执行上下文
* @return CompletableFuture<InstructionResult>
*/
private CompletableFuture<InstructionResult> waitForCallbackAsync(
CallbackConfig callbackConfig,
InstructionContext context) {
CompletableFuture<InstructionResult> future = new CompletableFuture<>();
AtomicBoolean callbackReceived = new AtomicBoolean(false);
// 注册回调包含 tid/bid 过滤
String callbackId = callbackRegistry.registerCallback(
callbackConfig.getTopic(),
messageBody -> {
// 判断消息是否匹配
boolean matches = callbackConfig.matches(messageBody);
if (matches) {
// 匹配成功
if (callbackReceived.compareAndSet(false, true)) {
future.complete(InstructionResult.success(messageBody));
log.debug("收到匹配的回调消息: topic={}, type={}",
callbackConfig.getTopic(), callbackConfig.getCallbackType());
}
} else {
// 不匹配根据回调类型决定行为
if (callbackConfig.getCallbackType() == CallbackConfig.CallbackType.METHOD) {
// 方法回调不匹配就失败
if (callbackReceived.compareAndSet(false, true)) {
future.complete(InstructionResult.failure("方法回调不匹配"));
log.warn("方法回调不匹配,指令失败: topic={}, expected={}, actual={}",
callbackConfig.getTopic(),
callbackConfig.getExpectedValue(),
messageBody);
}
} else {
// 状态回调不匹配继续等待
// 使用 CAS 确保只处理一次然后重置状态
if (callbackReceived.compareAndSet(false, true)) {
callbackReceived.set(false); // 重置状态继续等待下一条消息
log.debug("状态回调不匹配,继续等待: topic={}, expected={}, actual={}",
callbackConfig.getTopic(),
callbackConfig.getExpectedValue(),
messageBody);
}
}
}
},
callbackConfig.getTimeoutMs(),
callbackConfig.getTidFieldPath(),
callbackConfig.getExpectedTid(),
callbackConfig.getBidFieldPath(),
callbackConfig.getExpectedBid()
);
// 设置超时不阻塞线程
timeoutScheduler.schedule(() -> {
// 使用 CAS 确保只处理一次
if (callbackReceived.compareAndSet(false, true)) {
future.complete(InstructionResult.timeout());
log.warn("等待回调超时: topic={}, timeout={}ms",
callbackConfig.getTopic(), callbackConfig.getTimeoutMs());
}
}, callbackConfig.getTimeoutMs(), TimeUnit.MILLISECONDS);
// 清理回调无论成功还是超时
return future.whenComplete((result, throwable) -> {
callbackRegistry.unregisterCallback(callbackId);
});
}
}

View File

@ -0,0 +1,72 @@
package com.ruoyi.device.domain.impl.machine.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
/**
* 线程池配置
* 用于命令执行的异步任务
*/
@Slf4j
@Configuration
public class ExecutorConfig {
/**
* 命令执行线程池
*
* 设计说明
* 1. 核心线程数CPU 核心数 * 2适合 I/O 密集型任务
* 2. 最大线程数200控制最大并发防止线程爆炸
* 3. 队列容量10000缓冲等待执行的任务
* 4. 拒绝策略CallerRunsPolicy背压机制让调用者线程执行
*
* 性能预估
* - 假设每个命令平均执行 10
* - 200 个线程可以同时处理 200 个命令
* - 队列可以缓冲 10000 个命令
* - 总容量10200 个并发命令
* - 吞吐量200 / 10 = 20 个命令/
*/
@Bean(name = "commandExecutor")
public Executor commandExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 核心线程数根据 CPU 核心数设置
int corePoolSize = Runtime.getRuntime().availableProcessors() * 2;
executor.setCorePoolSize(corePoolSize);
log.info("命令执行线程池核心线程数: {}", corePoolSize);
// 最大线程数限制最大并发防止线程爆炸
executor.setMaxPoolSize(200);
// 队列容量缓冲等待执行的任务
executor.setQueueCapacity(10000);
// 拒绝策略队列满时调用者线程执行背压机制
// 这样可以防止任务丢失同时给系统施加背压
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 线程名称前缀方便日志追踪
executor.setThreadNamePrefix("cmd-exec-");
// 等待任务完成后再关闭优雅关闭
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(60);
// 允许核心线程超时节省资源
executor.setAllowCoreThreadTimeOut(true);
executor.setKeepAliveSeconds(60);
executor.initialize();
log.info("命令执行线程池初始化完成: corePoolSize={}, maxPoolSize={}, queueCapacity={}",
corePoolSize, 200, 10000);
return executor;
}
}

View File

@ -0,0 +1,36 @@
package com.ruoyi.device.domain.impl.machine.config;
import com.ruoyi.device.domain.impl.machine.vendor.VendorRegistry;
import com.ruoyi.device.domain.impl.machine.vendor.dji.DjiVendorConfig;
import com.ruoyi.device.domain.impl.machine.vendor.tuoheng.TuohengVendorConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 设备框架配置类
*/
@Slf4j
@Configuration
public class MachineFrameworkConfig {
/**
* 自动注册所有厂家配置
*/
@Bean
public CommandLineRunner registerVendors(VendorRegistry vendorRegistry,
DjiVendorConfig djiVendorConfig,
TuohengVendorConfig tuohengVendorConfig) {
return args -> {
// 注册大疆厂家配置
vendorRegistry.registerVendor(djiVendorConfig);
// 注册拓恒厂家配置
vendorRegistry.registerVendor(tuohengVendorConfig);
log.info("设备框架初始化完成,已注册厂家: {}", vendorRegistry.getAllVendorTypes());
};
}
}

View File

@ -0,0 +1,30 @@
package com.ruoyi.device.domain.impl.machine.config;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
/**
* Redis 配置类
* 用于配置 Redis Pub/Sub 相关的 Bean
*/
@Configuration
@ConditionalOnProperty(name = "machine.state.store.type", havingValue = "redis")
public class RedisConfig {
/**
* 创建 Redis 消息监听容器专用于机器框架的 MQTT 回调
* 用于 Redis Pub/Sub 功能
*
* 注意使用特定的 Bean 名称避免与其他模块冲突
*/
@Bean(name = "machineFrameworkRedisMessageListenerContainer")
public RedisMessageListenerContainer machineFrameworkRedisMessageListenerContainer(
RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
return container;
}
}

View File

@ -0,0 +1,57 @@
package com.ruoyi.device.domain.impl.machine.instruction;
/**
* 抽象指令基类提供默认实现和下游节点管理
*/
public abstract class AbstractInstruction implements Instruction {
/**
* 成功后执行的下一个指令
*/
private Instruction onSuccessInstruction;
/**
* 失败后执行的下一个指令
*/
private Instruction onFailureInstruction;
@Override
public Instruction getOnSuccessInstruction() {
return onSuccessInstruction;
}
@Override
public Instruction getOnFailureInstruction() {
return onFailureInstruction;
}
/**
* 设置成功后执行的指令支持链式调用
*/
public <T extends AbstractInstruction> T onSuccess(Instruction instruction) {
this.onSuccessInstruction = instruction;
return (T) this;
}
/**
* 设置失败后执行的指令支持链式调用
*/
public <T extends AbstractInstruction> T onFailure(Instruction instruction) {
this.onFailureInstruction = instruction;
return (T) this;
}
/**
* 设置无论成功失败都执行的指令支持链式调用
*/
public <T extends AbstractInstruction> T then(Instruction instruction) {
this.onFailureInstruction = instruction;
this.onSuccessInstruction = instruction;
return (T) this;
}
}

View File

@ -0,0 +1,174 @@
package com.ruoyi.device.domain.impl.machine.instruction;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.function.Predicate;
/**
* 回调配置用于方法回调和状态回调
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CallbackConfig {
/**
* 回调类型枚举
*/
public enum CallbackType {
/**
* 方法回调设备对指令的直接响应
* - 收到匹配的响应 成功
* - 收到不匹配的响应 失败立即
* - 超时 失败
*/
METHOD,
/**
* 状态回调等待设备状态变化
* - 收到匹配的状态 成功
* - 收到不匹配的状态 继续等待
* - 超时 失败
*/
STATE
}
/**
* 监听的MQTT主题
*/
private String topic;
/**
* 字段路径支持嵌套 "data.status"
*/
private String fieldPath;
/**
* 期望的字段值
*/
private Object expectedValue;
/**
* 自定义判断逻辑如果设置则优先使用此逻辑
*/
private Predicate<Object> customPredicate;
/**
* 超时时间毫秒
*/
@Builder.Default
private long timeoutMs = 10000;
/**
* 事务ID字段路径用于匹配回调消息 "tid"
*/
private String tidFieldPath;
/**
* 业务ID字段路径用于匹配回调消息 "bid"
*/
private String bidFieldPath;
/**
* 期望的事务ID值从InstructionContext中获取
*/
private String expectedTid;
/**
* 期望的业务ID值从InstructionContext中获取
*/
private String expectedBid;
/**
* 回调类型由框架自动设置不需要手动指定
* - getMethodCallbackConfig() 返回的配置会被设置为 METHOD
* - getStateCallbackConfig() 返回的配置会被设置为 STATE
*/
private CallbackType callbackType;
/**
* 判断消息是否匹配
* 注意tid/bid 的匹配已经在 MqttCallbackRegistry 注册层完成这里只检查业务字段
*/
public boolean matches(Object messageBody) {
if (customPredicate != null) {
return customPredicate.test(messageBody);
}
// 检查业务字段是否匹配
Object fieldValue = extractFieldValue(messageBody, fieldPath);
return expectedValue == null || flexibleEquals(expectedValue, fieldValue);
}
/**
* 灵活的相等性比较支持字符串和数字之间的自动转换
* 例如flexibleEquals("2", 2) 返回 true
*/
private boolean flexibleEquals(Object expected, Object actual) {
if (expected == null && actual == null) {
return true;
}
if (expected == null || actual == null) {
return false;
}
// 如果类型相同直接比较
if (expected.equals(actual)) {
return true;
}
// 尝试将两者都转换为字符串进行比较
String expectedStr = String.valueOf(expected);
String actualStr = String.valueOf(actual);
return expectedStr.equals(actualStr);
}
/**
* 从消息体中提取字段值
*/
private Object extractFieldValue(Object messageBody, String path) {
if (messageBody == null || path == null) {
return null;
}
// 如果 messageBody 是字符串尝试解析为 JSON
Object current = messageBody;
if (messageBody instanceof String) {
try {
ObjectMapper objectMapper = new ObjectMapper();
current = objectMapper.readValue((String) messageBody, Object.class);
} catch (Exception e) {
// 解析失败返回 null
return null;
}
}
String[] parts = path.split("\\.");
for (String part : parts) {
if (current == null) {
return null;
}
if (current instanceof java.util.Map) {
current = ((java.util.Map<?, ?>) current).get(part);
} else {
try {
java.lang.reflect.Field field = current.getClass().getDeclaredField(part);
field.setAccessible(true);
current = field.get(current);
} catch (Exception e) {
return null;
}
}
}
return current;
}
}

View File

@ -0,0 +1,87 @@
package com.ruoyi.device.domain.impl.machine.instruction;
/**
* 指令接口
* 一个指令包含四个部分
* a. 判断是否可以执行该指令
* b. 执行远程调用如MQTT发送
* c. 等待方法回调并判断该方法是否成功执行
* d. 等待状态回调并判断结果是否OK
*/
public interface Instruction {
/**
* 获取指令名称
*/
String getName();
/**
* a. 判断是否可以执行该指令
*/
default boolean canExecute(InstructionContext context){
return true;
}
/**
* b. 执行远程调用如MQTT发送
*/
default void executeRemoteCall(InstructionContext context) throws Exception{
}
/**
* c. 获取方法回调配置可选
* 返回null表示不需要方法回调
*/
CallbackConfig getMethodCallbackConfig(InstructionContext context);
/**
* d. 获取状态回调配置可选
* 返回null表示不需要状态回调
*/
CallbackConfig getStateCallbackConfig(InstructionContext context);
/**
* 获取指令超时时间毫秒
*/
default long getTimeoutMs() {
return 1000; // 默认10秒
}
/**
* 指令执行完成回调无论成功失败都会调用
*/
default void onComplete(InstructionContext context, InstructionResult result) {
// 默认空实现
}
/**
* 获取成功后执行的下一个指令
*/
default Instruction getOnSuccessInstruction() {
return null;
}
/**
* 获取失败后执行的下一个指令
*/
default Instruction getOnFailureInstruction() {
return null;
}
/**
* 根据执行结果获取下一个指令
*
* @param success 是否成功
* @return 下一个指令如果没有则返回null
*/
default Instruction getNextInstruction(boolean success) {
// 根据成功失败返回对应的指令
if (success) {
return getOnSuccessInstruction();
} else {
return getOnFailureInstruction();
}
}
}

View File

@ -0,0 +1,104 @@
package com.ruoyi.device.domain.impl.machine.instruction;
import com.ruoyi.device.domain.impl.machine.mqtt.MqttClient;
import lombok.Data;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* 指令执行上下文
*/
@Data
public class InstructionContext {
/**
* 设备SN号
*/
private String sn;
/**
* 厂家类型
*/
private String vendorType;
/**
* MQTT客户端用于发送MQTT消息
*/
private MqttClient mqttClient;
/**
* 上下文数据用于在指令间传递数据
*/
private Map<String, Object> contextData = new HashMap<>();
/**
* 命令参数
*/
private Map<String, Object> commandParams = new HashMap<>();
/**
* 事务IDTransaction ID- 用于匹配回调消息
* 在命令执行阶段生成用于标识本次指令执行
*/
private String tid;
/**
* 业务IDBusiness ID- 用于匹配回调消息
* 在命令执行阶段生成用于标识本次业务操作
*/
private String bid;
public InstructionContext(String sn, String vendorType) {
this.sn = sn;
this.vendorType = vendorType;
// 自动生成 tid bid
this.tid = UUID.randomUUID().toString();
this.bid = UUID.randomUUID().toString();
}
public InstructionContext(String sn, String vendorType, MqttClient mqttClient) {
this.sn = sn;
this.vendorType = vendorType;
this.mqttClient = mqttClient;
// 自动生成 tid bid
this.tid = UUID.randomUUID().toString();
this.bid = UUID.randomUUID().toString();
}
public void putContextData(String key, Object value) {
contextData.put(key, value);
}
public Object getContextData(String key) {
return contextData.get(key);
}
public void putCommandParam(String key, Object value) {
commandParams.put(key, value);
}
public Object getCommandParam(String key) {
return commandParams.get(key);
}
/**
* 获取命令参数并转换为指定类型
* @param key 参数键
* @param type 目标类型
* @param <T> 泛型类型
* @return 转换后的参数值如果不存在或类型不匹配则返回null
*/
@SuppressWarnings("unchecked")
public <T> T getCommandParam(String key, Class<T> type) {
Object value = commandParams.get(key);
if (value == null) {
return null;
}
if (type.isInstance(value)) {
return (T) value;
}
return null;
}
}

View File

@ -0,0 +1,44 @@
package com.ruoyi.device.domain.impl.machine.instruction;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 指令执行结果
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class InstructionResult {
/**
* 是否成功
*/
private boolean success;
/**
* 错误信息
*/
private String errorMessage;
/**
* 结果数据
*/
private Object data;
public static InstructionResult success() {
return new InstructionResult(true, null, null);
}
public static InstructionResult success(Object data) {
return new InstructionResult(true, null, data);
}
public static InstructionResult failure(String errorMessage) {
return new InstructionResult(false, errorMessage, null);
}
public static InstructionResult timeout() {
return new InstructionResult(false, "指令执行超时", null);
}
}

View File

@ -0,0 +1,45 @@
package com.ruoyi.device.domain.impl.machine.mqtt;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.function.Consumer;
/**
* MQTT回调处理器
*/
@Data
@AllArgsConstructor
public class MqttCallbackHandler {
/**
* 回调ID用于取消注册
*/
private String callbackId;
/**
* 监听的主题
*/
private String topic;
/**
* 消息处理器
*/
private Consumer<Object> messageHandler;
/**
* 超时时间毫秒
*/
private long timeoutMs;
/**
* 注册时间
*/
private long registerTime;
/**
* 是否已超时
*/
public boolean isTimeout() {
return System.currentTimeMillis() - registerTime > timeoutMs;
}
}

View File

@ -0,0 +1,371 @@
package com.ruoyi.device.domain.impl.machine.mqtt;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.device.domain.impl.machine.mqtt.store.MqttCallbackInfo;
import com.ruoyi.device.domain.impl.machine.mqtt.store.MqttCallbackStore;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.net.InetAddress;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
/**
* MQTT回调注册中心
* 用于注册和管理MQTT消息的回调处理器,他的 handleMessage 需要被真实的MQTT回调去调用
*
* 架构说明
* - 回调元数据存储在 MqttCallbackStore 支持内存/Redis
* - Consumer<Object> 回调函数存储在本地内存中无法序列化
* - 多节点部署时通过 Redis Pub/Sub 在节点间传递消息
*/
@Slf4j
@Component
public class MqttCallbackRegistry {
/**
* 回调存储层支持内存Redis等多种实现
*/
private final MqttCallbackStore callbackStore;
/**
* 回调ID -> 本地消息处理器Consumer 无法序列化只能存储在本地
*/
private final Map<String, Consumer<Object>> localHandlers = new ConcurrentHashMap<>();
/**
* 当前节点ID用于 Redis Pub/Sub 路由
*/
private String nodeId;
/**
* ObjectMapper 用于序列化消息
*/
private final ObjectMapper objectMapper = new ObjectMapper();
@Value("${machine.node.id:#{null}}")
private String configuredNodeId;
public MqttCallbackRegistry(MqttCallbackStore callbackStore) {
this.callbackStore = callbackStore;
}
@PostConstruct
public void init() {
// 初始化节点ID
if (configuredNodeId != null && !configuredNodeId.isEmpty()) {
nodeId = configuredNodeId;
} else {
// 自动生成节点ID主机名 + UUID
try {
String hostname = InetAddress.getLocalHost().getHostName();
nodeId = hostname + "-" + UUID.randomUUID().toString().substring(0, 8);
} catch (Exception e) {
nodeId = "node-" + UUID.randomUUID().toString().substring(0, 8);
}
}
// 订阅当前节点的消息用于 Redis Pub/Sub
callbackStore.subscribeNodeMessages(nodeId, this::handleNodeMessage);
log.info("MQTT回调注册中心初始化完成节点ID: {}, 存储实现: {}",
nodeId, callbackStore.getClass().getSimpleName());
}
/**
* 注册回调
*
* @param topic 监听的主题
* @param messageHandler 消息处理器
* @param timeoutMs 超时时间毫秒
* @return 回调ID用于取消注册
*/
public String registerCallback(String topic, Consumer<Object> messageHandler, long timeoutMs) {
return registerCallback(topic, messageHandler, timeoutMs, null, null, null, null);
}
/**
* 注册回调支持 tid/bid 过滤
*
* @param topic 监听的主题
* @param messageHandler 消息处理器
* @param timeoutMs 超时时间毫秒
* @param tidFieldPath tid 字段路径 "tid"
* @param expectedTid 期望的 tid
* @param bidFieldPath bid 字段路径 "bid"
* @param expectedBid 期望的 bid
* @return 回调ID用于取消注册
*/
public String registerCallback(String topic, Consumer<Object> messageHandler, long timeoutMs,
String tidFieldPath, String expectedTid,
String bidFieldPath, String expectedBid) {
String callbackId = UUID.randomUUID().toString();
// 1. 创建回调信息并存储到存储层
MqttCallbackInfo callbackInfo = MqttCallbackInfo.builder()
.callbackId(callbackId)
.topic(topic)
.timeoutMs(timeoutMs)
.registerTime(System.currentTimeMillis())
.nodeId(nodeId)
.tidFieldPath(tidFieldPath)
.expectedTid(expectedTid)
.bidFieldPath(bidFieldPath)
.expectedBid(expectedBid)
.build();
callbackStore.registerCallback(callbackInfo);
// 2. Consumer 存储到本地内存
localHandlers.put(callbackId, messageHandler);
log.debug("注册MQTT回调: callbackId={}, topic={}, timeoutMs={}, nodeId={}, tid={}, bid={}",
callbackId, topic, timeoutMs, nodeId, expectedTid, expectedBid);
return callbackId;
}
/**
* 取消注册回调
*
* @param callbackId 回调ID
*/
public void unregisterCallback(String callbackId) {
// 1. 从存储层删除回调信息
callbackStore.unregisterCallback(callbackId);
// 2. 从本地内存删除 Consumer
localHandlers.remove(callbackId);
log.debug("取消注册MQTT回调: callbackId={}", callbackId);
}
/**
* 处理接收到的MQTT消息由真实的 MQTT 客户端调用
*
* @param topic 主题
* @param messageBody 消息体
*/
public void handleMessage(String topic, Object messageBody) {
// 1. 从存储层获取所有等待该 topic 的回调信息
List<MqttCallbackInfo> callbacks = callbackStore.getCallbacksByTopic(topic);
// 如果是 confirm realTime/data 消息打印详细日志
if (topic.contains("/control/confirm") || topic.contains("/realTime/data")) {
log.info("【Machine MqttCallbackRegistry】处理消息: topic={}, callbackCount={}, messageBody={}",
topic, callbacks.size(), messageBody);
}
if (callbacks.isEmpty()) {
if (topic.contains("/control/confirm") || topic.contains("/realTime/data")) {
log.debug("【Machine MqttCallbackRegistry】没有找到匹配的回调: topic={}", topic);
}
return;
}
log.debug("处理MQTT消息: topic={}, callbackCount={}", topic, callbacks.size());
// 2. 序列化消息体用于跨节点传递
String messageBodyJson;
try {
messageBodyJson = objectMapper.writeValueAsString(messageBody);
} catch (Exception e) {
log.error("序列化消息体失败: topic={}", topic, e);
return;
}
// 3. 处理每个回调
for (MqttCallbackInfo callbackInfo : callbacks) {
try {
// 检查是否超时
if (callbackInfo.isTimeout()) {
log.warn("MQTT回调已超时: callbackId={}, topic={}",
callbackInfo.getCallbackId(), topic);
unregisterCallback(callbackInfo.getCallbackId());
continue;
}
// 检查 tid/bid 是否匹配如果配置了
if (!matchesTidBid(callbackInfo, messageBody)) {
log.debug("MQTT消息 tid/bid 不匹配,跳过回调: callbackId={}, topic={}",
callbackInfo.getCallbackId(), topic);
continue;
}
// 判断回调是在本节点还是其他节点
if (nodeId.equals(callbackInfo.getNodeId())) {
// 本节点的回调直接执行
executeLocalCallback(callbackInfo.getCallbackId(), messageBody);
} else {
// 其他节点的回调通过 Redis Pub/Sub 转发
callbackStore.publishMessageToNode(
callbackInfo.getNodeId(),
callbackInfo.getCallbackId(),
messageBodyJson
);
log.debug("转发消息到节点: nodeId={}, callbackId={}",
callbackInfo.getNodeId(), callbackInfo.getCallbackId());
}
} catch (Exception e) {
log.error("处理MQTT回调失败: callbackId={}, topic={}",
callbackInfo.getCallbackId(), topic, e);
}
}
}
/**
* 检查消息的 tid/bid 是否匹配
*
* @param callbackInfo 回调信息
* @param messageBody 消息体
* @return true 如果匹配或未配置 tid/bidfalse 如果不匹配
*/
private boolean matchesTidBid(MqttCallbackInfo callbackInfo, Object messageBody) {
// 1. 检查 tid 是否匹配如果配置了
if (callbackInfo.getTidFieldPath() != null && callbackInfo.getExpectedTid() != null) {
Object tidValue = extractFieldValue(messageBody, callbackInfo.getTidFieldPath());
if (!callbackInfo.getExpectedTid().equals(tidValue)) {
log.debug("tid 不匹配: expected={}, actual={}", callbackInfo.getExpectedTid(), tidValue);
return false; // tid 不匹配
}
}
// 2. 检查 bid 是否匹配如果配置了
if (callbackInfo.getBidFieldPath() != null && callbackInfo.getExpectedBid() != null) {
Object bidValue = extractFieldValue(messageBody, callbackInfo.getBidFieldPath());
if (!callbackInfo.getExpectedBid().equals(bidValue)) {
log.debug("bid 不匹配: expected={}, actual={}", callbackInfo.getExpectedBid(), bidValue);
return false; // bid 不匹配
}
}
// 3. tid/bid 都匹配或未配置
return true;
}
/**
* 从消息体中提取字段值
*
* @param messageBody 消息体
* @param fieldPath 字段路径支持嵌套 "data.status"
* @return 字段值
*/
private Object extractFieldValue(Object messageBody, String fieldPath) {
if (messageBody == null || fieldPath == null) {
return null;
}
// 如果 messageBody 是字符串尝试解析为 JSON
Object current = messageBody;
if (messageBody instanceof String) {
try {
current = objectMapper.readValue((String) messageBody, Object.class);
} catch (Exception e) {
log.warn("解析消息体失败: {}", messageBody);
return null;
}
}
String[] parts = fieldPath.split("\\.");
for (String part : parts) {
if (current == null) {
return null;
}
if (current instanceof Map) {
current = ((Map<?, ?>) current).get(part);
} else {
try {
java.lang.reflect.Field field = current.getClass().getDeclaredField(part);
field.setAccessible(true);
current = field.get(current);
} catch (Exception e) {
log.debug("提取字段失败: fieldPath={}, part={}", fieldPath, part);
return null;
}
}
}
return current;
}
/**
* 执行本地回调
*
* @param callbackId 回调ID
* @param messageBody 消息体
*/
private void executeLocalCallback(String callbackId, Object messageBody) {
Consumer<Object> handler = localHandlers.get(callbackId);
if (handler != null) {
try {
handler.accept(messageBody);
log.debug("执行本地回调成功: callbackId={}", callbackId);
} catch (Exception e) {
log.error("执行本地回调失败: callbackId={}", callbackId, e);
}
} else {
log.warn("本地回调处理器不存在: callbackId={}", callbackId);
}
}
/**
* 处理从 Redis Pub/Sub 接收到的节点消息
*
* @param callbackId 回调ID
* @param messageBodyJson 消息体JSON 字符串
*/
private void handleNodeMessage(String callbackId, String messageBodyJson) {
try {
// 反序列化消息体
Object messageBody = objectMapper.readValue(messageBodyJson, Object.class);
// 执行本地回调
executeLocalCallback(callbackId, messageBody);
} catch (Exception e) {
log.error("处理节点消息失败: callbackId={}", callbackId, e);
}
}
/**
* 清理超时的回调
*/
public void cleanupTimeoutCallbacks() {
List<MqttCallbackInfo> allCallbacks = callbackStore.getAllCallbacks();
for (MqttCallbackInfo callbackInfo : allCallbacks) {
if (callbackInfo.isTimeout()) {
log.warn("清理超时的MQTT回调: callbackId={}, topic={}",
callbackInfo.getCallbackId(), callbackInfo.getTopic());
unregisterCallback(callbackInfo.getCallbackId());
}
}
}
/**
* 获取当前注册的回调数量
*/
public int getCallbackCount() {
return localHandlers.size();
}
/**
* 清理所有回调仅用于测试环境
* 警告此方法会清理所有回调包括未超时的仅应在测试环境中使用
*/
public void cleanupAllCallbacks() {
List<MqttCallbackInfo> allCallbacks = callbackStore.getAllCallbacks();
for (MqttCallbackInfo callbackInfo : allCallbacks) {
log.debug("清理MQTT回调: callbackId={}, topic={}",
callbackInfo.getCallbackId(), callbackInfo.getTopic());
unregisterCallback(callbackInfo.getCallbackId());
}
log.info("已清理所有MQTT回调共{}个", allCallbacks.size());
}
}

View File

@ -0,0 +1,35 @@
package com.ruoyi.device.domain.impl.machine.mqtt;
import com.ruoyi.device.domain.impl.tuohengmqtt.manager.TuohengMqttClientManager;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* MQTT客户端
*/
@Slf4j
@Component
public class MqttClient {
@Autowired
private TuohengMqttClientManager tuohengMqttClientManager;
public void sendMessage(String topic, String message) {
try {
log.info("发送MQTT消息: topic={}, message={}", topic, message);
// 使用拓恒MQTT客户端发送消息
if (tuohengMqttClientManager != null && tuohengMqttClientManager.isConnected()) {
tuohengMqttClientManager.getClient().publish(topic, message);
log.info("MQTT消息发送成功");
} else {
log.error("MQTT客户端未连接无法发送消息");
throw new RuntimeException("MQTT客户端未连接");
}
} catch (Exception e) {
log.error("发送MQTT消息失败: topic={}, message={}", topic, message, e);
throw new RuntimeException("发送MQTT消息失败: " + e.getMessage(), e);
}
}
}

View File

@ -0,0 +1,85 @@
package com.ruoyi.device.domain.impl.machine.mqtt.store;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;
/**
* 基于内存的 MQTT 回调存储实现
* 适用于单节点部署或开发测试环境
*/
@Slf4j
@Component
@ConditionalOnProperty(name = "machine.state.store.type", havingValue = "memory", matchIfMissing = true)
public class InMemoryMqttCallbackStore implements MqttCallbackStore {
/**
* 主题 -> 回调信息列表
*/
private final Map<String, CopyOnWriteArrayList<MqttCallbackInfo>> topicCallbacks = new ConcurrentHashMap<>();
/**
* 回调ID -> 回调信息
*/
private final Map<String, MqttCallbackInfo> callbackMap = new ConcurrentHashMap<>();
@Override
public void registerCallback(MqttCallbackInfo callbackInfo) {
topicCallbacks.computeIfAbsent(callbackInfo.getTopic(), k -> new CopyOnWriteArrayList<>())
.add(callbackInfo);
callbackMap.put(callbackInfo.getCallbackId(), callbackInfo);
log.debug("注册MQTT回调到内存: callbackId={}, topic={}",
callbackInfo.getCallbackId(), callbackInfo.getTopic());
}
@Override
public void unregisterCallback(String callbackId) {
MqttCallbackInfo callbackInfo = callbackMap.remove(callbackId);
if (callbackInfo != null) {
CopyOnWriteArrayList<MqttCallbackInfo> callbacks = topicCallbacks.get(callbackInfo.getTopic());
if (callbacks != null) {
callbacks.remove(callbackInfo);
if (callbacks.isEmpty()) {
topicCallbacks.remove(callbackInfo.getTopic());
}
}
log.debug("从内存中取消注册MQTT回调: callbackId={}, topic={}",
callbackId, callbackInfo.getTopic());
}
}
@Override
public List<MqttCallbackInfo> getCallbacksByTopic(String topic) {
CopyOnWriteArrayList<MqttCallbackInfo> callbacks = topicCallbacks.get(topic);
return callbacks != null ? new ArrayList<>(callbacks) : new ArrayList<>();
}
@Override
public MqttCallbackInfo getCallbackById(String callbackId) {
return callbackMap.get(callbackId);
}
@Override
public List<MqttCallbackInfo> getAllCallbacks() {
return new ArrayList<>(callbackMap.values());
}
@Override
public void publishMessageToNode(String nodeId, String callbackId, String messageBody) {
// 内存实现中不需要跨节点通信此方法为空操作
log.trace("内存实现不需要发布消息到节点: nodeId={}, callbackId={}", nodeId, callbackId);
}
@Override
public void subscribeNodeMessages(String nodeId, NodeMessageListener messageListener) {
// 内存实现中不需要订阅节点消息此方法为空操作
log.trace("内存实现不需要订阅节点消息: nodeId={}", nodeId);
}
}

View File

@ -0,0 +1,72 @@
package com.ruoyi.device.domain.impl.machine.mqtt.store;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* MQTT 回调信息可序列化到 Redis
* 不包含 Consumer只包含回调的元数据
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MqttCallbackInfo implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 回调ID用于取消注册
*/
private String callbackId;
/**
* 监听的主题
*/
private String topic;
/**
* 超时时间毫秒
*/
private long timeoutMs;
/**
* 注册时间
*/
private long registerTime;
/**
* 注册该回调的节点ID用于 Redis Pub/Sub 路由
*/
private String nodeId;
/**
* 事务ID字段路径用于匹配回调消息 "tid"
*/
private String tidFieldPath;
/**
* 期望的事务ID值
*/
private String expectedTid;
/**
* 业务ID字段路径用于匹配回调消息 "bid"
*/
private String bidFieldPath;
/**
* 期望的业务ID值
*/
private String expectedBid;
/**
* 是否已超时
*/
public boolean isTimeout() {
return System.currentTimeMillis() - registerTime > timeoutMs;
}
}

View File

@ -0,0 +1,79 @@
package com.ruoyi.device.domain.impl.machine.mqtt.store;
import java.util.List;
/**
* MQTT 回调存储接口
* 提供回调信息的存储和获取抽象支持多种实现内存Redis等
*/
public interface MqttCallbackStore {
/**
* 注册回调信息
*
* @param callbackInfo 回调信息
*/
void registerCallback(MqttCallbackInfo callbackInfo);
/**
* 取消注册回调
*
* @param callbackId 回调ID
*/
void unregisterCallback(String callbackId);
/**
* 根据 topic 获取所有等待该 topic 的回调信息
*
* @param topic MQTT 主题
* @return 回调信息列表
*/
List<MqttCallbackInfo> getCallbacksByTopic(String topic);
/**
* 根据 callbackId 获取回调信息
*
* @param callbackId 回调ID
* @return 回调信息如果不存在返回 null
*/
MqttCallbackInfo getCallbackById(String callbackId);
/**
* 获取所有回调信息用于清理超时回调
*
* @return 所有回调信息列表
*/
List<MqttCallbackInfo> getAllCallbacks();
/**
* 发布消息到指定节点用于 Redis Pub/Sub
* 在内存实现中此方法为空操作
*
* @param nodeId 节点ID
* @param callbackId 回调ID
* @param messageBody 消息体JSON 字符串
*/
void publishMessageToNode(String nodeId, String callbackId, String messageBody);
/**
* 订阅当前节点的消息用于 Redis Pub/Sub
* 在内存实现中此方法为空操作
*
* @param nodeId 当前节点ID
* @param messageListener 消息监听器
*/
void subscribeNodeMessages(String nodeId, NodeMessageListener messageListener);
/**
* 节点消息监听器
*/
interface NodeMessageListener {
/**
* 处理接收到的消息
*
* @param callbackId 回调ID
* @param messageBody 消息体JSON 字符串
*/
void onMessage(String callbackId, String messageBody);
}
}

View File

@ -0,0 +1,267 @@
package com.ruoyi.device.domain.impl.machine.mqtt.store;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* 基于 Redis MQTT 回调存储实现
* 适用于多节点部署的生产环境
*
* 架构说明
* 1. 回调信息存储在 Redis Hash mqtt:callback:{callbackId} -> MqttCallbackInfo (JSON)
* 2. Topic 索引存储在 Redis Set mqtt:topic:{topic} -> Set<callbackId>
* 3. 使用 Redis Pub/Sub 在节点间传递 MQTT 消息mqtt:node:{nodeId} -> {callbackId, messageBody}
*
* 工作流程
* - 节点A 注册回调 -> 存储到 Redis
* - 节点B 收到 MQTT 消息 -> Redis 查询等待该 topic 的回调 -> 通过 Pub/Sub 发送到对应节点
* - 节点A 收到 Pub/Sub 消息 -> 执行本地的 Consumer 回调
*
* 使用方式
* 1. application.properties 中配置machine.state.store.type=redis
* 2. 配置 Redis 连接信息
* 3. 实现 Redis 相关的序列化和 Pub/Sub 逻辑
*/
@Slf4j
@Component
@ConditionalOnProperty(name = "machine.state.store.type", havingValue = "redis")
public class RedisMqttCallbackStore implements MqttCallbackStore {
private final StringRedisTemplate stringRedisTemplate;
private final RedisMessageListenerContainer redisMessageListenerContainer;
private final ObjectMapper objectMapper;
// Redis key 前缀
private static final String CALLBACK_KEY_PREFIX = "mqtt:callback:";
private static final String TOPIC_INDEX_PREFIX = "mqtt:topic:";
private static final String NODE_CHANNEL_PREFIX = "mqtt:node:";
// 配置回调信息的过期时间
private static final long EXPIRE_SECONDS = 3600; // 1小时
/**
* Lua 脚本注册 MQTT 回调
* 使用 Lua 脚本保证原子性避免竞态条件
*
* KEYS[1]: Topic 索引 key (mqtt:topic:{topic})
* KEYS[2]: 回调信息 key (mqtt:callback:{callbackId})
* ARGV[1]: callbackId
* ARGV[2]: 过期时间
* ARGV[3]: 回调信息 JSON
*
* 返回值: 1 表示成功
*/
private static final String REGISTER_CALLBACK_SCRIPT =
"redis.call('SADD', KEYS[1], ARGV[1]) " +
"redis.call('EXPIRE', KEYS[1], ARGV[2]) " +
"redis.call('SETEX', KEYS[2], ARGV[2], ARGV[3]) " +
"return 1";
/**
* Lua 脚本取消注册 MQTT 回调
* 使用 Lua 脚本保证原子性
*
* KEYS[1]: Topic 索引 key (mqtt:topic:{topic})
* KEYS[2]: 回调信息 key (mqtt:callback:{callbackId})
* ARGV[1]: callbackId
*
* 返回值: 1 表示成功
*/
private static final String UNREGISTER_CALLBACK_SCRIPT =
"redis.call('SREM', KEYS[1], ARGV[1]) " +
"redis.call('DEL', KEYS[2]) " +
"return 1";
public RedisMqttCallbackStore(
StringRedisTemplate stringRedisTemplate,
@Qualifier("machineFrameworkRedisMessageListenerContainer") RedisMessageListenerContainer redisMessageListenerContainer,
ObjectMapper objectMapper) {
this.stringRedisTemplate = stringRedisTemplate;
this.redisMessageListenerContainer = redisMessageListenerContainer;
this.objectMapper = objectMapper;
log.info("使用 Redis MQTT 回调存储实现");
}
@Override
public void registerCallback(MqttCallbackInfo callbackInfo) {
try {
// 1. 序列化回调信息为 JSON
String json = objectMapper.writeValueAsString(callbackInfo);
// 2. 准备 Redis key
String topicKey = TOPIC_INDEX_PREFIX + callbackInfo.getTopic();
String callbackKey = CALLBACK_KEY_PREFIX + callbackInfo.getCallbackId();
// 3. 使用 Lua 脚本原子性地注册回调
// 先添加到 Topic 索引再存储回调信息避免竞态条件
stringRedisTemplate.execute(
new DefaultRedisScript<>(REGISTER_CALLBACK_SCRIPT, Long.class),
Arrays.asList(topicKey, callbackKey),
callbackInfo.getCallbackId(),
String.valueOf(EXPIRE_SECONDS),
json
);
log.debug("注册MQTT回调到Redis: callbackId={}, topic={}",
callbackInfo.getCallbackId(), callbackInfo.getTopic());
} catch (JsonProcessingException e) {
log.error("序列化回调信息失败: callbackId={}, topic={}",
callbackInfo.getCallbackId(), callbackInfo.getTopic(), e);
throw new RuntimeException("注册MQTT回调失败: 序列化错误", e);
} catch (Exception e) {
log.error("注册MQTT回调到Redis失败: callbackId={}, topic={}",
callbackInfo.getCallbackId(), callbackInfo.getTopic(), e);
// 不抛出异常让上层通过超时机制处理
// 这样可以避免因为 Redis 临时故障导致整个命令执行失败
}
}
@Override
public void unregisterCallback(String callbackId) {
try {
// 1. 获取回调信息需要知道 topic 才能删除索引
MqttCallbackInfo callbackInfo = getCallbackById(callbackId);
if (callbackInfo == null) {
log.debug("回调信息不存在,无需取消注册: callbackId={}", callbackId);
return;
}
// 2. 准备 Redis key
String topicKey = TOPIC_INDEX_PREFIX + callbackInfo.getTopic();
String callbackKey = CALLBACK_KEY_PREFIX + callbackId;
// 3. 使用 Lua 脚本原子性地取消注册回调
stringRedisTemplate.execute(
new DefaultRedisScript<>(UNREGISTER_CALLBACK_SCRIPT, Long.class),
Arrays.asList(topicKey, callbackKey),
callbackId
);
log.debug("从Redis中取消注册MQTT回调: callbackId={}, topic={}",
callbackId, callbackInfo.getTopic());
} catch (Exception e) {
log.error("从Redis中取消注册MQTT回调失败: callbackId={}", callbackId, e);
// 不抛出异常取消注册失败不影响主流程
// 回调会因为 TTL 自动过期
}
}
@Override
public List<MqttCallbackInfo> getCallbacksByTopic(String topic) {
// 1. Topic 索引获取所有 callbackId
String topicKey = TOPIC_INDEX_PREFIX + topic;
Set<String> callbackIds = stringRedisTemplate.opsForSet().members(topicKey);
if (callbackIds == null || callbackIds.isEmpty()) {
return new ArrayList<>();
}
// 2. 批量获取回调信息
List<MqttCallbackInfo> callbacks = new ArrayList<>();
for (String callbackId : callbackIds) {
MqttCallbackInfo callbackInfo = getCallbackById(callbackId);
if (callbackInfo != null) {
callbacks.add(callbackInfo);
}
}
return callbacks;
}
@Override
public MqttCallbackInfo getCallbackById(String callbackId) {
String callbackKey = CALLBACK_KEY_PREFIX + callbackId;
String json = stringRedisTemplate.opsForValue().get(callbackKey);
if (json == null) {
return null;
}
try {
return objectMapper.readValue(json, MqttCallbackInfo.class);
} catch (JsonProcessingException e) {
log.error("反序列化回调信息失败: callbackId={}", callbackId, e);
return null;
}
}
@Override
public List<MqttCallbackInfo> getAllCallbacks() {
// 1. 扫描所有 mqtt:callback:* key
Set<String> keys = stringRedisTemplate.keys(CALLBACK_KEY_PREFIX + "*");
if (keys == null || keys.isEmpty()) {
return new ArrayList<>();
}
// 2. 批量获取回调信息
List<MqttCallbackInfo> callbacks = new ArrayList<>();
for (String key : keys) {
String callbackId = key.substring(CALLBACK_KEY_PREFIX.length());
MqttCallbackInfo callbackInfo = getCallbackById(callbackId);
if (callbackInfo != null) {
callbacks.add(callbackInfo);
}
}
return callbacks;
}
@Override
public void publishMessageToNode(String nodeId, String callbackId, String messageBody) {
try {
// 1. 构造消息体
Map<String, String> message = new HashMap<>();
message.put("callbackId", callbackId);
message.put("messageBody", messageBody);
// 2. 序列化消息
String json = objectMapper.writeValueAsString(message);
// 3. 发布到节点频道
String channel = NODE_CHANNEL_PREFIX + nodeId;
stringRedisTemplate.convertAndSend(channel, json);
log.debug("发布消息到节点: nodeId={}, callbackId={}, channel={}",
nodeId, callbackId, channel);
} catch (JsonProcessingException e) {
log.error("序列化节点消息失败: nodeId={}, callbackId={}", nodeId, callbackId, e);
}
}
@Override
public void subscribeNodeMessages(String nodeId, NodeMessageListener messageListener) {
// 1. 创建消息监听器
MessageListener redisMessageListener = (message, pattern) -> {
try {
String json = new String(message.getBody());
Map<String, String> data = objectMapper.readValue(json,
new TypeReference<Map<String, String>>() {});
String callbackId = data.get("callbackId");
String messageBody = data.get("messageBody");
messageListener.onMessage(callbackId, messageBody);
} catch (Exception e) {
log.error("处理Redis Pub/Sub消息失败", e);
}
};
// 2. 订阅节点频道
String channel = NODE_CHANNEL_PREFIX + nodeId;
redisMessageListenerContainer.addMessageListener(redisMessageListener, new ChannelTopic(channel));
log.info("订阅节点消息: nodeId={}, channel={}", nodeId, channel);
}
}

View File

@ -0,0 +1,218 @@
生产需要实现
MysqlSnVendorMappingRepository 这个类
MQTT回调的地方需要转发到MqttCallbackRegistry 这个类
需要实现 MqttClient 这边消息发送的逻辑
需要配置 sn.repository.type=mysql
单节点部署(默认)
# 使用内存存储(默认配置)
machine.state.store.type=memory
# 以下配置生产需要修改为mysql同时实现 MysqlSnVendorMappingRepository 这个类
sn.repository.type=memory
多节点部署
# 切换到 Redis 存储
machine.state.store.type=redis
# 配置节点ID可选不配置会自动生成
machine.node.id=node-1
#本地启动redis
#docker run --name some-redis -d -p 6379:6379 redi
# Redis 配置
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=your-password
┌─────────────────────────────────────────────────────────────────┐
│ 步骤1: 节点A 执行命令并注册回调 │
└─────────────────────────────────────────────────────────────────┘
节点A: executeCommand(TAKE_OFF)
节点A: registerCallback(topic="dji/SN9527/response", ...)
节点A: MqttCallbackStore.registerCallback()
Redis: 存储回调信息(使用两个 Key 的原因:性能优化)
【Key 1】回调详细信息Hash 结构)
- Key: mqtt:callback:{callbackId}
- Value: {callbackId, topic, nodeId="nodeA", timeoutMs, registerTime, ...}
- 作用: 存储单个回调的完整信息
- 查询: O(1) 时间复杂度,通过 callbackId 直接获取
【Key 2】Topic 索引Set 结构)
- Key: mqtt:topic:dji/SN9527/response
- Value: Set<callbackId> // 例如: ["abc-123", "def-456", "ghi-789"]
- 作用: 快速查询等待某个 topic 的所有回调
- 查询: O(1) 时间复杂度,直接获取 callbackId 列表
【为什么需要两个 Key
如果只用一个 Key 存储所有回调,查询时需要遍历所有回调并过滤 topic
时间复杂度为 O(n)。使用 Topic 索引后,可以直接获取目标回调列表,
时间复杂度降为 O(1),大幅提升性能。
【示例】
假设有 3 个回调:
- callbackId="abc-123", topic="dji/SN9527/response", nodeId="nodeA"
- callbackId="def-456", topic="dji/SN9527/state", nodeId="nodeB"
- callbackId="ghi-789", topic="dji/SN9527/response", nodeId="nodeA"
Redis 存储结构:
mqtt:callback:abc-123 → {callbackId:"abc-123", topic:"dji/SN9527/response", nodeId:"nodeA"}
mqtt:callback:def-456 → {callbackId:"def-456", topic:"dji/SN9527/state", nodeId:"nodeB"}
mqtt:callback:ghi-789 → {callbackId:"ghi-789", topic:"dji/SN9527/response", nodeId:"nodeA"}
mqtt:topic:dji/SN9527/response → ["abc-123", "ghi-789"]
mqtt:topic:dji/SN9527/state → ["def-456"]
查询 topic="dji/SN9527/response" 的回调:
1. 从索引获取: SMEMBERS mqtt:topic:dji/SN9527/response → ["abc-123", "ghi-789"]
2. 批量获取详情: MGET mqtt:callback:abc-123 mqtt:callback:ghi-789
3. 总耗时: O(1) + O(k)k 是该 topic 的回调数量(通常很小)
【Redis 数据清理时机】
Redis 中的回调数据有两种清理机制:
┌─────────────────────────────────────────────────────────────┐
│ 1⃣ 主动清理(业务逻辑触发) │
└─────────────────────────────────────────────────────────────┘
触发时机:
✅ 回调成功执行后TransactionExecutor 的 finally 块)
✅ 回调超时后TransactionExecutor 的 finally 块)
✅ handleMessage 检测到超时(转发前检查)
清理操作:
unregisterCallback(callbackId)
1. 获取回调信息: GET mqtt:callback:{callbackId}
2. 删除回调信息: DEL mqtt:callback:{callbackId}
3. 从索引中移除: SREM mqtt:topic:{topic} {callbackId}
示例:
T0: 注册回调,超时时间 10 秒
T5: 收到 MQTT 响应,回调执行成功
T5: 立即清理 Redis 数据 ✅
- DEL mqtt:callback:abc-123
- SREM mqtt:topic:dji/SN9527/response abc-123
┌─────────────────────────────────────────────────────────────┐
│ 2⃣ 被动清理Redis TTL 自动过期) │
└─────────────────────────────────────────────────────────────┘
作用:兜底机制,防止异常情况下的数据残留
设置方式:
// 注册回调时设置 TTL
SET mqtt:callback:{callbackId} {json} EX 3600 // 1小时后自动过期
EXPIRE mqtt:topic:{topic} 3600 // 1小时后自动过期
触发时机:
⚠️ 应用异常崩溃,主动清理未执行
⚠️ 网络分区,无法删除 Redis 数据
⚠️ 代码 Bug主动清理失败
示例:
T0: 注册回调TTL=3600秒1小时
T5: 应用崩溃,主动清理未执行 ❌
T3600: Redis 自动删除过期数据 ✅
- mqtt:callback:abc-123 自动过期删除
- mqtt:topic:dji/SN9527/response 自动过期删除
【推荐配置】
TTL 应该设置为回调超时时间的 2-3 倍,例如:
- 回调超时: 10 秒
- Redis TTL: 30 秒10秒 × 3
这样可以确保:
✅ 正常情况下,主动清理会在 10 秒内完成
✅ 异常情况下Redis 会在 30 秒后自动清理
✅ 避免设置过长的 TTL 导致内存浪费
【注意事项】
⚠️ Topic 索引的 TTL 问题:
如果同一个 topic 有多个回调,每次添加新回调时都会刷新 TTL。
这可能导致索引的 TTL 比单个回调的 TTL 更长。
解决方案:
方案1: 不为 Topic 索引设置 TTL只在删除最后一个 callbackId 时删除索引
方案2: 每次查询时过滤掉已过期的 callbackId推荐
节点A: 本地内存存储 Consumer<Object>
- localHandlers.put(callbackId, consumer)
节点A: 订阅 Redis Pub/Sub 频道
- Channel: mqtt:node:nodeA
┌─────────────────────────────────────────────────────────────────┐
│ 步骤2: MQTT Broker 将响应路由到节点B不是节点A
└─────────────────────────────────────────────────────────────────┘
MQTT Broker: 收到设备响应
MQTT Broker: 将消息路由到节点B随机/轮询)
节点B: MqttCallbackRegistry.handleMessage(topic, messageBody)
┌─────────────────────────────────────────────────────────────────┐
│ 步骤3: 节点B 从 Redis 查询等待该 topic 的回调 │
└─────────────────────────────────────────────────────────────────┘
节点B: callbackStore.getCallbacksByTopic("dji/SN9527/response")
Redis: 查询 mqtt:topic:dji/SN9527/response
Redis: 返回 Set<callbackId>
Redis: 批量获取回调信息
- mqtt:callback:{callbackId1} → {nodeId="nodeA", ...}
- mqtt:callback:{callbackId2} → {nodeId="nodeA", ...}
节点B: 获得回调列表 List<MqttCallbackInfo>
┌─────────────────────────────────────────────────────────────────┐
│ 步骤4: 节点B 判断回调属于哪个节点 │
└─────────────────────────────────────────────────────────────────┘
节点B: for (MqttCallbackInfo callback : callbacks) {
if (nodeId.equals(callback.getNodeId())) {
// 本节点的回调,直接执行
executeLocalCallback(...)
} else {
// 其他节点的回调,转发到目标节点
callbackStore.publishMessageToNode(...)
}
}
┌─────────────────────────────────────────────────────────────────┐
│ 步骤5: 节点B 通过 Redis Pub/Sub 转发消息到节点A │
└─────────────────────────────────┘
节点B: callbackStore.publishMessageToNode(
nodeId="nodeA",
callbackId="xxx",
messageBody="{...}" // JSON 字符串
)
Redis Pub/Sub: PUBLISH mqtt:node:nodeA
{
"callbackId": "xxx",
"messageBody": "{...}"
}
┌─────────────────────────────────────────────────────────────────┐
│ 步骤6: 节点A 收到 Redis Pub/Sub 消息 │
└─────────────────────────────────────────────────────────────────┘
节点A: Redis Pub/Sub Listener 收到消息
节点A: handleNodeMessage(callbackId, messageBodyJson)
节点A: 反序列化消息体
- Object messageBody = objectMapper.readValue(messageBodyJson)
节点A: executeLocalCallback(callbackId, messageBody)
节点A: 从本地内存获取 Consumer
- Consumer<Object> handler = localHandlers.get(callbackId)
节点A: 执行回调
- handler.accept(messageBody)
✅ 命令执行成功!

View File

@ -0,0 +1,20 @@
package com.ruoyi.device.domain.impl.machine.state;
/**
* 机巢状态枚举
*/
public enum AirportState {
/**
* 未知状态服务器重启后的初始状态等待第一次心跳同步
*/
UNKNOWN,
/**
* 在线
*/
ONLINE,
/**
* 离线心跳超时
*/
OFFLINE
}

View File

@ -0,0 +1,22 @@
package com.ruoyi.device.domain.impl.machine.state;
/**
* 舱门状态枚举
*/
public enum CoverState {
/**
* 未知状态服务器重启后的初始状态等待第一次心跳同步
*/
UNKNOWN,
/**
* 舱门已关闭
*/
CLOSED,
/**
* 舱门已打开
*/
OPENED,
}

View File

@ -0,0 +1,21 @@
package com.ruoyi.device.domain.impl.machine.state;
/**
* 调试模式状态枚举
*/
public enum DebugModeState {
/**
* 未知状态
*/
UNKNOWN,
/**
* 调试模式
*/
ENTERED,
/**
* 退出调试模式
*/
EXITED
}

View File

@ -0,0 +1,23 @@
package com.ruoyi.device.domain.impl.machine.state;
/**
* 飞行控制模式DRC状态枚举
*/
public enum DrcState {
/**
* 未知状态服务器重启后的初始状态等待第一次心跳同步
*/
UNKNOWN,
/**
* 退出状态DRC模式已退出
*/
EXITED,
/**
* 进入状态已进入DRC模式
*/
ENTERED,
}

View File

@ -0,0 +1,38 @@
package com.ruoyi.device.domain.impl.machine.state;
/**
* 无人机状态枚举
* 分为准备中 -> 飞行中 -> 返航 三个大状态
*/
public enum DroneState {
/**
* 未知状态服务器重启后的初始状态等待第一次心跳同步
*/
UNKNOWN,
/**
* 关机状态
*/
POWER_OFF,
/**
* 在线已开机
*/
ONLINE,
/**
* 飞行中
*/
FLYING,
/**
* 到达目的地
*/
ARRIVED,
/**
* 返航中
*/
RETURNING,
}

View File

@ -0,0 +1,50 @@
package com.ruoyi.device.domain.impl.machine.state;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 设备的六套大状态
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MachineStates {
/**
* 无人机状态
*/
private DroneState droneState = DroneState.UNKNOWN;
/**
* 机巢状态
*/
private AirportState airportState = AirportState.UNKNOWN;
/**
* 舱门状态
*/
private CoverState coverState = CoverState.UNKNOWN;
/**
* DRC状态
*/
private DrcState drcState = DrcState.UNKNOWN;
/**
* 调试模式状态
*/
private DebugModeState debugModeState = DebugModeState.UNKNOWN;
/**
* 急停状态
*/
private StopState stopState = StopState.UNKNOWN;
/**
* 复制当前状态
*/
public MachineStates copy() {
return new MachineStates(droneState, airportState, coverState, drcState, debugModeState, stopState);
}
}

Some files were not shown because too many files have changed in this diff Show More