Эх сурвалжийг харах

初步完成地图相关功能

jiuling 4 өдөр өмнө
parent
commit
197170decc
63 өөрчлөгдсөн 7886 нэмэгдсэн , 78 устгасан
  1. 18 0
      ruoyi-admin/pom.xml
  2. 203 0
      ruoyi-admin/src/main/java/com/ruoyi/mqtt/QUICKSTART.md
  3. 75 2
      ruoyi-admin/src/main/java/com/ruoyi/mqtt/config/MqttConfig.java
  4. 307 0
      ruoyi-admin/src/main/java/com/ruoyi/mqtt/constant/MqttTopicConstants.java
  5. 175 30
      ruoyi-admin/src/main/java/com/ruoyi/mqtt/controller/MqttController.java
  6. 41 0
      ruoyi-admin/src/main/java/com/ruoyi/mqtt/dto/MqttBaseMessage.java
  7. 42 0
      ruoyi-admin/src/main/java/com/ruoyi/mqtt/dto/MqttResponseMessage.java
  8. 34 0
      ruoyi-admin/src/main/java/com/ruoyi/mqtt/dto/localization/InitPoseRequest.java
  9. 61 0
      ruoyi-admin/src/main/java/com/ruoyi/mqtt/dto/localization/PoseData.java
  10. 139 0
      ruoyi-admin/src/main/java/com/ruoyi/mqtt/dto/localization/RobotPoseInfo.java
  11. 35 0
      ruoyi-admin/src/main/java/com/ruoyi/mqtt/dto/localization/RtkData.java
  12. 45 0
      ruoyi-admin/src/main/java/com/ruoyi/mqtt/dto/localization/TimeData.java
  13. 35 0
      ruoyi-admin/src/main/java/com/ruoyi/mqtt/dto/localization/VelocityData.java
  14. 48 0
      ruoyi-admin/src/main/java/com/ruoyi/mqtt/dto/planning/PlanningRequest.java
  15. 61 0
      ruoyi-admin/src/main/java/com/ruoyi/mqtt/dto/planning/TrajectoryCompactData.java
  16. 75 0
      ruoyi-admin/src/main/java/com/ruoyi/mqtt/dto/task/ArriveEventInfo.java
  17. 48 0
      ruoyi-admin/src/main/java/com/ruoyi/mqtt/dto/task/TargetInfo.java
  18. 128 0
      ruoyi-admin/src/main/java/com/ruoyi/mqtt/dto/task/TaskRealtimeInfo.java
  19. 32 0
      ruoyi-admin/src/main/java/com/ruoyi/mqtt/enums/MqttTopicType.java
  20. 41 0
      ruoyi-admin/src/main/java/com/ruoyi/mqtt/enums/RobotRunState.java
  21. 44 0
      ruoyi-admin/src/main/java/com/ruoyi/mqtt/enums/RtkStatus.java
  22. 37 0
      ruoyi-admin/src/main/java/com/ruoyi/mqtt/enums/TaskErrorCode.java
  23. 500 29
      ruoyi-admin/src/main/java/com/ruoyi/mqtt/listener/MqttMessageListener.java
  24. 312 10
      ruoyi-admin/src/main/java/com/ruoyi/mqtt/service/MqttSendService.java
  25. 138 0
      ruoyi-admin/src/main/java/com/ruoyi/mqtt/util/MqttTopicUtil.java
  26. 38 0
      ruoyi-admin/src/main/java/com/ruoyi/robot/config/TaskSchedulerConfig.java
  27. 332 0
      ruoyi-admin/src/main/java/com/ruoyi/robot/task/LdTaskScheduler.java
  28. 198 0
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/robot/AsmController.java
  29. 538 0
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/robot/LdNavController.java
  30. 539 0
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/robot/LdTaskController.java
  31. 417 0
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/robot/RobotMapController.java
  32. 41 0
      ruoyi-admin/src/main/java/com/ruoyi/websocket/config/WebSocketConfig.java
  33. 107 0
      ruoyi-admin/src/main/java/com/ruoyi/websocket/controller/WebSocketController.java
  34. 384 0
      ruoyi-admin/src/main/java/com/ruoyi/websocket/service/WebSocketPushService.java
  35. 32 4
      ruoyi-admin/src/main/resources/application.yml
  36. 5 0
      ruoyi-common/pom.xml
  37. 97 0
      ruoyi-common/src/main/java/com/ruoyi/common/core/domain/LdMapProject.java
  38. 73 0
      ruoyi-common/src/main/java/com/ruoyi/common/core/domain/LdRoadmap.java
  39. 128 0
      ruoyi-common/src/main/java/com/ruoyi/common/core/domain/LdTilemapConfig.java
  40. 5 3
      ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java
  41. 308 0
      ruoyi-system/src/main/java/com/ruoyi/robot/domain/LdTask.java
  42. 246 0
      ruoyi-system/src/main/java/com/ruoyi/robot/domain/LdTaskExecutionLog.java
  43. 48 0
      ruoyi-system/src/main/java/com/ruoyi/robot/domain/vo/LdMapListVo.java
  44. 46 0
      ruoyi-system/src/main/java/com/ruoyi/robot/mapper/LdMapProjectMapper.java
  45. 19 0
      ruoyi-system/src/main/java/com/ruoyi/robot/mapper/LdRoadmapMapper.java
  46. 86 0
      ruoyi-system/src/main/java/com/ruoyi/robot/mapper/LdTaskExecutionLogMapper.java
  47. 119 0
      ruoyi-system/src/main/java/com/ruoyi/robot/mapper/LdTaskMapper.java
  48. 15 0
      ruoyi-system/src/main/java/com/ruoyi/robot/mapper/LdTilemapConfigMapper.java
  49. 53 0
      ruoyi-system/src/main/java/com/ruoyi/robot/service/ILdMapProjectService.java
  50. 24 0
      ruoyi-system/src/main/java/com/ruoyi/robot/service/ILdRoadmapService.java
  51. 84 0
      ruoyi-system/src/main/java/com/ruoyi/robot/service/ILdTaskExecutionLogService.java
  52. 110 0
      ruoyi-system/src/main/java/com/ruoyi/robot/service/ILdTaskService.java
  53. 24 0
      ruoyi-system/src/main/java/com/ruoyi/robot/service/ILdTilemapConfigService.java
  54. 110 0
      ruoyi-system/src/main/java/com/ruoyi/robot/service/impl/LdMapProjectServiceImpl.java
  55. 76 0
      ruoyi-system/src/main/java/com/ruoyi/robot/service/impl/LdRoadmapServiceImpl.java
  56. 129 0
      ruoyi-system/src/main/java/com/ruoyi/robot/service/impl/LdTaskExecutionLogServiceImpl.java
  57. 166 0
      ruoyi-system/src/main/java/com/ruoyi/robot/service/impl/LdTaskServiceImpl.java
  58. 81 0
      ruoyi-system/src/main/java/com/ruoyi/robot/service/impl/LdTilemapConfigServiceImpl.java
  59. 118 0
      ruoyi-system/src/main/resources/mapper/robot/LdMapProjectMapper.xml
  60. 49 0
      ruoyi-system/src/main/resources/mapper/robot/LdRoadmapMapper.xml
  61. 138 0
      ruoyi-system/src/main/resources/mapper/robot/LdTaskExecutionLogMapper.xml
  62. 184 0
      ruoyi-system/src/main/resources/mapper/robot/LdTaskMapper.xml
  63. 45 0
      ruoyi-system/src/main/resources/mapper/robot/LdTilemapConfigMapper.xml

+ 18 - 0
ruoyi-admin/pom.xml

@@ -86,6 +86,24 @@
             <artifactId>fastjson2</artifactId>
         </dependency>
 
+        <!-- Spring WebSocket -->
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-websocket</artifactId>
+        </dependency>
+
+        <!-- Spring Messaging (STOMP) -->
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-messaging</artifactId>
+        </dependency>
+
+        <!-- WebClient (用于调用LD导航系统HTTP API) -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-webflux</artifactId>
+        </dependency>
+
     </dependencies>
 
     <build>

+ 203 - 0
ruoyi-admin/src/main/java/com/ruoyi/mqtt/QUICKSTART.md

@@ -0,0 +1,203 @@
+# LD导航系统MQTT快速开始指南
+
+## 1. 配置MQTT连接
+
+编辑 `application.yml`:
+
+```yaml
+mqtt:
+  broker: tcp://192.168.0.30:1883
+  username: your_username
+  password: your_password
+  clientId: server_client_001
+  deviceIds: ld000001,ld000002  # 你的设备ID列表
+  subscribeTopics:               # 留空使用默认订阅
+  qos: 1
+```
+
+## 2. 启动应用
+
+启动Spring Boot应用后,系统会自动:
+- 连接到MQTT Broker
+- 订阅所有LD导航系统相关主题
+- 开始接收机器人消息
+
+## 3. 测试连接
+
+### 方式1: 使用Swagger UI
+
+访问: `http://localhost:8080/swagger-ui.html`
+
+找到"MQTT消息管理"分组,可以看到所有可用的API接口。
+
+### 方式2: 使用Postman或curl
+
+#### 请求地图列表
+```bash
+curl -X POST "http://localhost:8080/mqtt/ld/map/list?deviceId=ld000001"
+```
+
+#### 开启导航
+```bash
+curl -X POST "http://localhost:8080/mqtt/ld/navigation/start?deviceId=ld000001&mapName=map_a"
+```
+
+#### 前往目标点
+```bash
+curl -X POST "http://localhost:8080/mqtt/ld/task/gotoByNid?deviceId=ld000001&mapName=map_a&nid=19"
+```
+
+## 4. 查看日志
+
+启动后查看日志,应该能看到:
+```
+MQTT消息监听器初始化完成
+接收MQTT消息 -> 主题:/robot4inspection/ld000001/localization/pose,QoS:0
+处理LD导航系统消息 -> 设备ID:ld000001,短主题:/localization/pose
+```
+
+## 5. 常用操作流程
+
+### 完整的导航流程
+
+```bash
+# 1. 请求地图列表
+POST /mqtt/ld/map/list?deviceId=ld000001
+
+# 2. 开启导航(使用某个地图)
+POST /mqtt/ld/navigation/start?deviceId=ld000001&mapName=map_a
+
+# 3. 位姿初始化(如果需要)
+POST /mqtt/ld/localization/initByNid?deviceId=ld000001&nid=1
+
+# 4. 前往目标点
+POST /mqtt/ld/task/gotoByNid?deviceId=ld000001&mapName=map_a&nid=19
+
+# 5. 监听到达事件(自动接收)
+# 系统会自动接收 /task/target/event/arrive 消息
+
+# 6. 如需暂停
+POST /mqtt/ld/task/pause?deviceId=ld000001
+
+# 7. 继续任务
+POST /mqtt/ld/task/resume?deviceId=ld000001
+
+# 8. 取消任务
+POST /mqtt/ld/task/cancel?deviceId=ld000001
+```
+
+## 6. 在代码中使用
+
+### 注入服务
+```java
+@Autowired
+private MqttSendService mqttSendService;
+
+@Autowired
+private RedisCache redisCache;
+```
+
+### 发送指令
+```java
+// 开启导航
+boolean success = mqttSendService.startNavigation("ld000001", "map_a");
+
+// 前往目标点
+mqttSendService.gotoTargetByNid("ld000001", "map_a", 19);
+
+// 暂停任务
+mqttSendService.pauseTask("ld000001");
+```
+
+### 获取机器人状态
+```java
+// 从Redis获取实时位姿
+RobotPoseInfo poseInfo = redisCache.getCacheObject("ld:pose:ld000001");
+if (poseInfo != null) {
+    List<Double> xyz = poseInfo.getPose().getXyz();
+    Integer confidence = poseInfo.getConfidence();
+    Integer runState = poseInfo.getRunState();
+}
+
+// 从Redis获取任务实时信息
+TaskRealtimeInfo taskInfo = redisCache.getCacheObject("ld:task:realtime:ld000001");
+if (taskInfo != null) {
+    String driveMode = taskInfo.getDriveMode();
+    Double remainDistance = taskInfo.getOdom().getRemain();
+}
+```
+
+## 7. 自定义消息处理
+
+编辑 `MqttMessageListener.java`,在对应的处理方法中添加业务逻辑:
+
+```java
+private void handleArriveEvent(String deviceId, String payload) {
+    try {
+        JSONObject json = JSON.parseObject(payload);
+        JSONArray argsArray = json.getJSONArray("args");
+        if (argsArray != null && !argsArray.isEmpty()) {
+            ArriveEventInfo arriveInfo = argsArray.getObject(0, ArriveEventInfo.class);
+            
+            if ("ok".equals(arriveInfo.getStatus())) {
+                // 到达成功 - 实现你的业务逻辑
+                log.info("机器人{}成功到达目标点", deviceId);
+                // 例如: 更新数据库任务状态
+                // taskService.updateTaskStatus(deviceId, "completed");
+            } else {
+                // 到达失败 - 处理错误
+                TaskErrorCode errorCode = TaskErrorCode.fromCode(arriveInfo.getError());
+                log.error("机器人{}到达失败: {}", deviceId, errorCode.getDescription());
+                // 例如: 记录错误日志
+                // errorLogService.recordError(deviceId, errorCode);
+            }
+        }
+    } catch (Exception e) {
+        log.error("解析到达事件失败", e);
+    }
+}
+```
+
+## 8. 常见问题
+
+### Q: 连接不上MQTT Broker?
+A: 检查:
+1. broker地址是否正确
+2. 用户名密码是否正确
+3. 网络是否通畅
+4. MQTT Broker是否正常运行
+
+### Q: 收不到消息?
+A: 检查:
+1. 订阅主题是否正确
+2. 设备是否在线并发送消息
+3. 查看日志是否有错误信息
+
+### Q: 发送指令没反应?
+A: 检查:
+1. 设备ID是否正确
+2. 主题格式是否符合规范
+3. 设备是否订阅了对应的指令主题
+4. 查看MQTT Broker的消息日志
+
+### Q: 如何调试MQTT消息?
+A: 使用MQTT客户端工具:
+1. MQTTX (推荐,图形化界面)
+2. mosquitto_sub/mosquitto_pub (命令行)
+3. 查看应用日志
+
+## 9. 下一步
+
+- 阅读完整文档: `README.md`
+- 查看接口规范: `LD导航系统MQTT接口.md`
+- 查看变更日志: `CHANGELOG.md`
+- 实现自定义业务逻辑
+- 编写单元测试
+
+## 10. 技术支持
+
+如有问题,请查看:
+1. 项目文档
+2. 日志文件
+3. MQTT Broker管理界面
+4. 联系开发团队

+ 75 - 2
ruoyi-admin/src/main/java/com/ruoyi/mqtt/config/MqttConfig.java

@@ -35,9 +35,12 @@ public class MqttConfig {
     @Value("${mqtt.clientId:mqttx_1e1f7a7e}")
     private String clientId;
 
-    @Value("${mqtt.subscribeTopics:device/+/report/job/create_result,device/+/report/job/status,log/+/event,device/+/cmd/jc/execute}")
+    @Value("${mqtt.subscribeTopics:}")
     private String subscribeTopics;
 
+    @Value("${mqtt.deviceId:}")
+    private String deviceId;
+
     @Value("${mqtt.qos:1}")
     private int qos;
 
@@ -53,6 +56,17 @@ public class MqttConfig {
     @Value("${mqtt.cleanSession:false}")
     private boolean cleanSession;
 
+    @Value("${mqtt.productId:robot4inspection}")
+    private String productId;
+
+    public String getProductId() {
+        return productId;
+    }
+
+    public String getDeviceId() {
+        return deviceId;
+    }
+
     @Bean
     public MqttConnectOptions mqttConnectOptions() {
         MqttConnectOptions options = new MqttConnectOptions();
@@ -80,7 +94,20 @@ public class MqttConfig {
 
     @Bean
     public MessageProducer inbound() {
-        String[] topics = subscribeTopics.split(",");
+        String[] topics;
+        
+        // 如果配置了具体的订阅主题,使用配置的主题
+        if (subscribeTopics != null && !subscribeTopics.trim().isEmpty()) {
+            topics = subscribeTopics.split(",");
+        } else {
+            // 否则使用LD导航系统的默认订阅主题
+            // 必须配置deviceId才能订阅
+            if (deviceId == null || deviceId.trim().isEmpty()) {
+                throw new IllegalArgumentException("mqtt.deviceId 配置不能为空!请在 application.yml 中配置 mqtt.deviceId");
+            }
+            topics = buildDefaultSubscribeTopics();
+        }
+        
         MqttPahoMessageDrivenChannelAdapter adapter = new MqttPahoMessageDrivenChannelAdapter(
                 clientId + "-inbound", mqttClientFactory(), topics);
         adapter.setConverter(new DefaultPahoMessageConverter());
@@ -88,6 +115,52 @@ public class MqttConfig {
         adapter.setOutputChannel(mqttInputChannel());
         return adapter;
     }
+    
+    /**
+     * 构建默认订阅主题列表
+     * 订阅指定设备的响应和事件消息
+     * 主题格式: /${ProductID}/${DeviceID}/${ShortTopic}
+     */
+    private String[] buildDefaultSubscribeTopics() {
+        // 构建主题前缀: /${ProductID}/${DeviceID}
+        String prefix = "/" + productId + "/" + deviceId;
+        return new String[]{
+            // 地图相关
+            prefix + "/map/service/maplist/response",
+            prefix + "/map/property/current_map",
+            
+            // 定位相关
+            prefix + "/localization/pose",
+            prefix + "/localization/action/init/reply",
+            
+            // 导航相关
+            prefix + "/navigation/stack/action/start/reply",
+            prefix + "/navigation/stack/action/stop/reply",
+            prefix + "/navigation/stack/action/restart/reply",
+            
+            // 规划相关
+            prefix + "/planning/service/plan/response",
+            prefix + "/planning/trajectory",
+            prefix + "/planning/trajectory/2d/compact",
+            prefix + "/planning/action/replan/reply",
+            
+            // 任务相关
+            prefix + "/task/target/action/goto/reply",
+            prefix + "/task/target/event/arrive",
+            prefix + "/task/realtime/info",
+            prefix + "/task/procedure/action/pause/reply",
+            prefix + "/task/procedure/action/resume/reply",
+            prefix + "/task/procedure/action/cancel/reply",
+            
+            // ASM远程功能调用相关
+            prefix + "/ability/function/action/exec/reply",
+            prefix + "/ability/function/action/exec/progress",
+            prefix + "/ability/function/action/exec/state",
+
+            // 传感器相关(电池信息)
+            prefix + "/sensor/battery"
+        };
+    }
 
     @Bean
     @ServiceActivator(inputChannel = "mqttInputChannel")

+ 307 - 0
ruoyi-admin/src/main/java/com/ruoyi/mqtt/constant/MqttTopicConstants.java

@@ -0,0 +1,307 @@
+package com.ruoyi.mqtt.constant;
+
+/**
+ * MQTT主题常量类
+ * 根据LD导航系统MQTT接口规范定义
+ * 主题格式: /${ProductID}/${DeviceID}/${ShortTopic}
+ */
+public class MqttTopicConstants {
+    
+    // ==================== 地图相关主题 ====================
+    
+    /**
+     * 请求地图列表 (订阅)
+     * QoS: 2, Retain: 0
+     */
+    public static final String MAP_SERVICE_MAPLIST_REQUEST = "/map/service/maplist/request";
+    
+    /**
+     * 请求地图列表响应 (发布)
+     * QoS: 2, Retain: 0
+     */
+    public static final String MAP_SERVICE_MAPLIST_RESPONSE = "/map/service/maplist/response";
+    
+    /**
+     * 当前地图 (发布)
+     * QoS: 2, Retain: 1
+     */
+    public static final String MAP_PROPERTY_CURRENT_MAP = "/map/property/current_map";
+    
+    // ==================== 定位相关主题 ====================
+    
+    /**
+     * 机器人实时位姿 (发布)
+     * QoS: 0, Retain: 0
+     */
+    public static final String LOCALIZATION_POSE = "/localization/pose";
+    
+    /**
+     * 位姿初始化 (订阅)
+     * QoS: 2, Retain: 0
+     */
+    public static final String LOCALIZATION_ACTION_INIT = "/localization/action/init";
+    
+    /**
+     * 位姿初始化响应 (发布)
+     * QoS: 2, Retain: 0
+     */
+    public static final String LOCALIZATION_ACTION_INIT_REPLY = "/localization/action/init/reply";
+    
+    // ==================== 导航相关主题 ====================
+    
+    /**
+     * 开启导航 (订阅)
+     * QoS: 2, Retain: 0
+     */
+    public static final String NAVIGATION_STACK_ACTION_START = "/navigation/stack/action/start";
+    
+    /**
+     * 开启导航响应 (发布)
+     * QoS: 2, Retain: 0
+     */
+    public static final String NAVIGATION_STACK_ACTION_START_REPLY = "/navigation/stack/action/start/reply";
+    
+    /**
+     * 关闭导航 (订阅)
+     * QoS: 2, Retain: 0
+     */
+    public static final String NAVIGATION_STACK_ACTION_STOP = "/navigation/stack/action/stop";
+    
+    /**
+     * 关闭导航响应 (发布)
+     * QoS: 2, Retain: 0
+     */
+    public static final String NAVIGATION_STACK_ACTION_STOP_REPLY = "/navigation/stack/action/stop/reply";
+    
+    /**
+     * 重启导航 (订阅)
+     * QoS: 2, Retain: 0
+     */
+    public static final String NAVIGATION_STACK_ACTION_RESTART = "/navigation/stack/action/restart";
+    
+    /**
+     * 重启导航响应 (发布)
+     * QoS: 2, Retain: 0
+     */
+    public static final String NAVIGATION_STACK_ACTION_RESTART_REPLY = "/navigation/stack/action/restart/reply";
+    
+    // ==================== 规划相关主题 ====================
+    
+    /**
+     * 请求路径规划 (订阅)
+     * QoS: 2, Retain: 0
+     */
+    public static final String PLANNING_SERVICE_PLAN_REQUEST = "/planning/service/plan/request";
+    
+    /**
+     * 请求路径规划响应 (发布)
+     * QoS: 2, Retain: 0
+     */
+    public static final String PLANNING_SERVICE_PLAN_RESPONSE = "/planning/service/plan/response";
+    
+    /**
+     * 当前行驶轨迹 (发布)
+     * QoS: 2, Retain: 1
+     */
+    public static final String PLANNING_TRAJECTORY = "/planning/trajectory";
+    
+    /**
+     * 当前行驶轨迹-紧凑格式 (发布)
+     * QoS: 2, Retain: 1
+     */
+    public static final String PLANNING_TRAJECTORY_2D_COMPACT = "/planning/trajectory/2d/compact";
+    
+    /**
+     * 遇障重规划 (订阅)
+     * QoS: 2, Retain: 0
+     */
+    public static final String PLANNING_ACTION_REPLAN = "/planning/action/replan";
+    
+    /**
+     * 重规划响应 (发布)
+     * QoS: 2, Retain: 0
+     */
+    public static final String PLANNING_ACTION_REPLAN_REPLY = "/planning/action/replan/reply";
+    
+    // ==================== 任务相关主题 ====================
+    
+    /**
+     * 前往目标点 (订阅)
+     * QoS: 2, Retain: 0
+     */
+    public static final String TASK_TARGET_ACTION_GOTO = "/task/target/action/goto";
+    
+    /**
+     * 前往目标点响应 (发布)
+     * QoS: 2, Retain: 0
+     */
+    public static final String TASK_TARGET_ACTION_GOTO_REPLY = "/task/target/action/goto/reply";
+    
+    /**
+     * 到达目标点 (发布)
+     * QoS: 2, Retain: 1
+     */
+    public static final String TASK_TARGET_EVENT_ARRIVE = "/task/target/event/arrive";
+    
+    /**
+     * 任务实时状态信息 (发布)
+     * QoS: 0, Retain: 0
+     */
+    public static final String TASK_REALTIME_INFO = "/task/realtime/info";
+    
+    /**
+     * 暂停任务 (订阅)
+     * QoS: 1, Retain: 0
+     */
+    public static final String TASK_PROCEDURE_ACTION_PAUSE = "/task/procedure/action/pause";
+    
+    /**
+     * 暂停任务响应 (发布)
+     * QoS: 2, Retain: 0
+     */
+    public static final String TASK_PROCEDURE_ACTION_PAUSE_REPLY = "/task/procedure/action/pause/reply";
+    
+    /**
+     * 继续任务 (订阅)
+     * QoS: 2, Retain: 0
+     */
+    public static final String TASK_PROCEDURE_ACTION_RESUME = "/task/procedure/action/resume";
+    
+    /**
+     * 继续任务响应 (发布)
+     * QoS: 2, Retain: 0
+     */
+    public static final String TASK_PROCEDURE_ACTION_RESUME_REPLY = "/task/procedure/action/resume/reply";
+    
+    /**
+     * 取消任务 (订阅)
+     * QoS: 1, Retain: 0
+     */
+    public static final String TASK_PROCEDURE_ACTION_CANCEL = "/task/procedure/action/cancel";
+
+    /**
+     * 启动任务 (订阅)
+     * QoS: 2, Retain: 0
+     */
+    public static final String TASK_PROCEDURE_ACTION_START = "/task/procedure/action/start";
+    
+    /**
+     * 取消任务响应 (发布)
+     * QoS: 2, Retain: 0
+     */
+    public static final String TASK_PROCEDURE_ACTION_CANCEL_REPLY = "/task/procedure/action/cancel/reply";
+    
+    // ==================== ASM远程功能调用相关主题 ====================
+    
+    /**
+     * 发起远程功能调用 (订阅)
+     * QoS: 2, Retain: 0
+     */
+    public static final String ABILITY_FUNCTION_ACTION_EXEC = "/ability/function/action/exec";
+    
+    /**
+     * 远程调用响应 (发布)
+     * QoS: 2, Retain: 0
+     */
+    public static final String ABILITY_FUNCTION_ACTION_EXEC_REPLY = "/ability/function/action/exec/reply";
+    
+    /**
+     * 远程调用进度反馈 (发布)
+     * QoS: 0, Retain: 0
+     */
+    public static final String ABILITY_FUNCTION_ACTION_EXEC_PROGRESS = "/ability/function/action/exec/progress";
+    
+    /**
+     * 远程调用状态反馈 (发布)
+     * QoS: 2, Retain: 0
+     */
+    public static final String ABILITY_FUNCTION_ACTION_EXEC_STATE = "/ability/function/action/exec/state";
+    
+    // ==================== ASM功能名称常量 ====================
+    
+    /** 电池信息 (发布)
+     * QoS: 0, Retain: 0
+     */
+    public static final String SENSOR_BATTERY = "/sensor/battery";
+    
+    /** 开始传感器录制 */
+    public static final String ASM_SENSOR_RECORD_START = "ASM.sensor_record.start";
+    
+    /** 停止传感器录制 */
+    public static final String ASM_SENSOR_RECORD_STOP = "ASM.sensor_record.stop";
+    
+    /** 开始地图构建 */
+    public static final String ASM_MAP_BUILD_START = "ASM.map_build.start";
+    
+    /** 停止地图构建 */
+    public static final String ASM_MAP_BUILD_STOP = "ASM.map_build.stop";
+    
+    /** 开始实时建图 */
+    public static final String ASM_MAP_SLAM_START = "ASM.map_slam.start";
+    
+    /** 停止实时建图 */
+    public static final String ASM_MAP_SLAM_STOP = "ASM.map_slam.stop";
+    
+    /** 启动标准导航 */
+    public static final String ASM_NAV_STANDARD_START = "ASM.nav_standard.start";
+    
+    /** 结束标准导航 */
+    public static final String ASM_NAV_STANDARD_STOP = "ASM.nav_standard.stop";
+
+    // ==================== 车辆控制相关主题 ====================
+
+    /**
+     * 车辆急停控制 (订阅)
+     * QoS: 2, Retain: 0
+     */
+    public static final String CONTROL_VEHICLE_ACTION_STOP = "/control/vehicle/action/stop";
+
+    /**
+     * 构建完整主题前缀(productId + deviceId)
+     * @return 主题前缀: /${ProductID}/${DeviceID}
+     */
+    public static String buildPrefix(String productId, String deviceId) {
+        return "/" + productId + "/" + deviceId;
+    }
+    
+    /**
+     * 构建完整主题(使用配置的前缀)
+     * @param prefix 前缀(格式:/${productId}/${deviceId})
+     * @param shortTopic 短主题名
+     * @return 完整主题
+     */
+    public static String buildTopic(String prefix, String shortTopic) {
+        return prefix + shortTopic;
+    }
+    
+    /**
+     * 构建完整主题
+     * @param productId 产品ID
+     * @param deviceId 设备ID
+     * @param shortTopic 短主题名
+     * @return 完整主题: /${ProductID}/${DeviceID}/${ShortTopic}
+     */
+    public static String buildTopic(String productId, String deviceId, String shortTopic) {
+        return "/" + productId + "/" + deviceId + shortTopic;
+    }
+    
+  /*  *//**
+     * 构建通配符订阅主题(使用配置的前缀,订阅所有设备)
+     * @param prefix 前缀(格式:/${productId})
+     * @param shortTopic 短主题名
+     * @return 通配符主题: /${ProductID}/+/${ShortTopic}
+     *//*
+    public static String buildWildcardTopic(String prefix, String shortTopic) {
+        return prefix + "/+" + shortTopic;
+    }
+    
+    *//**
+     * 构建通配符订阅主题(订阅所有设备)
+     * @param productId 产品ID
+     * @param shortTopic 短主题名
+     * @return 通配符主题: /${ProductID}/+/${ShortTopic}
+     *//*
+    public static String buildWildcardTopic(String productId, String shortTopic) {
+        return "/" + productId + "/+" + shortTopic;
+    }*/
+}

+ 175 - 30
ruoyi-admin/src/main/java/com/ruoyi/mqtt/controller/MqttController.java

@@ -3,10 +3,10 @@ package com.ruoyi.mqtt.controller;
 import com.alibaba.fastjson2.JSON;
 import com.ruoyi.common.core.domain.AjaxResult;
 import com.ruoyi.mqtt.service.MqttSendService;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestParam;
-import org.springframework.web.bind.annotation.RestController;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import org.springframework.web.bind.annotation.*;
 
 import javax.annotation.Resource;
 import java.util.HashMap;
@@ -16,28 +16,190 @@ import java.util.Map;
  * MQTT服务的对外接口Controller
  * 提供REST API用于发送MQTT消息和作业指令
  */
+@Api(tags = "MQTT消息管理")
 @RestController
+@RequestMapping("/mqtt")
 public class MqttController {
 
     @Resource
     private MqttSendService mqttSendService;
 
-    /**
-     * 发送MQTT消息
+
+    /*
+    发送MQTT消息
      */
-    @PostMapping("/mqtt/sendMessage")
+    @ApiOperation("发送MQTT消息")
+    @PostMapping("/sendMessage")
     public AjaxResult sendMqttMessage(
-            @RequestParam("topic") String topic,
-            @RequestParam("payload") String payload
+            @ApiParam("主题") @RequestParam("topic") String topic,
+            @ApiParam("消息内容") @RequestParam("payload") String payload
     ) {
         boolean result = mqttSendService.sendMessage(topic, payload);
         return result ? AjaxResult.success("消息发送成功") : AjaxResult.error("消息发送失败");
     }
+    
+    // ==================== LD导航系统接口 ====================
+    
+    @ApiOperation("请求地图列表")
+    @PostMapping("/ld/map/list")
+    public AjaxResult requestMapList(
+            @ApiParam("设备ID") @RequestParam("deviceId") String deviceId
+    ) {
+        boolean result = mqttSendService.requestMapList();
+        return result ? AjaxResult.success("请求发送成功") : AjaxResult.error("请求发送失败");
+    }
+    
+    @ApiOperation("位姿初始化(坐标)")
+    @PostMapping("/ld/localization/init")
+    public AjaxResult initPose(
+//            @ApiParam("设备ID") @RequestParam("deviceId") String deviceId,
+            @ApiParam("X坐标") @RequestParam("x") double x,
+            @ApiParam("Y坐标") @RequestParam("y") double y,
+            @ApiParam("航向角(弧度)") @RequestParam("yaw") double yaw
+    ) {
+        boolean result = mqttSendService.initPose(x, y, yaw);
+        return result ? AjaxResult.success("位姿初始化指令发送成功") : AjaxResult.error("位姿初始化指令发送失败");
+    }
+    
+    @ApiOperation("位姿初始化(路网点ID)")
+    @PostMapping("/ld/localization/initByNid")
+    public AjaxResult initPoseByNid(
+            @ApiParam("路网点ID") @RequestParam("nid") int nid
+    ) {
+        boolean result = mqttSendService.initPoseByNid(nid);
+        return result ? AjaxResult.success("位姿初始化指令发送成功") : AjaxResult.error("位姿初始化指令发送失败");
+    }
+    
+    @ApiOperation("开启导航")
+    @PostMapping("/ld/navigation/start")
+    public AjaxResult startNavigation(
+            @ApiParam("地图名称") @RequestParam("mapName") String mapName
+    ) {
+        boolean result = mqttSendService.startNavigation(mapName);
+        return result ? AjaxResult.success("开启导航指令发送成功") : AjaxResult.error("开启导航指令发送失败");
+    }
+    
+    @ApiOperation("关闭导航")
+    @PostMapping("/ld/navigation/stop")
+    public AjaxResult stopNavigation(
+    ) {
+        boolean result = mqttSendService.stopNavigation();
+        return result ? AjaxResult.success("关闭导航指令发送成功") : AjaxResult.error("关闭导航指令发送失败");
+    }
+    
+    @ApiOperation("重启导航")
+    @PostMapping("/ld/navigation/restart")
+    public AjaxResult restartNavigation(
+            @ApiParam("地图名称(可选)") @RequestParam(value = "mapName", required = false) String mapName
+    ) {
+        boolean result = mqttSendService.restartNavigation(mapName);
+        return result ? AjaxResult.success("重启导航指令发送成功") : AjaxResult.error("重启导航指令发送失败");
+    }
+    
+    @ApiOperation("前往目标点(路网点ID)")
+    @PostMapping("/ld/task/gotoByNid")
+    public AjaxResult gotoTargetByNid(
+            @ApiParam("地图名称") @RequestParam("mapName") String mapName,
+            @ApiParam("目标点ID") @RequestParam("nid") int nid
+    ) {
+        boolean result = mqttSendService.gotoTargetByNid(mapName, nid);
+        return result ? AjaxResult.success("前往目标点指令发送成功") : AjaxResult.error("前往目标点指令发送失败");
+    }
+    
+    @ApiOperation("前往目标点(坐标)")
+    @PostMapping("/ld/task/gotoByCoord")
+    public AjaxResult gotoTargetByCoord(
+            @ApiParam("地图名称") @RequestParam("mapName") String mapName,
+            @ApiParam("X坐标") @RequestParam("x") double x,
+            @ApiParam("Y坐标") @RequestParam("y") double y
+    ) {
+        boolean result = mqttSendService.gotoTargetByCoord(mapName, x, y);
+        return result ? AjaxResult.success("前往目标点指令发送成功") : AjaxResult.error("前往目标点指令发送失败");
+    }
+    
+    @ApiOperation("暂停任务")
+    @PostMapping("/ld/task/pause")
+    public AjaxResult pauseTask() {
+        boolean result = mqttSendService.pauseTask();
+        return result ? AjaxResult.success("暂停任务指令发送成功") : AjaxResult.error("暂停任务指令发送失败");
+    }
+    
+    @ApiOperation("继续任务")
+    @PostMapping("/ld/task/resume")
+    public AjaxResult resumeTask() {
+        boolean result = mqttSendService.resumeTask();
+        return result ? AjaxResult.success("继续任务指令发送成功") : AjaxResult.error("继续任务指令发送失败");
+    }
+    
+    @ApiOperation("取消任务")
+    @PostMapping("/ld/task/cancel")
+    public AjaxResult cancelTask() {
+        boolean result = mqttSendService.cancelTask();
+        return result ? AjaxResult.success("取消任务指令发送成功") : AjaxResult.error("取消任务指令发送失败");
+    }
+    
+    @ApiOperation("遇障重规划")
+    @PostMapping("/ld/planning/replan")
+    public AjaxResult replan() {
+        boolean result = mqttSendService.replan();
+        return result ? AjaxResult.success("重规划指令发送成功") : AjaxResult.error("重规划指令发送失败");
+    }
 
-    /**
-     * 发送作业指令
-     */
-    @PostMapping("/mqtt/sendTaskCommand")
+    // ==================== 急停操作接口 ====================
+
+    @ApiOperation("紧急停止")
+    @PostMapping("/emergency/stop")
+    public AjaxResult emergencyStop(
+            @ApiParam("停止类型") @RequestParam(value = "type", defaultValue = "true") boolean type
+    ) {
+        boolean result = mqttSendService.emergencyStop(type);
+        return result ? AjaxResult.success("急停指令发送成功") : AjaxResult.error("急停指令发送失败");
+    }
+
+    @ApiOperation("释放急停")
+    @PostMapping("/emergency/release")
+    public AjaxResult releaseEmergencyStop() {
+        boolean result = mqttSendService.emergencyStopRelease();
+        return result ? AjaxResult.success("释放急停指令发送成功") : AjaxResult.error("释放急停指令发送失败");
+    }
+
+    @ApiOperation("启动标准导航")
+    @PostMapping("/nav/standard/start")
+    public AjaxResult startNavStandard(
+            @ApiParam("地图名称") @RequestParam("mapName") String mapName
+    ) {
+        boolean result = mqttSendService.startNavStandard(mapName);
+        return result ? AjaxResult.success("启动标准导航指令发送成功") : AjaxResult.error("启动标准导航指令发送失败");
+    }
+
+    @ApiOperation("请求路径规划")
+    @PostMapping("/planning/plan/request")
+    public AjaxResult requestPlanning(
+            @ApiParam("地图名称") @RequestParam("mapName") String mapName,
+            @ApiParam("目标X坐标") @RequestParam("x") double x,
+            @ApiParam("目标Y坐标") @RequestParam("y") double y
+    ) {
+        boolean result = mqttSendService.requestPlanning( mapName, x, y);
+        return result ? AjaxResult.success("路径规划请求发送成功") : AjaxResult.error("路径规划请求发送失败");
+    }
+
+    @ApiOperation("启动任务")
+    @PostMapping("/task/procedure/start")
+    public AjaxResult startTask(
+            @ApiParam("地图名称") @RequestParam("mapName") String mapName,
+            @ApiParam("任务名称") @RequestParam("taskName") String taskName
+    ) {
+        boolean result = mqttSendService.startTask(mapName, taskName);
+        return result ? AjaxResult.success("任务启动指令发送成功") : AjaxResult.error("任务启动指令发送失败");
+    }
+
+    // ==================== 旧版本接口(兼容) ====================
+    
+ /*
+ 发送作业指令
+ */
+    @ApiOperation("发送作业指令(旧版)")
+    @PostMapping("/sendTaskCommand")
     public AjaxResult sendTaskCommand(
             @RequestParam("machineCode") String machineCode,
             @RequestParam("jobId") String jobId,
@@ -54,21 +216,4 @@ public class MqttController {
 
         return result ? AjaxResult.success("作业指令下发成功") : AjaxResult.error("作业指令下发失败");
     }
-
-    /**
-     * 下发作业(创建作业指令下发)
-     * 对应Topic: device/{deviceId}/cmd/job/execute
-     */
-//    @PostMapping("/mqtt/createJob")
-//    public AjaxResult createJob(
-//            @RequestParam("machineCode") String machineCode,
-//            @RequestBody JobCreateRequestDTO request
-//    ) {
-//        System.out.println(request);
-//        String topic = String.format("device/%s/cmd/job/execute", machineCode);
-//        boolean result = mqttSendService.sendMessage(topic, JSON.toJSONString(request));
-//        return result ?
-//                AjaxResult.success("作业指令下发成功") :
-//                AjaxResult.error("作业指令下发失败");
-//    }
 }

+ 41 - 0
ruoyi-admin/src/main/java/com/ruoyi/mqtt/dto/MqttBaseMessage.java

@@ -0,0 +1,41 @@
+package com.ruoyi.mqtt.dto;
+
+/**
+ * MQTT消息基类
+ */
+public class MqttBaseMessage<T> {
+    
+    /**
+     * 时间戳
+     */
+    private Long timestamp;
+    
+    /**
+     * 消息参数
+     */
+    private T args;
+    
+    public MqttBaseMessage() {
+    }
+    
+    public MqttBaseMessage(Long timestamp, T args) {
+        this.timestamp = timestamp;
+        this.args = args;
+    }
+    
+    public Long getTimestamp() {
+        return timestamp;
+    }
+    
+    public void setTimestamp(Long timestamp) {
+        this.timestamp = timestamp;
+    }
+    
+    public T getArgs() {
+        return args;
+    }
+    
+    public void setArgs(T args) {
+        this.args = args;
+    }
+}

+ 42 - 0
ruoyi-admin/src/main/java/com/ruoyi/mqtt/dto/MqttResponseMessage.java

@@ -0,0 +1,42 @@
+package com.ruoyi.mqtt.dto;
+
+/**
+ * MQTT响应消息基类
+ */
+public class MqttResponseMessage<T> extends MqttBaseMessage<T> {
+    
+    /**
+     * 发布时间戳(原始请求的时间戳)
+     */
+    private Long pubTimestamp;
+    
+    /**
+     * 状态: ok/fail
+     */
+    private String status;
+    
+    public MqttResponseMessage() {
+    }
+    
+    public MqttResponseMessage(Long timestamp, Long pubTimestamp, String status, T args) {
+        super(timestamp, args);
+        this.pubTimestamp = pubTimestamp;
+        this.status = status;
+    }
+    
+    public Long getPubTimestamp() {
+        return pubTimestamp;
+    }
+    
+    public void setPubTimestamp(Long pubTimestamp) {
+        this.pubTimestamp = pubTimestamp;
+    }
+    
+    public String getStatus() {
+        return status;
+    }
+    
+    public void setStatus(String status) {
+        this.status = status;
+    }
+}

+ 34 - 0
ruoyi-admin/src/main/java/com/ruoyi/mqtt/dto/localization/InitPoseRequest.java

@@ -0,0 +1,34 @@
+package com.ruoyi.mqtt.dto.localization;
+
+/**
+ * 位姿初始化请求
+ */
+public class InitPoseRequest {
+    
+    /**
+     * 位姿数据(可选)
+     */
+    private PoseData pose;
+    
+    /**
+     * 路网点ID(可选)
+     * 如果pose和nid同时存在,优先使用pose
+     */
+    private Integer nid;
+    
+    public PoseData getPose() {
+        return pose;
+    }
+    
+    public void setPose(PoseData pose) {
+        this.pose = pose;
+    }
+    
+    public Integer getNid() {
+        return nid;
+    }
+    
+    public void setNid(Integer nid) {
+        this.nid = nid;
+    }
+}

+ 61 - 0
ruoyi-admin/src/main/java/com/ruoyi/mqtt/dto/localization/PoseData.java

@@ -0,0 +1,61 @@
+package com.ruoyi.mqtt.dto.localization;
+
+import java.util.List;
+
+/**
+ * 位姿数据
+ */
+public class PoseData {
+    
+    /**
+     * 局部地图坐标系的xyz坐标,单位:米
+     */
+    private List<Double> xyz;
+    
+    /**
+     * 横滚、俯仰、航向角,单位:弧度
+     */
+    private List<Double> rpy;
+    
+    /**
+     * GNSS数据:纬度、经度、高度(米)
+     */
+    private List<Double> blh;
+    
+    /**
+     * GNSS真北坐标系方向角,单位:弧度
+     */
+    private Double heading;
+    
+    public List<Double> getXyz() {
+        return xyz;
+    }
+    
+    public void setXyz(List<Double> xyz) {
+        this.xyz = xyz;
+    }
+    
+    public List<Double> getRpy() {
+        return rpy;
+    }
+    
+    public void setRpy(List<Double> rpy) {
+        this.rpy = rpy;
+    }
+    
+    public List<Double> getBlh() {
+        return blh;
+    }
+    
+    public void setBlh(List<Double> blh) {
+        this.blh = blh;
+    }
+    
+    public Double getHeading() {
+        return heading;
+    }
+    
+    public void setHeading(Double heading) {
+        this.heading = heading;
+    }
+}

+ 139 - 0
ruoyi-admin/src/main/java/com/ruoyi/mqtt/dto/localization/RobotPoseInfo.java

@@ -0,0 +1,139 @@
+package com.ruoyi.mqtt.dto.localization;
+
+/**
+ * 机器人实时位姿信息
+ */
+public class RobotPoseInfo {
+    
+    /**
+     * 位姿数据
+     */
+    private PoseData pose;
+    
+    /**
+     * 速度数据
+     */
+    private VelocityData vel;
+    
+    /**
+     * RTK数据
+     */
+    private RtkData rtk;
+    
+    /**
+     * 时间数据
+     */
+    private TimeData time;
+    
+    /**
+     * 机器人定位置信度,范围0-100,值越大定位置信度越高
+     */
+    private Integer confidence;
+    
+    /**
+     * 激光配准误差,score越小表示匹配越精确
+     */
+    private Double score;
+    
+    /**
+     * 序列号,与/planning/trajectory主题相关
+     */
+    private Integer sequence;
+    
+    /**
+     * 导航累计里程,单位:m
+     */
+    private Double odometer;
+    
+    /**
+     * 机器人行驶状态
+     * 1:正常行驶; 2:暂停; 3:停车待命; 4:遇障停车;
+     * 5:交通管制等待; 6:定位切换等待; 7:人工接管; 8:绕障
+     */
+    private Integer runState;
+    
+    /**
+     * 遇障重规划剩余时间,单位:秒
+     */
+    private Integer replan;
+    
+    public PoseData getPose() {
+        return pose;
+    }
+    
+    public void setPose(PoseData pose) {
+        this.pose = pose;
+    }
+    
+    public VelocityData getVel() {
+        return vel;
+    }
+    
+    public void setVel(VelocityData vel) {
+        this.vel = vel;
+    }
+    
+    public RtkData getRtk() {
+        return rtk;
+    }
+    
+    public void setRtk(RtkData rtk) {
+        this.rtk = rtk;
+    }
+    
+    public TimeData getTime() {
+        return time;
+    }
+    
+    public void setTime(TimeData time) {
+        this.time = time;
+    }
+    
+    public Integer getConfidence() {
+        return confidence;
+    }
+    
+    public void setConfidence(Integer confidence) {
+        this.confidence = confidence;
+    }
+    
+    public Double getScore() {
+        return score;
+    }
+    
+    public void setScore(Double score) {
+        this.score = score;
+    }
+    
+    public Integer getSequence() {
+        return sequence;
+    }
+    
+    public void setSequence(Integer sequence) {
+        this.sequence = sequence;
+    }
+    
+    public Double getOdometer() {
+        return odometer;
+    }
+    
+    public void setOdometer(Double odometer) {
+        this.odometer = odometer;
+    }
+    
+    public Integer getRunState() {
+        return runState;
+    }
+    
+    public void setRunState(Integer runState) {
+        this.runState = runState;
+    }
+    
+    public Integer getReplan() {
+        return replan;
+    }
+    
+    public void setReplan(Integer replan) {
+        this.replan = replan;
+    }
+}

+ 35 - 0
ruoyi-admin/src/main/java/com/ruoyi/mqtt/dto/localization/RtkData.java

@@ -0,0 +1,35 @@
+package com.ruoyi.mqtt.dto.localization;
+
+/**
+ * RTK数据
+ */
+public class RtkData {
+    
+    /**
+     * GNSS卫星颗数
+     */
+    private Integer star;
+    
+    /**
+     * GNSS/RTK定位状态
+     * -1:纯激光定位; 0:初始化; 1:单点定位; 2:码差分; 3:无效PPS;
+     * 4:固定解; 5:浮点解; 6:正在估算; 7:人工输入固定值; 8:模拟模式; 9:WAAS差分
+     */
+    private Integer status;
+    
+    public Integer getStar() {
+        return star;
+    }
+    
+    public void setStar(Integer star) {
+        this.star = star;
+    }
+    
+    public Integer getStatus() {
+        return status;
+    }
+    
+    public void setStatus(Integer status) {
+        this.status = status;
+    }
+}

+ 45 - 0
ruoyi-admin/src/main/java/com/ruoyi/mqtt/dto/localization/TimeData.java

@@ -0,0 +1,45 @@
+package com.ruoyi.mqtt.dto.localization;
+
+/**
+ * 时间数据
+ */
+public class TimeData {
+    
+    private TotalTime total;
+    
+    public TotalTime getTotal() {
+        return total;
+    }
+    
+    public void setTotal(TotalTime total) {
+        this.total = total;
+    }
+    
+    public static class TotalTime {
+        /**
+         * 导航工控机累计运行时间,单位:秒
+         */
+        private Integer run;
+        
+        /**
+         * 自主导航累计时间,单位:秒
+         */
+        private Integer nav;
+        
+        public Integer getRun() {
+            return run;
+        }
+        
+        public void setRun(Integer run) {
+            this.run = run;
+        }
+        
+        public Integer getNav() {
+            return nav;
+        }
+        
+        public void setNav(Integer nav) {
+            this.nav = nav;
+        }
+    }
+}

+ 35 - 0
ruoyi-admin/src/main/java/com/ruoyi/mqtt/dto/localization/VelocityData.java

@@ -0,0 +1,35 @@
+package com.ruoyi.mqtt.dto.localization;
+
+import java.util.List;
+
+/**
+ * 速度数据
+ */
+public class VelocityData {
+    
+    /**
+     * 东、北、天速度,单位:m/s
+     */
+    private List<Double> enu;
+    
+    /**
+     * 航向速度,单位:m/s
+     */
+    private Double heading;
+    
+    public List<Double> getEnu() {
+        return enu;
+    }
+    
+    public void setEnu(List<Double> enu) {
+        this.enu = enu;
+    }
+    
+    public Double getHeading() {
+        return heading;
+    }
+    
+    public void setHeading(Double heading) {
+        this.heading = heading;
+    }
+}

+ 48 - 0
ruoyi-admin/src/main/java/com/ruoyi/mqtt/dto/planning/PlanningRequest.java

@@ -0,0 +1,48 @@
+package com.ruoyi.mqtt.dto.planning;
+
+import java.util.List;
+
+/**
+ * 路径规划请求
+ */
+public class PlanningRequest {
+    
+    /**
+     * 地图名称
+     */
+    private String roadmap;
+    
+    /**
+     * 路网点ID列表
+     */
+    private List<Integer> nid;
+    
+    /**
+     * 坐标列表 [[x1,y1], [x2,y2], ...]
+     */
+    private List<List<Double>> coord;
+    
+    public String getRoadmap() {
+        return roadmap;
+    }
+    
+    public void setRoadmap(String roadmap) {
+        this.roadmap = roadmap;
+    }
+    
+    public List<Integer> getNid() {
+        return nid;
+    }
+    
+    public void setNid(List<Integer> nid) {
+        this.nid = nid;
+    }
+    
+    public List<List<Double>> getCoord() {
+        return coord;
+    }
+    
+    public void setCoord(List<List<Double>> coord) {
+        this.coord = coord;
+    }
+}

+ 61 - 0
ruoyi-admin/src/main/java/com/ruoyi/mqtt/dto/planning/TrajectoryCompactData.java

@@ -0,0 +1,61 @@
+package com.ruoyi.mqtt.dto.planning;
+
+import java.util.List;
+
+/**
+ * 轨迹紧凑格式数据
+ */
+public class TrajectoryCompactData {
+    
+    /**
+     * 轨迹点列表
+     */
+    private List<List<Integer>> trj;
+    
+    /**
+     * 索引列表
+     */
+    private List<Integer> idx;
+    
+    /**
+     * 总数
+     */
+    private Integer total;
+    
+    /**
+     * 除数(用于还原实际坐标)
+     */
+    private Integer div;
+    
+    public List<List<Integer>> getTrj() {
+        return trj;
+    }
+    
+    public void setTrj(List<List<Integer>> trj) {
+        this.trj = trj;
+    }
+    
+    public List<Integer> getIdx() {
+        return idx;
+    }
+    
+    public void setIdx(List<Integer> idx) {
+        this.idx = idx;
+    }
+    
+    public Integer getTotal() {
+        return total;
+    }
+    
+    public void setTotal(Integer total) {
+        this.total = total;
+    }
+    
+    public Integer getDiv() {
+        return div;
+    }
+    
+    public void setDiv(Integer div) {
+        this.div = div;
+    }
+}

+ 75 - 0
ruoyi-admin/src/main/java/com/ruoyi/mqtt/dto/task/ArriveEventInfo.java

@@ -0,0 +1,75 @@
+package com.ruoyi.mqtt.dto.task;
+
+import java.util.List;
+
+/**
+ * 到达目标点事件信息
+ */
+public class ArriveEventInfo {
+    
+    /**
+     * 地图名称
+     */
+    private String roadmap;
+    
+    /**
+     * 目标点ID列表
+     */
+    private List<Integer> nid;
+    
+    /**
+     * 目标点坐标列表
+     */
+    private List<List<Double>> coord;
+    
+    /**
+     * 状态: ok/fail
+     */
+    private String status;
+    
+    /**
+     * 错误码(status为fail时存在)
+     * 21:规划失败; 22:偏离车道线; 23:任务提前终止; 24:定位异常
+     */
+    private Integer error;
+    
+    public String getRoadmap() {
+        return roadmap;
+    }
+    
+    public void setRoadmap(String roadmap) {
+        this.roadmap = roadmap;
+    }
+    
+    public List<Integer> getNid() {
+        return nid;
+    }
+    
+    public void setNid(List<Integer> nid) {
+        this.nid = nid;
+    }
+    
+    public List<List<Double>> getCoord() {
+        return coord;
+    }
+    
+    public void setCoord(List<List<Double>> coord) {
+        this.coord = coord;
+    }
+    
+    public String getStatus() {
+        return status;
+    }
+    
+    public void setStatus(String status) {
+        this.status = status;
+    }
+    
+    public Integer getError() {
+        return error;
+    }
+    
+    public void setError(Integer error) {
+        this.error = error;
+    }
+}

+ 48 - 0
ruoyi-admin/src/main/java/com/ruoyi/mqtt/dto/task/TargetInfo.java

@@ -0,0 +1,48 @@
+package com.ruoyi.mqtt.dto.task;
+
+import java.util.List;
+
+/**
+ * 目标点信息
+ */
+public class TargetInfo {
+    
+    /**
+     * 地图名称
+     */
+    private String roadmap;
+    
+    /**
+     * 目标点ID列表
+     */
+    private List<Integer> nid;
+    
+    /**
+     * 目标点坐标列表 [[x1,y1], [x2,y2], ...]
+     */
+    private List<List<Double>> coord;
+    
+    public String getRoadmap() {
+        return roadmap;
+    }
+    
+    public void setRoadmap(String roadmap) {
+        this.roadmap = roadmap;
+    }
+    
+    public List<Integer> getNid() {
+        return nid;
+    }
+    
+    public void setNid(List<Integer> nid) {
+        this.nid = nid;
+    }
+    
+    public List<List<Double>> getCoord() {
+        return coord;
+    }
+    
+    public void setCoord(List<List<Double>> coord) {
+        this.coord = coord;
+    }
+}

+ 128 - 0
ruoyi-admin/src/main/java/com/ruoyi/mqtt/dto/task/TaskRealtimeInfo.java

@@ -0,0 +1,128 @@
+package com.ruoyi.mqtt.dto.task;
+
+/**
+ * 任务实时状态信息
+ */
+public class TaskRealtimeInfo {
+    
+    /**
+     * 里程信息
+     */
+    private OdomData odom;
+    
+    /**
+     * 时间信息
+     */
+    private TimeData time;
+    
+    /**
+     * 驾驶模式: auto(自动驾驶)/manual(人工驾驶)
+     */
+    private String driveMode;
+    
+    /**
+     * 是否满足自动驾驶条件
+     */
+    private Boolean autoReady;
+    
+    public OdomData getOdom() {
+        return odom;
+    }
+    
+    public void setOdom(OdomData odom) {
+        this.odom = odom;
+    }
+    
+    public TimeData getTime() {
+        return time;
+    }
+    
+    public void setTime(TimeData time) {
+        this.time = time;
+    }
+    
+    public String getDriveMode() {
+        return driveMode;
+    }
+    
+    public void setDriveMode(String driveMode) {
+        this.driveMode = driveMode;
+    }
+    
+    public Boolean getAutoReady() {
+        return autoReady;
+    }
+    
+    public void setAutoReady(Boolean autoReady) {
+        this.autoReady = autoReady;
+    }
+    
+    public static class OdomData {
+        /**
+         * 当前任务总里程,单位:m
+         */
+        private Double tottal;
+        
+        /**
+         * 当前任务剩余里程,单位:m
+         */
+        private Double remain;
+        
+        public Double getTottal() {
+            return tottal;
+        }
+        
+        public void setTottal(Double tottal) {
+            this.tottal = tottal;
+        }
+        
+        public Double getRemain() {
+            return remain;
+        }
+        
+        public void setRemain(Double remain) {
+            this.remain = remain;
+        }
+    }
+    
+    public static class TimeData {
+        /**
+         * 当前任务预估总时长,单位:秒
+         */
+        private Integer total;
+        
+        /**
+         * 当前任务预估剩余时长,单位:秒
+         */
+        private Integer remain;
+        
+        /**
+         * 当前任务已持续时长,单位:秒
+         */
+        private Integer duration;
+        
+        public Integer getTotal() {
+            return total;
+        }
+        
+        public void setTotal(Integer total) {
+            this.total = total;
+        }
+        
+        public Integer getRemain() {
+            return remain;
+        }
+        
+        public void setRemain(Integer remain) {
+            this.remain = remain;
+        }
+        
+        public Integer getDuration() {
+            return duration;
+        }
+        
+        public void setDuration(Integer duration) {
+            this.duration = duration;
+        }
+    }
+}

+ 32 - 0
ruoyi-admin/src/main/java/com/ruoyi/mqtt/enums/MqttTopicType.java

@@ -0,0 +1,32 @@
+package com.ruoyi.mqtt.enums;
+
+/**
+ * MQTT主题类型枚举
+ * 根据LD导航系统MQTT接口规范定义
+ */
+public enum MqttTopicType {
+    
+    /**
+     * service: 请求-响应模式,应答携带业务数据
+     * 如: 路径规划
+     */
+    SERVICE,
+    
+    /**
+     * action: 请求-应答模式,执行操作
+     * 如: 前往目标点
+     */
+    ACTION,
+    
+    /**
+     * event: 突发事件同步
+     * 如: 到达目标点
+     */
+    EVENT,
+    
+    /**
+     * property: 属性变量或环境变量
+     * 如: 当前使用地图
+     */
+    PROPERTY
+}

+ 41 - 0
ruoyi-admin/src/main/java/com/ruoyi/mqtt/enums/RobotRunState.java

@@ -0,0 +1,41 @@
+package com.ruoyi.mqtt.enums;
+
+/**
+ * 机器人行驶状态枚举
+ */
+public enum RobotRunState {
+    
+    NORMAL(1, "正常行驶状态"),
+    PAUSED(2, "暂停状态"),
+    STANDBY(3, "停车待命状态"),
+    OBSTACLE_STOP(4, "遇障停车状态"),
+    TRAFFIC_WAIT(5, "交通管制等待状态"),
+    LOCALIZATION_SWITCH(6, "定位切换等待状态"),
+    MANUAL_CONTROL(7, "人工接管状态"),
+    OBSTACLE_AVOIDANCE(8, "绕障状态");
+    
+    private final int code;
+    private final String description;
+    
+    RobotRunState(int code, String description) {
+        this.code = code;
+        this.description = description;
+    }
+    
+    public int getCode() {
+        return code;
+    }
+    
+    public String getDescription() {
+        return description;
+    }
+    
+    public static RobotRunState fromCode(int code) {
+        for (RobotRunState state : values()) {
+            if (state.code == code) {
+                return state;
+            }
+        }
+        return null;
+    }
+}

+ 44 - 0
ruoyi-admin/src/main/java/com/ruoyi/mqtt/enums/RtkStatus.java

@@ -0,0 +1,44 @@
+package com.ruoyi.mqtt.enums;
+
+/**
+ * GNSS/RTK定位状态枚举
+ */
+public enum RtkStatus {
+    
+    LASER_ONLY(-1, "纯激光定位"),
+    INIT(0, "初始化"),
+    SINGLE(1, "单点定位"),
+    DGPS(2, "码差分"),
+    INVALID_PPS(3, "无效PPS"),
+    RTK_FIXED(4, "固定解"),
+    RTK_FLOAT(5, "浮点解"),
+    ESTIMATING(6, "正在估算"),
+    MANUAL_INPUT(7, "人工输入固定值"),
+    SIMULATION(8, "模拟模式"),
+    WAAS(9, "WAAS差分");
+    
+    private final int code;
+    private final String description;
+    
+    RtkStatus(int code, String description) {
+        this.code = code;
+        this.description = description;
+    }
+    
+    public int getCode() {
+        return code;
+    }
+    
+    public String getDescription() {
+        return description;
+    }
+    
+    public static RtkStatus fromCode(int code) {
+        for (RtkStatus status : values()) {
+            if (status.code == code) {
+                return status;
+            }
+        }
+        return null;
+    }
+}

+ 37 - 0
ruoyi-admin/src/main/java/com/ruoyi/mqtt/enums/TaskErrorCode.java

@@ -0,0 +1,37 @@
+package com.ruoyi.mqtt.enums;
+
+/**
+ * 任务错误码枚举
+ */
+public enum TaskErrorCode {
+    
+    PLANNING_FAILED(21, "规划失败"),
+    LANE_DEVIATION(22, "偏离车道线"),
+    TASK_TERMINATED(23, "任务提前终止"),
+    LOCALIZATION_ERROR(24, "定位异常,任务无法继续执行");
+    
+    private final int code;
+    private final String description;
+    
+    TaskErrorCode(int code, String description) {
+        this.code = code;
+        this.description = description;
+    }
+    
+    public int getCode() {
+        return code;
+    }
+    
+    public String getDescription() {
+        return description;
+    }
+    
+    public static TaskErrorCode fromCode(int code) {
+        for (TaskErrorCode error : values()) {
+            if (error.code == code) {
+                return error;
+            }
+        }
+        return null;
+    }
+}

+ 500 - 29
ruoyi-admin/src/main/java/com/ruoyi/mqtt/listener/MqttMessageListener.java

@@ -1,61 +1,71 @@
 package com.ruoyi.mqtt.listener;
 
 import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONArray;
+import com.alibaba.fastjson2.JSONObject;
 import com.ruoyi.common.core.redis.RedisCache;
+import com.ruoyi.common.utils.DateUtils;
 import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.mqtt.constant.MqttTopicConstants;
+import com.ruoyi.mqtt.dto.localization.RobotPoseInfo;
+import com.ruoyi.mqtt.dto.task.ArriveEventInfo;
+import com.ruoyi.mqtt.dto.task.TaskRealtimeInfo;
+import com.ruoyi.mqtt.service.MqttSendService;
+import com.ruoyi.mqtt.util.MqttTopicUtil;
+import com.ruoyi.robot.domain.LdTask;
+import com.ruoyi.robot.domain.LdTaskExecutionLog;
+import com.ruoyi.robot.mapper.LdMapProjectMapper;
+import com.ruoyi.robot.service.ILdTaskExecutionLogService;
+import com.ruoyi.robot.service.ILdTaskService;
+import com.ruoyi.websocket.service.WebSocketPushService;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.http.HttpEntity;
-import org.springframework.http.HttpHeaders;
-import org.springframework.http.MediaType;
-import org.springframework.http.ResponseEntity;
 import org.springframework.messaging.Message;
 import org.springframework.messaging.MessageHandler;
 import org.springframework.messaging.MessagingException;
 import org.springframework.stereotype.Component;
-import org.springframework.web.client.RestTemplate;
 
 import javax.annotation.PostConstruct;
 import javax.annotation.PreDestroy;
-import java.math.BigDecimal;
-import java.time.LocalDateTime;
-import java.time.format.DateTimeFormatter;
-import java.time.format.DateTimeParseException;
-import java.util.*;
-import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.List;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.TimeUnit;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
 
 /**
  * MQTT 消息监听器
  * 处理设备上报的作业相关消息、状态及日志事件
+ * 集成WebSocket推送服务,将数据实时推送给前端
  */
 @Component
 public class MqttMessageListener implements MessageHandler {
 
     private static final Logger log = LoggerFactory.getLogger(MqttMessageListener.class);
 
-    private static final Pattern DEVICE_TOPIC_PATTERN = Pattern.compile("device/(\\w+)/(cmd|report)/job/(\\w+)");
-    private static final Pattern LOG_TOPIC_PATTERN = Pattern.compile("log/(\\w+)/event");
-    private static final Pattern DISEASE_RECOGNITION_TOPIC_PATTERN =
-            Pattern.compile("device/([a-zA-Z0-9_-]+)/cmd/jc/execute");
-
-    private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
-
-//    private final Queue<VehicleRealtimeData> buffer = new ConcurrentLinkedQueue<>();
     private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
 
     @Autowired
     private RedisCache redisCache;
 
+    @Autowired(required = false)
+    private WebSocketPushService webSocketPushService;
+
+    @Autowired
+    private LdMapProjectMapper mapProjectMapper;
+
+    @Autowired(required = false)
+    private MqttSendService mqttSendService;
+
+    @Autowired(required = false)
+    private ILdTaskService ldTaskService;
+
+    @Autowired(required = false)
+    private ILdTaskExecutionLogService ldTaskExecutionLogService;
 
     @PostConstruct
     public void init() {
-//        scheduler.scheduleAtFixedRate(this::flushBuffer, 30, 30, TimeUnit.SECONDS);
+        log.info("MQTT消息监听器初始化完成, WebSocket推送服务: {}", 
+                webSocketPushService != null ? "已启用" : "未启用(请检查websocket模块)");
     }
 
     @PreDestroy
@@ -69,20 +79,481 @@ public class MqttMessageListener implements MessageHandler {
             String payload = message.getPayload().toString();
             String topic = message.getHeaders().get("mqtt_receivedTopic").toString();
             int qos = (int) message.getHeaders().get("mqtt_receivedQos");
-            log.info("接收MQTT消息 -> 主题:{},QoS:{},内容:{}", topic, qos, payload);
+            
+            log.info("接收MQTT消息 -> 主题:{},QoS:{}", topic, qos);
+            
             if (StringUtils.isEmpty(payload)) {
                 log.warn("消息体为空,主题:{}", topic);
                 return;
             }
-            if (topic.startsWith("")) {
+            
+            // 优先处理LD导航系统消息
+            if (MqttTopicUtil.isValidTopic(topic)) {
+                handleLdNavigationMessage(topic, payload);
+                return;
+            }
+        } catch (Exception e) {
+            log.error("处理MQTT消息失败", e);
+        }
+    }
+    
+    /**
+     * 处理LD导航系统消息
+     */
+    private void handleLdNavigationMessage(String topic, String payload) {
+        String deviceId = MqttTopicUtil.extractDeviceId(topic);
+        String shortTopic = MqttTopicUtil.extractShortTopic(topic);
+        
+        if (deviceId == null || shortTopic == null) {
+            log.warn("无法解析主题:{}", topic);
+            return;
+        }
+        
+        log.info("处理LD导航系统消息 -> 设备ID:{},短主题:{}", deviceId, shortTopic);
+        
+        try {
+            switch (shortTopic) {
+                // 地图相关
+                case MqttTopicConstants.MAP_SERVICE_MAPLIST_RESPONSE:
+                    handleMapListResponse(deviceId, payload);
+                    break;
+                case MqttTopicConstants.MAP_PROPERTY_CURRENT_MAP:
+                    handleCurrentMap(deviceId, payload);
+                    break;
+                    
+                // 定位相关
+                case MqttTopicConstants.LOCALIZATION_POSE:
+                    handleLocalizationPose(deviceId, payload);
+                    break;
+                case MqttTopicConstants.LOCALIZATION_ACTION_INIT_REPLY:
+                    handleInitPoseReply(deviceId, payload);
+                    break;
+                    
+                // 导航相关
+                case MqttTopicConstants.NAVIGATION_STACK_ACTION_START_REPLY:
+                case MqttTopicConstants.NAVIGATION_STACK_ACTION_STOP_REPLY:
+                case MqttTopicConstants.NAVIGATION_STACK_ACTION_RESTART_REPLY:
+                    handleNavigationReply(deviceId, shortTopic, payload);
+                    break;
+                    
+                // 规划相关
+                case MqttTopicConstants.PLANNING_SERVICE_PLAN_RESPONSE:
+                    handlePlanningResponse(deviceId, payload);
+                    break;
+                case MqttTopicConstants.PLANNING_TRAJECTORY:
+                case MqttTopicConstants.PLANNING_TRAJECTORY_2D_COMPACT:
+                    handleTrajectory(deviceId, shortTopic, payload);
+                    break;
+                case MqttTopicConstants.PLANNING_ACTION_REPLAN_REPLY:
+                    handleReplanReply(deviceId, payload);
+                    break;
+                    
+                // 任务相关
+                case MqttTopicConstants.TASK_TARGET_ACTION_GOTO_REPLY:
+                    handleGotoReply(deviceId, payload);
+                    break;
+                case MqttTopicConstants.TASK_TARGET_EVENT_ARRIVE:
+                    handleArriveEvent(deviceId, payload);
+                    break;
+                case MqttTopicConstants.TASK_REALTIME_INFO:
+                    handleTaskRealtimeInfo(deviceId, payload);
+                    break;
+                case MqttTopicConstants.TASK_PROCEDURE_ACTION_PAUSE_REPLY:
+                case MqttTopicConstants.TASK_PROCEDURE_ACTION_RESUME_REPLY:
+                case MqttTopicConstants.TASK_PROCEDURE_ACTION_CANCEL_REPLY:
+                    handleTaskProcedureReply(deviceId, shortTopic, payload);
+                    break;
+                    
+                // ASM远程功能调用相关
+                case MqttTopicConstants.ABILITY_FUNCTION_ACTION_EXEC_REPLY:
+                    handleAsmExecReply(deviceId, payload);
+                    break;
+                case MqttTopicConstants.ABILITY_FUNCTION_ACTION_EXEC_PROGRESS:
+                    handleAsmExecProgress(deviceId, payload);
+                    break;
+                case MqttTopicConstants.ABILITY_FUNCTION_ACTION_EXEC_STATE:
+                    handleAsmExecState(deviceId, payload);
+                    break;
+                    
+                // 传感器相关
+                case MqttTopicConstants.SENSOR_BATTERY:
+                    handleBatteryInfo(deviceId, payload);
+                    break;
+                    
+                default:
+                    log.warn("未处理的LD导航系统主题:{}", shortTopic);
+                    // 推送原始消息到WebSocket,便于前端处理未预定义的消息
+                    if (webSocketPushService != null) {
+                        webSocketPushService.pushRawMessage(deviceId, topic, payload);
+                    }
+            }
+        } catch (Exception e) {
+            log.error("处理LD导航系统消息异常,设备ID:{},主题:{}", deviceId, shortTopic, e);
+        }
+    }
+    
+    // ==================== LD导航系统消息处理方法 ====================
+    
+    private void handleMapListResponse(String deviceId, String payload) {
+        log.info("收到地图列表响应 -> 设备ID:{}", deviceId);
+        // 推送WebSocket
+        if (webSocketPushService != null) {
+            webSocketPushService.pushMapListResponse(deviceId, payload);
+        }
+        // 缓存到Redis
+        String cacheKey = "ld:maplist:" + deviceId;
+        redisCache.setCacheObject(cacheKey, payload);
+    }
+    
+    private void handleCurrentMap(String deviceId, String payload) {
+        log.info("收到当前地图 -> 设备ID:{}", deviceId);
+        // 推送WebSocket
+        if (webSocketPushService != null) {
+            webSocketPushService.pushCurrentMap(deviceId, payload);
+        }
+        // 缓存到Redis
+        String cacheKey = "ld:currentmap:" + deviceId;
+        redisCache.setCacheObject(cacheKey, payload);
+    }
+    
+    private void handleLocalizationPose(String deviceId, String payload) {
+        try {
+            JSONObject json = JSON.parseObject(payload);
+            JSONArray argsArray = json.getJSONArray("args");
+            if (argsArray != null && !argsArray.isEmpty()) {
+                RobotPoseInfo poseInfo = argsArray.getObject(0, RobotPoseInfo.class);
+                log.debug("机器人位姿 -> 设备ID:{},坐标:{},置信度:{},状态:{}", 
+                    deviceId, 
+                    poseInfo.getPose() != null ? poseInfo.getPose().getXyz() : null,
+                    poseInfo.getConfidence(),
+                    poseInfo.getRunState());
+                
+                // 缓存到Redis
+                String cacheKey = "ld:pose:" + deviceId;
+                redisCache.setCacheObject(cacheKey, poseInfo);
+                
+                // 推送WebSocket(关键:实时推送位姿数据给前端)
+                if (webSocketPushService != null) {
+                    webSocketPushService.pushRobotPose(deviceId, payload);
+                }
+            }
+        } catch (Exception e) {
+            log.error("解析机器人位姿失败", e);
+        }
+    }
+    
+    private void handleInitPoseReply(String deviceId, String payload) {
+        log.info("收到位姿初始化响应 -> 设备ID:{}", deviceId);
+        if (webSocketPushService != null) {
+            webSocketPushService.pushNavigationReply(deviceId, "init_reply", payload);
+        }
+    }
+    
+    private void handleNavigationReply(String deviceId, String shortTopic, String payload) {
+        log.info("收到导航操作响应 -> 设备ID:{},操作:{}", deviceId, shortTopic);
+        if (webSocketPushService != null) {
+            webSocketPushService.pushNavigationReply(deviceId, shortTopic, payload);
+        }
+    }
+    
+    private void handlePlanningResponse(String deviceId, String payload) {
+        log.info("收到路径规划响应 -> 设备ID:{}", deviceId);
+        if (webSocketPushService != null) {
+            webSocketPushService.pushPlanningResponse(deviceId, "plan_response", payload);
+        }
+    }
+    
+    private void handleTrajectory(String deviceId, String shortTopic, String payload) {
+        log.debug("收到轨迹数据 -> 设备ID:{},类型:{}", deviceId, shortTopic);
+        if (webSocketPushService != null) {
+            webSocketPushService.pushTrajectory(deviceId, shortTopic, payload);
+        }
+    }
+    
+    private void handleReplanReply(String deviceId, String payload) {
+        log.info("收到重规划响应 -> 设备ID:{}", deviceId);
+        if (webSocketPushService != null) {
+            webSocketPushService.pushPlanningResponse(deviceId, "replan_reply", payload);
+        }
+    }
+    
+    private void handleGotoReply(String deviceId, String payload) {
+        log.info("收到前往目标点响应 -> 设备ID:{}", deviceId);
+        if (webSocketPushService != null) {
+            webSocketPushService.pushPlanningResponse(deviceId, "goto_reply", payload);
+        }
+    }
+    
+    private void handleArriveEvent(String deviceId, String payload) {
+        try {
+            JSONObject json = JSON.parseObject(payload);
+            JSONArray argsArray = json.getJSONArray("args");
+            if (argsArray != null && !argsArray.isEmpty()) {
+                ArriveEventInfo arriveInfo = argsArray.getObject(0, ArriveEventInfo.class);
+                log.info("机器人到达目标点 -> 设备ID:{},状态:{},错误码:{}", 
+                    deviceId, arriveInfo.getStatus(), arriveInfo.getError());
+                
+                // 推送WebSocket
+                if (webSocketPushService != null) {
+                    webSocketPushService.pushArriveEvent(deviceId, payload);
+                }
+            }
+        } catch (Exception e) {
+            log.error("解析到达事件失败", e);
+        }
+    }
+    
+    private void handleTaskRealtimeInfo(String deviceId, String payload) {
+        try {
+            JSONObject json = JSON.parseObject(payload);
+            JSONArray argsArray = json.getJSONArray("args");
+            if (argsArray != null && !argsArray.isEmpty()) {
+                TaskRealtimeInfo taskInfo = argsArray.getObject(0, TaskRealtimeInfo.class);
+                log.debug("任务实时信息 -> 设备ID:{},驾驶模式:{},剩余里程:{}m", 
+                    deviceId, 
+                    taskInfo.getDriveMode(),
+                    taskInfo.getOdom() != null ? taskInfo.getOdom().getRemain() : null);
+                
+                // 缓存到Redis
+                String cacheKey = "ld:task:realtime:" + deviceId;
+                redisCache.setCacheObject(cacheKey, taskInfo);
+                
+                // 推送WebSocket(关键:实时推送任务信息给前端)
+                if (webSocketPushService != null) {
+                    webSocketPushService.pushTaskRealtimeInfo(deviceId, payload);
+                }
+            }
+        } catch (Exception e) {
+            log.error("解析任务实时信息失败", e);
+        }
+    }
+    
+    /**
+     * 处理任务控制响应(暂停/继续/取消)
+     * 根据响应结果更新数据库中对应任务的状态
+     */
+    private void handleTaskProcedureReply(String deviceId, String shortTopic, String payload) {
+        log.info("收到任务控制响应 -> 设备ID:{},操作:{}", deviceId, shortTopic);
+        try {
+            JSONObject json = JSON.parseObject(payload);
+            String status = json.getString("status");
+
+            // 判断响应是否成功
+            boolean success = "ok".equalsIgnoreCase(status);
+
+            // 根据操作类型确定目标状态
+            String targetStatus = determineTargetStatus(shortTopic, success);
+
+            // 更新数据库中该设备对应的任务状态
+            if (ldTaskService != null && StringUtils.isNotEmpty(targetStatus)) {
+                int updatedCount = ldTaskService.updateTaskStatusByDeviceId(deviceId, targetStatus);
+                if (updatedCount > 0) {
+                    log.info("任务状态已更新 -> 设备ID:{},操作:{},新状态:{},更新记录数:{}",
+                            deviceId, shortTopic, targetStatus, updatedCount);
 
-            } else if (topic.startsWith("")) {
+                    // 如果是取消操作,记录执行日志
+                    if (success && MqttTopicConstants.TASK_PROCEDURE_ACTION_CANCEL_REPLY.equals(shortTopic)) {
+                        recordTaskExecutionLog(deviceId, "cancelled", shortTopic);
+                    }
+                } else {
+                    log.warn("未找到该设备对应的运行中任务 -> 设备ID:{}", deviceId);
+                }
+            }
 
-            } else {
-                log.warn("未匹配的消息主题:{}", topic);
+            // 推送WebSocket通知前端
+            if (webSocketPushService != null) {
+                webSocketPushService.pushTaskProcedureReply(deviceId, shortTopic, payload);
             }
         } catch (Exception e) {
-            log.error("处理MQTT消息失败", e);
+            log.error("处理任务控制响应失败 -> 设备ID:{},操作:{}", deviceId, shortTopic, e);
+        }
+    }
+
+    /**
+     * 根据操作类型确定目标状态
+     *
+     * @param shortTopic 操作主题
+     * @param success 操作是否成功
+     * @return 目标状态,null表示不需要更新
+     */
+    private String determineTargetStatus(String shortTopic, boolean success) {
+        if (!success) {
+            // 操作失败时保持原状态
+            return null;
+        }
+
+        if (MqttTopicConstants.TASK_PROCEDURE_ACTION_PAUSE_REPLY.equals(shortTopic)) {
+            return "paused";
+        } else if (MqttTopicConstants.TASK_PROCEDURE_ACTION_RESUME_REPLY.equals(shortTopic)) {
+            return "running";
+        } else if (MqttTopicConstants.TASK_PROCEDURE_ACTION_CANCEL_REPLY.equals(shortTopic)) {
+            return "idle";
+        }
+        return null;
+    }
+
+    /**
+     * 记录任务执行日志
+     */
+    private void recordTaskExecutionLog(String deviceId, String status, String operation) {
+        try {
+            if (ldTaskExecutionLogService == null) {
+                return;
+            }
+
+            // 获取该设备正在运行的任务
+            if (ldTaskService != null) {
+                List<LdTask> runningTasks = ldTaskService.selectRunningTasksByDeviceId(deviceId);
+                if (runningTasks != null && !runningTasks.isEmpty()) {
+                    for (LdTask task : runningTasks) {
+                        LdTaskExecutionLog log = new LdTaskExecutionLog();
+                        log.setTaskId(task.getId());
+                        log.setTaskName(task.getTaskName());
+                        log.setMapName(task.getMapName());
+                        log.setExecuteTime(task.getLastExecuteTime());
+                        log.setEndTime(DateUtils.getNowDate());
+                        log.setStatus(status);
+                        log.setDeviceId(deviceId);
+                        log.setRemark("任务被取消,操作类型:" + operation);
+
+                        ldTaskExecutionLogService.insertLdTaskExecutionLog(log);
+                    }
+                }
+            }
+        } catch (Exception e) {
+            log.error("记录任务执行日志失败 -> 设备ID:{}", deviceId, e);
+        }
+    }
+    
+    // ==================== ASM远程功能调用消息处理方法 ====================
+    
+    /**
+     * 处理ASM远程调用响应
+     */
+    private void handleAsmExecReply(String deviceId, String payload) {
+        log.info("收到ASM远程调用响应 -> 设备ID:{}", deviceId);
+        try {
+            JSONObject json = JSON.parseObject(payload);
+            String function = json.getString("function");
+            String status = json.getString("status");
+            Long pubTimestamp = json.getLong("pub_timestamp");
+            log.info("ASM响应 -> 功能:{},状态:{},请求时间:{}", function, status, pubTimestamp);
+        } catch (Exception e) {
+            log.error("解析ASM响应失败", e);
+        }
+        if (webSocketPushService != null) {
+            webSocketPushService.pushAsmReply(deviceId, payload);
+        }
+    }
+    
+    /**
+     * 处理ASM远程调用进度反馈
+     */
+    private void handleAsmExecProgress(String deviceId, String payload) {
+        log.debug("收到ASM远程调用进度反馈 -> 设备ID:{}", deviceId);
+        try {
+            JSONObject json = JSON.parseObject(payload);
+            JSONArray argsArray = json.getJSONArray("args");
+            if (argsArray != null && !argsArray.isEmpty()) {
+                JSONObject argObj = argsArray.getJSONObject(0);
+                String function = argObj.getString("function");
+                Object progress = argObj.get("progress");
+                log.debug("ASM进度 -> 功能:{},进度:{}", function, progress);
+                
+                // 缓存ASM进度到Redis
+                String cacheKey = "ld:asm:progress:" + deviceId;
+                redisCache.setCacheObject(cacheKey, argObj);
+            }
+        } catch (Exception e) {
+            log.error("解析ASM进度反馈失败", e);
+        }
+        if (webSocketPushService != null) {
+            webSocketPushService.pushAsmProgress(deviceId, payload);
+        }
+    }
+    
+    /**
+     * 处理ASM远程调用状态反馈
+     * state值:0-准备,1-进行中,2-执行完毕,3-执行失败,4-取消
+     */
+    private void handleAsmExecState(String deviceId, String payload) {
+        log.info("收到ASM远程调用状态反馈 -> 设备ID:{}", deviceId);
+        try {
+            JSONObject json = JSON.parseObject(payload);
+            JSONArray argsArray = json.getJSONArray("args");
+            if (argsArray != null && !argsArray.isEmpty()) {
+                JSONObject argObj = argsArray.getJSONObject(0);
+                String function = argObj.getString("function");
+                Integer state = argObj.getInteger("state");
+
+                String stateDesc;
+                switch (state != null ? state : -1) {
+                    case 0: stateDesc = "准备"; break;
+                    case 1: stateDesc = "进行中"; break;
+                    case 2: stateDesc = "执行完毕";break;
+                    case 3: stateDesc = "执行失败"; break;
+                    case 4: stateDesc = "取消"; break;
+                    default: stateDesc = "未知状态(" + state + ")";
+                }
+                log.info("ASM状态 -> 功能:{},状态:{},描述:{}", function, state, stateDesc);
+                
+                // 缓存ASM状态到Redis
+                String cacheKey = "ld:asm:state:" + deviceId;
+                redisCache.setCacheObject(cacheKey, argObj);
+                
+                // 更新地图列表(ASM状态变化时需要刷新地图列表)
+                if (function != null && (function.startsWith("ASM.sensor_record") ||
+                    function.startsWith("ASM.map_build") || function.startsWith("ASM.map_slam"))) {
+                    log.info("ASM地图相关操作状态变化,触发地图列表刷新 -> 功能:{},状态:{}", function, stateDesc);
+
+                }
+            }
+        } catch (Exception e) {
+            log.error("解析ASM状态反馈失败", e);
+        }
+        if (webSocketPushService != null) {
+            webSocketPushService.pushAsmState(deviceId, payload);
+        }
+    }
+    
+    /**
+     * 处理电池信息
+     * capacity: 电量百分比,范围0.0 ~ 1.0,对应电量0% ~ 100%
+     * voltage: 电池电压,单位V
+     * current: 电池电流,单位A,一般充电时候为正值,非充电状态下为负值
+     * charging: 充电状态,true表示正在充电,false表示未充电
+     * other: 其他电量百分比,范围0~100,用于上传车上其他设备电量
+     */
+    private void handleBatteryInfo(String deviceId, String payload) {
+        try {
+            JSONObject json = JSON.parseObject(payload);
+            JSONArray argsArray = json.getJSONArray("args");
+            if (argsArray != null && !argsArray.isEmpty()) {
+                JSONObject batteryData = argsArray.getJSONObject(0);
+                
+                Double capacity = batteryData.getDouble("capacity");
+                Double temperature = batteryData.getDouble("temperature");
+                Double voltage = batteryData.getDouble("voltage");
+                Double current = batteryData.getDouble("current");
+                Boolean charging = batteryData.getBoolean("charging");
+                JSONArray otherArray = batteryData.getJSONArray("other");
+                
+                log.debug("电池信息 -> 设备ID:{},电量:{},电压:{}V,电流:{}A,温度:{}°C,充电:{},其他设备电量:{}", 
+                    deviceId, capacity != null ? (capacity * 100) + "%" : "N/A",
+                    voltage, current, temperature, charging, 
+                    otherArray != null ? otherArray.toString() : "N/A");
+                
+                // 缓存到Redis
+                String cacheKey = "ld:battery:" + deviceId;
+                redisCache.setCacheObject(cacheKey, batteryData);
+                
+                // 推送WebSocket
+                if (webSocketPushService != null) {
+                    webSocketPushService.pushBatteryInfo(deviceId, payload);
+                }
+            }
+        } catch (Exception e) {
+            log.error("解析电池信息失败", e);
         }
     }
 }

+ 312 - 10
ruoyi-admin/src/main/java/com/ruoyi/mqtt/service/MqttSendService.java

@@ -1,6 +1,10 @@
 package com.ruoyi.mqtt.service;
 
+import com.alibaba.fastjson2.JSON;
 import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.mqtt.constant.MqttTopicConstants;
+import com.ruoyi.mqtt.dto.MqttBaseMessage;
+import com.ruoyi.mqtt.util.MqttTopicUtil;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -8,6 +12,9 @@ import org.springframework.integration.support.MessageBuilder;
 import org.springframework.messaging.MessageChannel;
 import org.springframework.stereotype.Service;
 
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
 /**
  * MQTT 消息发送服务(向 EMQX 下发指令到设备)
  */
@@ -59,19 +66,314 @@ public class MqttSendService {
     public boolean sendMessage(String topic, String payload) {
         return sendMessage(topic, payload, 1, false);
     }
+    
+    // ==================== LD导航系统专用方法 ====================
+    
+    /**
+     * 发送LD导航系统消息
+     * @param shortTopic 短主题名
+     * @param args 消息参数
+     * @param qos QoS级别
+     * @param retained 是否保留
+     * @return 发送结果
+     */
+    public boolean sendLdMessage(String shortTopic, Object args, int qos, boolean retained) {
+        String fullTopic = MqttTopicUtil.buildTopic(shortTopic);
+        MqttBaseMessage<Object> message = new MqttBaseMessage<>(System.currentTimeMillis(), args);
+        String payload = JSON.toJSONString(message);
+        return sendMessage(fullTopic, payload, qos, retained);
+    }
+    
+    /**
+     * 请求地图列表
+     * @return 发送结果
+     */
+    public boolean requestMapList() {
+        return sendLdMessage(MqttTopicConstants.MAP_SERVICE_MAPLIST_REQUEST, null, 2, false);
+    }
+    
+    /**
+     * 位姿初始化(通过坐标)
+     * @param x X坐标
+     * @param y Y坐标
+     * @param yaw 航向角(弧度)
+     * @return 发送结果
+     */
+    public boolean initPose(double x, double y, double yaw) {
+        Object args = Collections.singletonList(
+            new Object() {
+                public final Object pose = new Object() {
+                    public final List<Double> xyz = Arrays.asList(x, y, 0.0);
+                    public final List<Double> rpy = Arrays.asList(0.0, 0.0, yaw);
+                    public final Object blh = null;
+                };
+            }
+        );
+        return sendLdMessage(MqttTopicConstants.LOCALIZATION_ACTION_INIT, args, 2, false);
+    }
+    
+    /**
+     * 位姿初始化(通过路网点ID)
+     * @param nidValue 路网点ID
+     * @return 发送结果
+     */
+    public boolean initPoseByNid(int nidValue) {
+        Object args = Collections.singletonList(
+                new Object() {
+                    public final Integer nid = Integer.valueOf(nidValue);
+                }
+        );
+        return sendLdMessage(MqttTopicConstants.LOCALIZATION_ACTION_INIT, args, 2, false);
+    }
+    
+    /**
+     * 开启导航
+     * @param mapName 地图名称
+     * @return 发送结果
+     */
+    public boolean startNavigation(String mapName) {
+        List<String> args = Collections.singletonList(mapName);
+        return sendLdMessage(MqttTopicConstants.NAVIGATION_STACK_ACTION_START, args, 2, false);
+    }
+    
+    /**
+     * 关闭导航
+     * @return 发送结果
+     */
+    public boolean stopNavigation() {
+        return sendLdMessage(MqttTopicConstants.NAVIGATION_STACK_ACTION_STOP, null, 2, false);
+    }
+    
+    /**
+     * 重启导航
+     * @param mapName 地图名称(可选,为null则使用当前地图)
+     * @return 发送结果
+     */
+    public boolean restartNavigation(String mapName) {
+        Object args = mapName != null ? Collections.singletonList(mapName) : null;
+        return sendLdMessage(MqttTopicConstants.NAVIGATION_STACK_ACTION_RESTART, args, 2, false);
+    }
+    
+    /**
+     * 前往目标点(通过路网点ID)
+     * @param mapName 地图名称
+     * @param nidValue 目标点ID
+     * @return 发送结果
+     */
+    public boolean gotoTargetByNid(String mapName, int nidValue) {
+        Object args = Collections.singletonList(
+            new Object() {
+                public final String roadmap = mapName;
+                public final List<Integer> nid = Collections.singletonList(Integer.valueOf(nidValue));
+            }
+        );
+        return sendLdMessage(MqttTopicConstants.TASK_TARGET_ACTION_GOTO, args, 2, false);
+    }
+    
+    /**
+     * 前往目标点(通过坐标)
+     * @param mapName 地图名称
+     * @param x X坐标
+     * @param y Y坐标
+     * @return 发送结果
+     */
+    public boolean gotoTargetByCoord(String mapName, double x, double y) {
+        Object args = Collections.singletonList(
+            new Object() {
+                public final String roadmap = mapName;
+                public final List<List<Double>> coord = Collections.singletonList(Arrays.asList(x, y));
+            }
+        );
+        return sendLdMessage(MqttTopicConstants.TASK_TARGET_ACTION_GOTO, args, 2, false);
+    }
+    
+    /**
+     * 暂停任务
+     * @return 发送结果
+     */
+    public boolean pauseTask() {
+        return sendLdMessage(MqttTopicConstants.TASK_PROCEDURE_ACTION_PAUSE, null, 1, false);
+    }
+    
+    /**
+     * 继续任务
+     * @return 发送结果
+     */
+    public boolean resumeTask() {
+        return sendLdMessage(MqttTopicConstants.TASK_PROCEDURE_ACTION_RESUME, null, 2, false);
+    }
+    
+    /**
+     * 取消任务
+     * @return 发送结果
+     */
+    public boolean cancelTask() {
+        return sendLdMessage(MqttTopicConstants.TASK_PROCEDURE_ACTION_CANCEL, null, 1, false);
+    }
+    
+    /**
+     * 遇障重规划
+     * @return 发送结果
+     */
+    public boolean replan() {
+        List<Integer> args = Collections.singletonList(0);
+        return sendLdMessage(MqttTopicConstants.PLANNING_ACTION_REPLAN, args, 2, false);
+    }
 
     /**
-     * 下发作业指令到农机
-     *
-     * @param machineCode 农机编号
-     * @param taskId 作业ID
-     * @param command 指令类型:start=启动作业,pause=暂停,stop=停止
+     * 请求路径规划
+     * @param mapName 地图名称
+     * @param x 目标X坐标
+     * @param y 目标Y坐标
      * @return 发送结果
      */
-    public boolean sendTaskCommand(String machineCode, Long taskId, String command) {
-        String payload = String.format("{\"machineCode\":\"%s\",\"taskId\":%d,\"command\":\"%s\",\"timestamp\":%d}",
-                machineCode, taskId, command, System.currentTimeMillis());
-        String topic = "device/control/" + machineCode;
-        return sendMessage(topic, payload);
+    public boolean requestPlanning(String mapName, double x, double y) {
+        Object args = Collections.singletonList(
+            new Object() {
+                public final String roadmap = mapName;
+                public final List<Integer> nid = Collections.emptyList();
+                public final List<List<Double>> coord = Collections.singletonList(Arrays.asList(x, y));
+            }
+        );
+        return sendLdMessage(MqttTopicConstants.PLANNING_SERVICE_PLAN_REQUEST, args, 2, false);
+    }
+
+    /**
+     * 启动任务
+     * @param mapName 地图名称
+     * @param taskName 任务名称
+     * @return 发送结果
+     */
+    public boolean startTask(String mapName, String taskName) {
+        Object args = Collections.singletonList(
+            new Object() {
+                public final String roadmap = mapName;
+                public final String task = taskName;
+            }
+        );
+        return sendLdMessage(MqttTopicConstants.TASK_PROCEDURE_ACTION_START, args, 2, false);
+    }
+    
+    // ==================== ASM远程功能调用方法 ====================
+    
+    /**
+     * 发送ASM远程功能调用
+     * @param functionName 功能名称
+     * @param argCount 参数个数
+     * @param argValues 参数列表
+     * @param qos QoS级别
+     * @return 发送结果
+     */
+    public boolean sendAsmFunction(String functionName, int argCount, List<String> argValues, int qos) {
+        Object args = Collections.singletonList(
+            new Object() {
+                public final String function = functionName;
+                public final int argc = argCount;
+                public final List<String> argv = argValues;
+            }
+        );
+        return sendLdMessage(MqttTopicConstants.ABILITY_FUNCTION_ACTION_EXEC, args, qos, false);
+    }
+    
+    /**
+     * 开始录制(sensor_record)
+     * @param mapName 地图名称
+     * @return 发送结果
+     */
+    public boolean startRecord(String mapName) {
+        List<String> argv = Collections.singletonList(mapName);
+        return sendAsmFunction(MqttTopicConstants.ASM_SENSOR_RECORD_START, 1, argv, 2);
+    }
+    
+    /**
+     * 停止录制(sensor_record)
+     * @return 发送结果
+     */
+    public boolean stopRecord() {
+        return sendAsmFunction(MqttTopicConstants.ASM_SENSOR_RECORD_STOP, 0, Collections.emptyList(), 2);
+    }
+    
+    /**
+     * 开始构建地图(map_build)
+     * @param mapName 地图名称
+     * @param buildSteps 构建步骤(null表示全部,数组如["recon", "kfmix", "octomap", "tilemap", "potree"])
+     * @return 发送结果
+     */
+    public boolean startBuild(String mapName, List<String> buildSteps) {
+        List<String> argv;
+        if (buildSteps != null && !buildSteps.isEmpty()) {
+            argv = new java.util.ArrayList<>();
+            argv.add(mapName);
+            argv.addAll(buildSteps);
+        } else {
+            argv = Collections.singletonList(mapName);
+        }
+        return sendAsmFunction(MqttTopicConstants.ASM_MAP_BUILD_START, argv.size(), argv, 2);
+    }
+    
+    /**
+     * 停止构建地图(map_build)
+     * @return 发送结果
+     */
+    public boolean stopBuild() {
+        return sendAsmFunction(MqttTopicConstants.ASM_MAP_BUILD_STOP, 0, Collections.emptyList(), 2);
+    }
+    
+    /**
+     * 开始实时建图(map_slam)
+     * @param mapName 地图名称
+     * @return 发送结果
+     */
+    public boolean startSlam(String mapName) {
+        List<String> argv = Collections.singletonList(mapName);
+        return sendAsmFunction(MqttTopicConstants.ASM_MAP_SLAM_START, 1, argv, 2);
+    }
+    
+    /**
+     * 停止实时建图(map_slam)
+     * @return 发送结果
+     */
+    public boolean stopSlam() {
+        return sendAsmFunction(MqttTopicConstants.ASM_MAP_SLAM_STOP, 0, Collections.emptyList(), 2);
+    }
+    
+    /**
+     * 启动标准导航(nav_standard)
+     * @param mapName 地图名称
+     * @return 发送结果
+     */
+    public boolean startNavStandard(String mapName) {
+        List<String> argv = Collections.singletonList(mapName);
+        return sendAsmFunction(MqttTopicConstants.ASM_NAV_STANDARD_START, 1, argv, 2);
+    }
+    
+    /**
+     * 结束标准导航(nav_standard)
+     * @return 发送结果
+     */
+    public boolean stopNavStandard() {
+        return sendAsmFunction(MqttTopicConstants.ASM_NAV_STANDARD_STOP, 0, Collections.emptyList(), 2);
+    }
+
+    // ==================== 急停控制方法 ====================
+
+    /**
+     * 车辆急停控制
+     * @param stop true-急停, false-释放
+     * @return 发送结果
+     */
+    public boolean emergencyStop(boolean stop) {
+        Object args = Collections.singletonList(stop);
+        return sendLdMessage(MqttTopicConstants.CONTROL_VEHICLE_ACTION_STOP, args, 2, false);
+    }
+
+    /**
+     * 释放急停
+     * @return 发送结果
+     */
+    public boolean emergencyStopRelease() {
+        Object args = Collections.singletonList(false);
+        return sendLdMessage(MqttTopicConstants.CONTROL_VEHICLE_ACTION_STOP, args, 2, false);
     }
 }
+

+ 138 - 0
ruoyi-admin/src/main/java/com/ruoyi/mqtt/util/MqttTopicUtil.java

@@ -0,0 +1,138 @@
+package com.ruoyi.mqtt.util;
+
+import com.ruoyi.mqtt.config.MqttConfig;
+import com.ruoyi.mqtt.constant.MqttTopicConstants;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PostConstruct;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * MQTT主题工具类
+ * 支持从配置文件动态读取ProductID
+ */
+@Component
+public class MqttTopicUtil {
+    
+    /**
+     * 主题解析正则: /${ProductID}/${DeviceID}/${ShortTopic}
+     */
+    private static final Pattern TOPIC_PATTERN = Pattern.compile("^/([^/]+)/([^/]+)(/.*)?$");
+    
+    @Autowired
+    private MqttConfig mqttConfig;
+    
+    private static MqttTopicUtil instance;
+    
+    @PostConstruct
+    public void init() {
+        instance = this;
+    }
+    
+    /**
+     * 获取配置的ProductID
+     * @return ProductID
+     */
+    public static String getProductId() {
+        return instance != null ? instance.mqttConfig.getProductId() : "robot4inspection";
+    }
+    
+    /**
+     * 获取配置的DeviceID
+     * @return DeviceID
+     */
+    public static String getDeviceId() {
+        return instance != null ? instance.mqttConfig.getDeviceId() : "ld000001";
+    }
+    
+    /**
+     * 获取完整主题前缀(productId + deviceId)
+     * @return 主题前缀: /${ProductID}/${DeviceID}
+     */
+    public static String getPrefix() {
+        return MqttTopicConstants.buildPrefix(getProductId(), getDeviceId());
+    }
+    
+    /**
+     * 构建完整主题
+     * @param shortTopic 短主题名
+     * @return 完整主题: /${ProductID}/${DeviceID}/${ShortTopic}
+     */
+    public static String buildTopic(String shortTopic) {
+        return MqttTopicConstants.buildTopic(getPrefix(), shortTopic);
+    }
+    
+    /**
+     * 构建完整主题(保留兼容性,带deviceId参数但会被忽略)
+     * @param deviceId 设备ID(已被废弃,使用配置中的deviceId)
+     * @param shortTopic 短主题名
+     * @return 完整主题
+     */
+    public static String buildTopic(String deviceId, String shortTopic) {
+        return buildTopic(shortTopic);
+    }
+    
+    /**
+     * 构建通配符订阅主题(订阅所有设备)
+     * @param shortTopic 短主题名
+     * @return 通配符主题: /${ProductID}/+/${ShortTopic}
+     */
+ /*   public static String buildWildcardTopic(String shortTopic) {
+        return MqttTopicConstants.buildWildcardTopic(getProductId(), shortTopic);
+    }*/
+    
+    /**
+     * 从完整主题中提取设备ID
+     * @param fullTopic 完整主题
+     * @return 设备ID,解析失败返回null
+     */
+    public static String extractDeviceId(String fullTopic) {
+        Matcher matcher = TOPIC_PATTERN.matcher(fullTopic);
+        if (matcher.matches()) {
+            return matcher.group(2);
+        }
+        return null;
+    }
+    
+    /**
+     * 从完整主题中提取产品ID
+     * @param fullTopic 完整主题
+     * @return 产品ID,解析失败返回null
+     */
+    public static String extractProductId(String fullTopic) {
+        Matcher matcher = TOPIC_PATTERN.matcher(fullTopic);
+        if (matcher.matches()) {
+            return matcher.group(1);
+        }
+        return null;
+    }
+    
+    /**
+     * 从完整主题中提取短主题名
+     * @param fullTopic 完整主题
+     * @return 短主题名,解析失败返回null
+     */
+    public static String extractShortTopic(String fullTopic) {
+        Matcher matcher = TOPIC_PATTERN.matcher(fullTopic);
+        if (matcher.matches()) {
+            return matcher.group(3);
+        }
+        return null;
+    }
+    
+    /**
+     * 验证主题是否符合LD导航系统规范
+     * @param fullTopic 完整主题
+     * @return true:符合规范, false:不符合
+     */
+    public static boolean isValidTopic(String fullTopic) {
+        Matcher matcher = TOPIC_PATTERN.matcher(fullTopic);
+        if (!matcher.matches()) {
+            return false;
+        }
+        String productId = matcher.group(1);
+        return getProductId().equals(productId);
+    }
+}

+ 38 - 0
ruoyi-admin/src/main/java/com/ruoyi/robot/config/TaskSchedulerConfig.java

@@ -0,0 +1,38 @@
+package com.ruoyi.robot.config;
+
+import com.ruoyi.robot.task.LdTaskScheduler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 定时任务配置
+ * 应用启动时自动启动任务调度器
+ */
+@Configuration
+public class TaskSchedulerConfig {
+
+    private static final Logger log = LoggerFactory.getLogger(TaskSchedulerConfig.class);
+
+    @Autowired
+    private LdTaskScheduler ldTaskScheduler;
+
+    /**
+     * 应用启动后自动启动定时任务调度器
+     */
+    @Bean
+    public ApplicationRunner taskSchedulerRunner() {
+        return applicationArguments -> {
+            log.info("正在启动定时任务调度器...");
+            try {
+                ldTaskScheduler.start();
+                log.info("定时任务调度器启动成功");
+            } catch (Exception e) {
+                log.error("定时任务调度器启动失败", e);
+            }
+        };
+    }
+}

+ 332 - 0
ruoyi-admin/src/main/java/com/ruoyi/robot/task/LdTaskScheduler.java

@@ -0,0 +1,332 @@
+package com.ruoyi.robot.task;
+
+import java.time.Instant;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.scheduling.support.SimpleTriggerContext;
+import org.springframework.stereotype.Component;
+import org.springframework.scheduling.support.CronTrigger;
+
+import java.time.Instant;
+import java.util.Date;
+
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONArray;
+import com.ruoyi.common.utils.DateUtils;
+import com.ruoyi.mqtt.service.MqttSendService;
+import com.ruoyi.robot.domain.LdTask;
+import com.ruoyi.robot.domain.LdTaskExecutionLog;
+import com.ruoyi.robot.service.ILdTaskExecutionLogService;
+import com.ruoyi.robot.service.ILdTaskService;
+/**
+ * 定时任务调度器
+ * 负责检查和执行定时任务
+ */
+@Component
+public class LdTaskScheduler {
+
+    private static final Logger log = LoggerFactory.getLogger(LdTaskScheduler.class);
+
+    private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);
+
+    // 今日已执行的任务缓存(任务ID -> 执行时间)
+    private final ConcurrentHashMap<Long, Date> executedTasksToday = new ConcurrentHashMap<>();
+
+    // 最后清理日期
+    private long lastCleanupDate = 0;
+
+    @Autowired
+    private ILdTaskService ldTaskService;
+
+    @Autowired
+    private ILdTaskExecutionLogService ldTaskExecutionLogService;
+
+    @Autowired(required = false)
+    private MqttSendService mqttSendService;
+
+    @Value("${mqtt.deviceId:ld000001}")
+    private String defaultDeviceId;
+
+    /**
+     * 启动定时任务检查
+     */
+    public void start() {
+        // 每分钟检查一次待执行的任务
+        scheduler.scheduleAtFixedRate(() -> {
+            try {
+                checkAndExecuteScheduledTasks();
+            } catch (Exception e) {
+                log.error("检查定时任务异常", e);
+            }
+        }, 0, 1, TimeUnit.MINUTES);
+
+        // 每天凌晨清理已执行记录
+        scheduler.scheduleAtFixedRate(() -> {
+            try {
+                cleanupExecutedTasksCache();
+            } catch (Exception e) {
+                log.error("清理定时任务缓存异常", e);
+            }
+        }, 0, 1, TimeUnit.HOURS);
+
+        log.info("定时任务调度器已启动");
+    }
+
+    /**
+     * 检查并执行到期的定时任务
+     */
+    private void checkAndExecuteScheduledTasks() {
+        // 每天零点重置缓存
+        cleanupDailyCacheIfNeeded();
+
+        // 获取待执行的定时任务
+        List<LdTask> pendingTasks = ldTaskService.selectPendingScheduledTasks();
+
+        if (pendingTasks == null || pendingTasks.isEmpty()) {
+            return;
+        }
+
+        Date now = new Date();
+        int currentHour = now.getHours();
+        int currentMinute = now.getMinutes();
+        int currentDayOfWeek = now.getDay(); // 0=周日, 1=周一, ...
+
+        for (LdTask task : pendingTasks) {
+            try {
+                // 检查任务是否应该现在执行
+                if (shouldExecuteNow(task, currentHour, currentMinute, currentDayOfWeek)) {
+                    // 检查是否今日已执行
+                    if (executedTasksToday.containsKey(task.getId())) {
+                        log.debug("任务 {} 已于今日执行过,跳过", task.getTaskName());
+                        continue;
+                    }
+
+                    log.info("执行定时任务: {} (地图: {})", task.getTaskName(), task.getMapName());
+                    executeTask(task);
+
+                    // 标记为已执行
+                    executedTasksToday.put(task.getId(), now);
+                }
+            } catch (Exception e) {
+                log.error("处理定时任务 {} 异常", task.getTaskName(), e);
+            }
+        }
+    }
+
+    /**
+     * 判断任务是否应该现在执行
+     */
+    private boolean shouldExecuteNow(LdTask task, int currentHour, int currentMinute, int currentDayOfWeek) {
+        // 检查Cron表达式或执行时间
+        String executeTime = task.getExecuteTime();
+        String executeWeekdays = task.getExecuteWeekdays();
+
+        if (executeTime == null || executeTime.isEmpty()) {
+            return false;
+        }
+
+        // 解析执行时间
+        String[] timeParts = executeTime.split(":");
+        if (timeParts.length < 2) {
+            return false;
+        }
+
+        int taskHour;
+        int taskMinute;
+        try {
+            taskHour = Integer.parseInt(timeParts[0]);
+            taskMinute = Integer.parseInt(timeParts[1]);
+        } catch (NumberFormatException e) {
+            return false;
+        }
+
+        // 检查时间是否匹配(允许1分钟误差)
+        if (currentHour != taskHour || Math.abs(currentMinute - taskMinute) > 1) {
+            return false;
+        }
+
+        // 检查星期几
+        if (executeWeekdays != null && !executeWeekdays.isEmpty()) {
+            String[] weekdays = executeWeekdays.split(",");
+            boolean dayMatch = false;
+            for (String day : weekdays) {
+                int dayValue = Integer.parseInt(day.trim());
+                // 转换:前端1-7(周一到周日) -> 后端1-7(周一到周日),但Calendar的dayOfWeek是1=周日
+                if (dayValue == 1 && currentDayOfWeek == 0) {
+                    dayMatch = true;
+                    break;
+                } else if (dayValue == currentDayOfWeek) {
+                    dayMatch = true;
+                    break;
+                }
+            }
+            if (!dayMatch) {
+                return false;
+            }
+        } else {
+            // 没有指定星期,默认每天执行
+        }
+
+        return true;
+    }
+
+    /**
+     * 执行任务
+     */
+    private void executeTask(LdTask task) {
+        String deviceId = task.getDeviceId() != null ? task.getDeviceId() : defaultDeviceId;
+
+        // 创建执行记录
+        LdTaskExecutionLog logTask = new LdTaskExecutionLog();
+        logTask.setTaskId(task.getId());
+        logTask.setTaskName(task.getTaskName());
+        logTask.setMapName(task.getMapName());
+        logTask.setExecuteTime(new Date());
+        logTask.setStatus("running");
+        logTask.setDeviceId(deviceId);
+
+        // 解析目标点数量
+        try {
+            if (task.getWaypointCoords() != null) {
+                JSONArray coords = JSON.parseArray(task.getWaypointCoords());
+                logTask.setTotalWaypoints(coords.size());
+            }
+        } catch (Exception e) {
+            log.warn("解析目标点数量失败", e);
+        }
+
+        ldTaskExecutionLogService.insertLdTaskExecutionLog(logTask);
+
+        // 更新任务状态为运行中
+        ldTaskService.updateTaskStatus(task.getId(), "running");
+
+        // 通过MQTT发送任务执行指令
+        try {
+            if (mqttSendService != null) {
+                // 构建任务数据并发送
+                String taskName = task.getTaskName();
+                boolean sendResult = mqttSendService.startTask(task.getMapName(), taskName);
+
+                if (!sendResult) {
+                    log.warn("MQTT发送任务启动指令失败: {}", taskName);
+                }
+            }
+        } catch (Exception e) {
+            log.error("发送MQTT任务指令异常", e);
+        }
+    }
+
+    /**
+     * 任务执行完成回调
+     */
+    public void onTaskCompleted(Long taskId, String status, String errorMessage) {
+        try {
+            LdTask task = ldTaskService.selectLdTaskById(taskId);
+            if (task == null) {
+                return;
+            }
+
+            // 更新任务状态
+            String newStatus = "completed".equals(status) ? "idle" : "failed";
+            ldTaskService.updateTaskStatus(taskId, newStatus);
+
+            // 更新执行记录
+            List<LdTaskExecutionLog> logs = ldTaskExecutionLogService.selectLdTaskExecutionLogByTaskId(taskId);
+            if (logs != null && !logs.isEmpty()) {
+                LdTaskExecutionLog latestLog = logs.get(0);
+                if ("running".equals(latestLog.getStatus())) {
+                    latestLog.setEndTime(new Date());
+                    latestLog.setStatus(status);
+                    if (errorMessage != null) {
+                        latestLog.setErrorMessage(errorMessage);
+                    }
+                    ldTaskExecutionLogService.updateLdTaskExecutionLog(latestLog);
+                }
+            }
+
+            // 更新任务统计
+            task.setLastExecuteTime(new Date());
+            task.setLastExecuteStatus(status);
+            task.setTotalExecuteCount(task.getTotalExecuteCount() != null ? task.getTotalExecuteCount() + 1 : 1);
+
+            // 计算下次执行时间
+            if (task.getCronExpression() != null && !task.getCronExpression().isEmpty()) {
+                task.setNextExecuteTime(calculateNextExecuteTime(task.getCronExpression()));
+            }
+
+            ldTaskService.updateLdTask(task);
+
+        } catch (Exception e) {
+            log.error("处理任务完成回调异常", e);
+        }
+    }
+
+    /**
+     * 每天零点清理缓存
+     */
+    private void cleanupDailyCacheIfNeeded() {
+        long today = DateUtils.dateTimeNow("yyyyMMdd").hashCode();
+        if (lastCleanupDate != today) {
+            executedTasksToday.clear();
+            lastCleanupDate = today;
+            log.info("已清理定时任务执行缓存");
+        }
+    }
+
+    /**
+     * 每小时清理缓存(备用机制)
+     */
+    private void cleanupExecutedTasksCache() {
+        long today = DateUtils.dateTimeNow("yyyyMMdd").hashCode();
+        if (lastCleanupDate != today) {
+            executedTasksToday.clear();
+            lastCleanupDate = today;
+        }
+    }
+
+    /**
+     * 根据Cron表达式计算下次执行时间
+     */
+/*    private Date calculateNextExecuteTime(String cronExpression) {
+        try {
+            org.springframework.scheduling.support.CronTrigger cronTrigger =
+                new org.springframework.scheduling.support.CronTrigger(cronExpression);
+            return cronTrigger.nextExecutionTime(new java.util.Date());
+        } catch (Exception e) {
+            log.warn("解析Cron表达式失败: {}", cronExpression, e);
+            return null;
+        }
+    }*/
+    private Date calculateNextExecuteTime(String cronExpression) {
+        try {
+            CronTrigger cronTrigger = new CronTrigger(cronExpression);
+
+            SimpleTriggerContext triggerContext = new SimpleTriggerContext();
+
+            return cronTrigger.nextExecutionTime(triggerContext);
+
+        } catch (Exception e) {
+            log.warn("解析Cron表达式失败: {}", cronExpression, e);
+            return null;
+        }
+    }
+
+    /**
+     * 停止调度器
+     */
+    public void stop() {
+        scheduler.shutdown();
+        log.info("定时任务调度器已停止");
+    }
+}

+ 198 - 0
ruoyi-admin/src/main/java/com/ruoyi/web/controller/robot/AsmController.java

@@ -0,0 +1,198 @@
+package com.ruoyi.web.controller.robot;
+
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.mqtt.service.MqttSendService;
+import com.ruoyi.robot.domain.LdMapProject;
+import com.ruoyi.robot.service.ILdMapProjectService;
+import io.netty.util.internal.ObjectUtil;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+
+import static java.util.List.*;
+
+/**
+ * ASM远程功能调用控制器
+ * 处理地图录制、构建、实时建图等ASM功能
+ */
+@Api(tags = "ASM远程功能调用")
+@RestController
+@RequestMapping("/robot/control/asm")
+public class AsmController {
+
+    @Resource
+    private MqttSendService mqttSendService;
+
+    @Resource
+    private ILdMapProjectService  ldMapProjectService;
+
+    // ==================== 录制操作 ====================
+
+    @ApiOperation("开始录制")
+    @PostMapping("/record/start")
+    public AjaxResult startRecord(
+            @ApiParam("设备ID") @RequestParam("deviceId") String deviceId,
+            @ApiParam("地图名称") @RequestParam("mapName") String mapName
+    ) {
+        if (deviceId.isEmpty() || mapName.isEmpty()) {
+            return AjaxResult.error("请输入地图名称");
+        }
+        LdMapProject existMap  = ldMapProjectService.selectMapByName(mapName);
+        if (existMap  != null) {
+            return AjaxResult.error("地图已存在,请重新输入名称");
+        }
+
+        LdMapProject ldMapProject = new LdMapProject();
+        ldMapProject.setDeviceId(deviceId);
+        ldMapProject.setMapName(mapName);
+        ldMapProject.setStatus("recording");
+        ldMapProjectService.insertLdMapProject(ldMapProject);
+
+        boolean result = mqttSendService.startRecord(mapName);
+        return result ? AjaxResult.success("开始录制指令发送成功") : AjaxResult.error("开始录制指令发送失败");
+    }
+
+    @ApiOperation("停止录制")
+    @PostMapping("/record/stop")
+    public AjaxResult stopRecord(
+//            @ApiParam("设备ID") @RequestParam("deviceId") String deviceId,
+            @ApiParam("地图名称") @RequestParam("mapName") String mapName
+    ) {
+        boolean result = mqttSendService.stopRecord();
+        LdMapProject ldMapProject = new LdMapProject();
+        ldMapProject.setMapName(mapName);
+        ldMapProject.setStatus("unavailable");
+        ldMapProjectService.updateLdMapProject(ldMapProject);
+        return result ? AjaxResult.success("停止录制指令发送成功") : AjaxResult.error("停止录制指令发送失败");
+    }
+
+    // ==================== 构建地图操作 ====================
+
+    @ApiOperation("开始构建地图")
+    @PostMapping("/build/start")
+    public AjaxResult startBuild(
+//            @ApiParam("设备ID") @RequestParam("deviceId") String deviceId,
+            @ApiParam("地图名称") @RequestParam("mapName") String mapName,
+            @ApiParam("构建步骤(可选,如:recon,kfmix,octomap,tilemap,potree)") @RequestParam(value = "buildSteps", required = false) String buildSteps
+    ) {
+        List<String> steps = null;
+        if (buildSteps != null && !buildSteps.isEmpty()) {
+            steps = Arrays.asList(buildSteps.split(","));
+        }
+        boolean result = mqttSendService.startBuild(mapName, steps);
+        return result ? AjaxResult.success("开始构建地图指令发送成功") : AjaxResult.error("开始构建地图指令发送失败");
+    }
+
+    @ApiOperation("停止构建地图")
+    @PostMapping("/build/stop")
+    public AjaxResult stopBuild(
+//            @ApiParam("设备ID") @RequestParam("deviceId") String deviceId
+    ) {
+        boolean result = mqttSendService.stopBuild();
+        return result ? AjaxResult.success("停止构建地图指令发送成功") : AjaxResult.error("停止构建地图指令发送失败");
+    }
+
+    @ApiOperation("地图构建完成")
+    @PostMapping("/build/completed")
+    public AjaxResult completed(
+            @ApiParam("地图名称") @RequestParam("mapName") String mapName,
+            @ApiParam("函数名称") @RequestParam(required = false) String funcName
+    ) {
+        if (StringUtils.isBlank(mapName)) {
+            return AjaxResult.error("地图名称不能为空");
+        }
+
+        if (StringUtils.isBlank(funcName)) {
+            return AjaxResult.success();
+        }
+
+        String status = null;
+
+        switch (funcName) {
+            case "MapBuilder.start":
+                status = "available";
+                break;
+
+            case "ASM.map_slam.start":
+                status = "slaming";
+                break;
+
+            case "ASM.map_slam.stop":
+                status = "available";
+                break;
+
+            default:
+                break;
+        }
+
+        // 需要更新状态时再执行
+        if (StringUtils.isNotBlank(status)) {
+            LdMapProject ldMapProject = new LdMapProject();
+            ldMapProject.setMapName(mapName);
+            ldMapProject.setStatus(status);
+
+            ldMapProjectService.updateLdMapProject(ldMapProject);
+        }
+
+        return AjaxResult.success();
+    }
+
+    // ==================== 实时建图操作 ====================
+
+    @ApiOperation("开始实时SLAM")
+    @PostMapping("/slam/start")
+    public AjaxResult startSlam(
+            @ApiParam("设备ID") @RequestParam("deviceId") String deviceId,
+            @ApiParam("地图名称") @RequestParam("mapName") String mapName
+    ) {
+        LdMapProject existMap  = ldMapProjectService.selectMapByName(mapName);
+        if (existMap  != null) {
+            return AjaxResult.error("地图已存在,请重新输入名称");
+        }
+        LdMapProject ldMapProject = new LdMapProject();
+        ldMapProject.setDeviceId(deviceId);
+        ldMapProject.setMapName(mapName);
+        ldMapProject.setStatus("slaming");
+        ldMapProjectService.insertLdMapProject(ldMapProject);
+
+        boolean result = mqttSendService.startSlam(mapName);
+        return result ? AjaxResult.success("开始实时建图指令发送成功") : AjaxResult.error("开始实时建图指令发送失败");
+    }
+
+    @ApiOperation("停止实时SLAM")
+    @PostMapping("/slam/stop")
+    public AjaxResult stopSlam(
+//            @ApiParam("设备ID") @RequestParam("deviceId") String deviceId
+    ) {
+        boolean result = mqttSendService.stopSlam();
+        return result ? AjaxResult.success("停止实时建图指令发送成功") : AjaxResult.error("停止实时建图指令发送失败");
+    }
+
+    // ==================== 标准导航操作 ====================
+
+    @ApiOperation("启动标准导航")
+    @PostMapping("/nav/standard/start")
+    public AjaxResult startNavStandard(
+//            @ApiParam("设备ID") @RequestParam("deviceId") String deviceId,
+            @ApiParam("地图名称") @RequestParam("mapName") String mapName
+    ) {
+        boolean result = mqttSendService.startNavStandard(mapName);
+        return result ? AjaxResult.success("启动标准导航指令发送成功") : AjaxResult.error("启动标准导航指令发送失败");
+    }
+
+    @ApiOperation("结束标准导航")
+    @PostMapping("/nav/standard/stop")
+    public AjaxResult stopNavStandard(
+//            @ApiParam("设备ID") @RequestParam("deviceId") String deviceId
+    ) {
+        boolean result = mqttSendService.stopNavStandard();
+        return result ? AjaxResult.success("结束标准导航指令发送成功") : AjaxResult.error("结束标准导航指令发送失败");
+    }
+}

+ 538 - 0
ruoyi-admin/src/main/java/com/ruoyi/web/controller/robot/LdNavController.java

@@ -0,0 +1,538 @@
+package com.ruoyi.web.controller.robot;
+
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONObject;
+import com.ruoyi.common.core.domain.R;
+import com.ruoyi.common.core.redis.RedisCache;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.robot.domain.LdMapProject;
+import com.ruoyi.robot.domain.LdRoadmap;
+import com.ruoyi.robot.domain.vo.LdMapListVo;
+import com.ruoyi.robot.service.ILdMapProjectService;
+import com.ruoyi.robot.service.ILdRoadmapService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.client.RestTemplate;
+
+import java.math.BigDecimal;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * LD导航系统HTTP接口
+ * 提供地图工程、瓦片地图、路网矢量地图相关接口
+ *
+ * 接口前缀: /v1
+ */
+@Api(tags = "LD导航系统HTTP接口")
+@RestController
+@RequestMapping("/v1")
+public class LdNavController {
+
+    private static final Logger log = LoggerFactory.getLogger(LdNavController.class);
+
+    @Autowired
+    private ILdMapProjectService mapProjectService;
+
+    @Autowired
+    private ILdRoadmapService roadmapService;
+
+    @Autowired
+    private RedisCache redisCache;
+
+    @Value("${ld-nav.url:http://192.168.0.102:8086}")
+    private String ldNavUrl;
+
+    @Value("${tilemap.base-url:http://192.168.0.30}")
+    private String tilemapBaseUrl;
+
+    private final RestTemplate restTemplate = new RestTemplate();
+
+    // config.json 缓存,按地图名索引
+    private final ConcurrentHashMap<String, AtomicReference<JSONObject>> configCache = new ConcurrentHashMap<>();
+
+    // ==================== 3.1 地图工程接口 ====================
+
+    /**
+     * 3.1.1 获取地图列表
+     * GET /v1/map/list
+     */
+    @ApiOperation("获取地图列表")
+    @GetMapping("/map/list")
+    public ResponseEntity<Map<String, Object>> getMapList() {
+        try {
+            // 先从本地数据库获取
+            List<LdMapListVo> mapList = mapProjectService.selectMapList();
+
+            if (mapList != null && !mapList.isEmpty()) {
+                Map<String, Object> response = new HashMap<>();
+                response.put("status", true);
+
+                List<String> maps = new java.util.ArrayList<>();
+                List<String> states = new java.util.ArrayList<>();
+                for (LdMapListVo vo : mapList) {
+                    maps.add(vo.getMapName());
+                    states.add(vo.getStatus());
+                }
+                response.put("maps", maps);
+                response.put("states", states);
+
+                return ResponseEntity.ok(response);
+            }
+
+            // 返回空列表
+            Map<String, Object> response = new HashMap<>();
+            response.put("status", true);
+            response.put("maps", new java.util.ArrayList<>());
+            response.put("states", new java.util.ArrayList<>());
+            return ResponseEntity.ok(response);
+
+        } catch (Exception e) {
+            log.error("获取地图列表异常", e);
+            return buildErrorResponse(4, "内部错误: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 3.1.2 获取当前地图
+     * GET /v1/map/using
+     */
+    @ApiOperation("获取当前使用中的地图")
+    @GetMapping("/map/using")
+    public ResponseEntity<Map<String, Object>> getCurrentMap(
+            @ApiParam("设备ID") @RequestParam(required = false) String deviceId) {
+        try {
+            // 尝试从本地数据库获取
+            LdMapProject currentMap = mapProjectService.selectCurrentMap(deviceId);
+
+            if (currentMap != null) {
+                Map<String, Object> response = new HashMap<>();
+                response.put("status", true);
+                response.put("name", currentMap.getMapName());
+                return ResponseEntity.ok(response);
+            }
+
+            // 如果本地没有数据,尝试从LD导航系统获取
+            String url = ldNavUrl + "/v1/map/using";
+            if (StringUtils.isNotBlank(deviceId)) {
+                url += "?deviceId=" + deviceId;
+            }
+            ResponseEntity<String> ldResponse = restTemplate.getForEntity(url, String.class);
+
+            if (ldResponse.getStatusCode() == HttpStatus.OK && ldResponse.getBody() != null) {
+                return ResponseEntity.ok(JSON.parseObject(ldResponse.getBody()));
+            }
+
+            return buildErrorResponse(2, "未找到当前使用中的地图");
+
+        } catch (Exception e) {
+            log.error("获取当前地图异常", e);
+            return buildErrorResponse(4, "内部错误: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 3.1.3 获取地图缩略图
+     * GET /v1/map/thumbnail?map=地图名
+     */
+    @ApiOperation("获取地图缩略图")
+    @GetMapping("/map/thumbnail")
+    public R getMapThumbnail(@ApiParam("地图名称") @RequestParam("map") String mapName) {
+        if (StringUtils.isBlank(mapName)) {
+            log.warn("获取地图缩略图失败:mapName 为空");
+            return R.fail("获取地图缩略图失败");
+        }
+
+        try {
+            String thumbnailUrl = mapProjectService.getThumbnailUrl(mapName);
+
+            if (StringUtils.isBlank(thumbnailUrl)) {
+                log.info("地图 {} 的缩略图为空", mapName);
+                return R.fail("地图缩略图为空");
+            }
+
+            return R.ok(tilemapBaseUrl + thumbnailUrl);
+        } catch (Exception e) {
+            log.error("获取地图 {} 缩略图异常", mapName, e);
+            return R.fail("获取地图缩略图异常");
+        }
+    }
+
+    /**
+     * 3.1.4 地图重命名
+     * POST /v1/map/rename
+     */
+    @ApiOperation("地图重命名")
+    @PostMapping("/map/rename")
+    public ResponseEntity<Map<String, Object>> renameMap(@RequestBody Map<String, String> params) {
+        try {
+            String map = params.get("map");
+            String rename = params.get("rename");
+
+            if (StringUtils.isBlank(map) || StringUtils.isBlank(rename)) {
+                return buildErrorResponse(1, "请求参数不合法:缺少map或rename参数");
+            }
+
+            // 先尝试本地重命名
+            int result = mapProjectService.renameMap(map, rename);
+            if (result > 0) {
+                Map<String, Object> response = new HashMap<>();
+                response.put("status", true);
+                return ResponseEntity.ok(response);
+            }
+
+            return buildErrorResponse(2, "地图不存在");
+
+        } catch (Exception e) {
+            log.error("地图重命名异常", e);
+            return buildErrorResponse(4, "内部错误: " + e.getMessage());
+        }
+    }
+
+    // ==================== 3.2 瓦片地图接口 ====================
+
+    /**
+     * 3.2.1 获取瓦片地图参数
+     * GET /v1/tilemap/details?map=地图名
+     */
+    @ApiOperation("获取瓦片地图参数")
+    @GetMapping("/tilemap/details")
+    public ResponseEntity<Map<String, Object>> getTilemapDetails(
+            @ApiParam("地图名称") @RequestParam("map") String mapName) {
+        try {
+            if (StringUtils.isBlank(mapName)) {
+                return buildErrorResponse(1, "请求参数不合法:缺少map参数");
+            }
+
+            // 从config.json获取配置
+            JSONObject tilemapConfig = getTilemapConfig(mapName);
+            if (tilemapConfig == null) {
+                return buildErrorResponse(2, "地图配置不存在");
+            }
+
+            Map<String, Object> response = new HashMap<>();
+            response.put("status", true);
+
+            // 获取图片尺寸
+            int imageWidth = tilemapConfig.getIntValue("ImageWidth", 256);
+            int imageHeight = tilemapConfig.getIntValue("ImageHeight", 256);
+
+            // 优先使用config.json中的地图参数
+            double originX = tilemapConfig.getDoubleValue("OriginX");
+            double originY = tilemapConfig.getDoubleValue("OriginY");
+            double mapWidth = tilemapConfig.getDoubleValue("MapWidth");
+            double mapHeight = tilemapConfig.getDoubleValue("MapHeight");
+            double resolution = tilemapConfig.getDoubleValue("Resolution");
+
+            // 如果config.json中没有这些字段,则使用Layers信息计算
+            if (mapWidth == 0 || mapHeight == 0) {
+                List<JSONObject> layers = tilemapConfig.getList("Layers", JSONObject.class);
+                if (layers == null || layers.isEmpty()) {
+                    return buildErrorResponse(2, "地图配置中没有Layers信息");
+                }
+
+                // 使用最高层级(最后一个)来计算地图总尺寸
+                JSONObject highestLayer = layers.get(layers.size() - 1);
+                int highestColCount = highestLayer.getIntValue("ColCount");
+                int highestRowCount = highestLayer.getIntValue("RowCount");
+                resolution = highestLayer.getDoubleValue("Resolution");
+
+                // 计算地图总尺寸
+                // 地图宽度 = 最高层列数 * 图片宽度 * 分辨率
+                // 地图高度 = 最高层行数 * 图片高度 * 分辨率
+                mapWidth = highestColCount * imageWidth * resolution;
+                mapHeight = highestRowCount * imageHeight * resolution;
+            }
+
+            Map<String, Object> projection = new HashMap<>();
+            projection.put("extent", new BigDecimal[]{
+                    BigDecimal.valueOf(originX),
+                    BigDecimal.valueOf(originY),
+                    BigDecimal.valueOf(originX + mapWidth),
+                    BigDecimal.valueOf(originY + mapHeight)
+            });
+
+            response.put("projection", projection);
+            response.put("layer_cnt", tilemapConfig.getIntValue("LayerCount"));
+            response.put("min_zoom", 1);
+            response.put("max_zoom", tilemapConfig.getIntValue("LayerCount", 5));
+            response.put("image_width", imageWidth);
+            response.put("image_height", imageHeight);
+            response.put("origin_x", originX);
+            response.put("origin_y", originY);
+            response.put("resolution", resolution);
+            response.put("map_width", mapWidth);
+            response.put("map_height", mapHeight);
+
+            return ResponseEntity.ok(response);
+
+        } catch (Exception e) {
+            log.error("获取瓦片地图参数异常", e);
+            return buildErrorResponse(4, "内部错误: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 3.2.2 获取地图瓦片
+     * GET /v1/tilemap/tile/{地图名}/{层级}/{x索引}/{y索引}
+     *
+     * 说明:
+     * - 层级: 起始值为1,代表第一层级 (layerIndex = zoom - 1)
+     * - x索引: 列索引(Col),起始值为0
+     * - y索引: 行索引(Row),起始值为0
+     * - Images数组按Col分组,每组内按Row排列
+     *   所以 tileIndex = Col * RowCount + Row = x * rowCount + y
+     */
+    @ApiOperation("获取地图瓦片")
+    @GetMapping("/tilemap/tile/{mapName}/{zoom}/{x}/{y}")
+    public ResponseEntity<byte[]> getTile(
+            @ApiParam("地图名称") @PathVariable("mapName") String mapName,
+            @ApiParam("层级") @PathVariable("zoom") int zoom,
+            @ApiParam("X索引(列)") @PathVariable("x") int x,
+            @ApiParam("Y索引(行)") @PathVariable("y") int y) {
+
+        try {
+            if (StringUtils.isBlank(mapName) || zoom < 1 || x < 0 || y < 0) {
+                return ResponseEntity.badRequest().build();
+            }
+
+            // 获取config.json配置
+            JSONObject tilemapConfig = getTilemapConfig(mapName);
+            if (tilemapConfig == null) {
+                log.warn("获取地图 {} 的config.json失败", mapName);
+                return ResponseEntity.notFound().build();
+            }
+
+            int layerIndex = zoom - 1;
+
+            // 获取该层级的行列数
+            int colCount = getLayerColCount(tilemapConfig, layerIndex);
+            int rowCount = getLayerRowCount(tilemapConfig, layerIndex);
+
+            if (colCount <= 0 || rowCount <= 0) {
+                log.warn("地图 {} 层级 {} 的行列数无效: colCount={}, rowCount={}", mapName, zoom, colCount, rowCount);
+                return ResponseEntity.notFound().build();
+            }
+
+            // 验证坐标范围
+            if (x < 0 || x >= colCount || y < 0 || y >= rowCount) {
+                log.warn("瓦片坐标超出范围: x={}, y={}, colCount={}, rowCount={},layerIndex={}", x, y, colCount, rowCount, layerIndex);
+                return ResponseEntity.notFound().build();
+            }
+
+            // Images数组按Col分组,每组内按Row排列
+            // 所以 tileIndex = Col * RowCount + Row = x * rowCount + y
+            int tileIndex = x * rowCount + y;
+
+            // 构建瓦片URL: tilemapBaseUrl/tilemap/{mapName}/{layerIndex}/{tileIndex}.png
+            String tileUrl = tilemapBaseUrl + "/tilemap/" + mapName + "/" + layerIndex + "/" + tileIndex + ".png";
+            log.info("获取瓦片: {}, tileIndex={}, col={}, row={}, colCount={}, rowCount={}",
+                    tileUrl, tileIndex, x, y, colCount, rowCount);
+
+            ResponseEntity<byte[]> tileResponse = restTemplate.getForEntity(tileUrl, byte[].class);
+            if (tileResponse.getStatusCode() == HttpStatus.OK && tileResponse.getBody() != null) {
+                return ResponseEntity.ok()
+                        .contentType(MediaType.IMAGE_PNG)
+                        .body(tileResponse.getBody());
+            }
+
+            return ResponseEntity.notFound().build();
+
+        } catch (Exception e) {
+            log.error("获取地图瓦片异常: map={}, zoom={}, x={}, y={}", mapName, zoom, x, y, e);
+            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
+        }
+    }
+
+    // ==================== 3.3 路网矢量地图接口 ====================
+
+    /**
+     * 3.3.1 获取路网数据
+     * GET /v1/roadmap/geojson?map=地图名
+     */
+    @ApiOperation("获取路网GeoJSON数据")
+    @GetMapping("/roadmap/geojson")
+    public ResponseEntity<Map<String, Object>> getRoadmapGeoJson(
+            @ApiParam("地图名称") @RequestParam("map") String mapName) {
+        try {
+            if (StringUtils.isBlank(mapName)) {
+                return buildErrorResponse(1, "请求参数不合法:缺少map参数");
+            }
+
+            // 先从本地数据库获取
+            String geoJson = roadmapService.getGeoJsonByMapName(mapName);
+
+            if (StringUtils.isNotBlank(geoJson)) {
+                Map<String, Object> response = new HashMap<>();
+                response.put("status", true);
+                response.put("data", JSON.parseObject(geoJson));
+                return ResponseEntity.ok(response);
+            }
+
+            return buildErrorResponse(2, "路网数据不存在");
+
+        } catch (Exception e) {
+            log.error("获取路网数据异常", e);
+            return buildErrorResponse(4, "内部错误: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 3.3.2 保存路网数据
+     * POST /v1/roadmap/geojson
+     */
+    @ApiOperation("保存路网GeoJSON数据")
+    @PostMapping("/roadmap/geojson")
+    public ResponseEntity<Map<String, Object>> saveRoadmapGeoJson(@RequestBody JSONObject geoJsonData) {
+        try {
+            String mapName = geoJsonData.getString("map");
+            String geoJson = geoJsonData.getString("features");
+
+            if (StringUtils.isBlank(mapName)) {
+                return buildErrorResponse(1, "请求参数不合法:缺少map参数");
+            }
+            Map<String, Object> response = new HashMap<>();
+            // 保存到本地数据库
+            int result = roadmapService.saveRoadmap(mapName, geoJson);
+            if (result < 0){
+                response.put("status", false);
+                return ResponseEntity.ok(response);
+            }
+
+            response.put("status", true);
+            return ResponseEntity.ok(response);
+
+        } catch (Exception e) {
+            log.error("保存路网数据异常", e);
+            return buildErrorResponse(4, "内部错误: " + e.getMessage());
+        }
+    }
+
+    // ==================== 辅助方法 ====================
+
+    /**
+     * 获取地图的config.json配置(带缓存)
+     */
+    private JSONObject getTilemapConfig(String mapName) {
+        if (StringUtils.isBlank(mapName)) {
+            return null;
+        }
+
+        AtomicReference<JSONObject> cacheRef = configCache.computeIfAbsent(mapName, k -> new AtomicReference<>());
+        JSONObject cached = cacheRef.get();
+        if (cached != null) {
+            return cached;
+        }
+
+        try {
+            // 从tilemapBaseUrl服务器获取config.json
+            String configUrl = tilemapBaseUrl + "/tilemap/" + mapName + "/config.json";
+            ResponseEntity<String> response = restTemplate.getForEntity(configUrl, String.class);
+            if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) {
+                JSONObject config = JSON.parseObject(response.getBody());
+                cacheRef.set(config);
+                return config;
+            }
+        } catch (Exception e) {
+            log.error("获取地图 {} 的config.json失败", mapName, e);
+        }
+
+        return null;
+    }
+
+    /**
+     * 根据层级索引获取该层级的ColCount
+     */
+    private int getLayerColCount(JSONObject config, int layerIndex) {
+        if (config == null) {
+            return -1;
+        }
+
+        List<JSONObject> layers = config.getList("Layers", JSONObject.class);
+        if (layers == null) {
+            return -1;
+        }
+
+        for (JSONObject layerObj : layers) {
+            if (layerObj.getIntValue("Index") == layerIndex) {
+                return layerObj.getIntValue("ColCount");
+            }
+        }
+        return -1;
+    }
+
+    /**
+     * 根据层级索引获取该层级的RowCount
+     */
+    private int getLayerRowCount(JSONObject config, int layerIndex) {
+        if (config == null) {
+            return -1;
+        }
+
+        List<JSONObject> layers = config.getList("Layers", JSONObject.class);
+        if (layers == null) {
+            return -1;
+        }
+
+        for (JSONObject layerObj : layers) {
+            if (layerObj.getIntValue("Index") == layerIndex) {
+                return layerObj.getIntValue("RowCount");
+            }
+        }
+        return -1;
+    }
+
+    /**
+     * 根据层级和坐标计算瓦片索引
+     * 索引 = y * ColCount + x
+     */
+    private int calculateTileIndex(int colCount, int x, int y) {
+        return y * colCount + x;
+    }
+
+    /**
+     * 构建错误响应
+     */
+    private ResponseEntity<Map<String, Object>> buildErrorResponse(int code, String msg) {
+        Map<String, Object> response = new HashMap<>();
+        response.put("status", false);
+        response.put("code", code);
+        response.put("msg", msg);
+        return ResponseEntity.ok(response);
+    }
+
+    /**
+     * 根据文件扩展名获取Content-Type
+     */
+    private String getContentType(String format) {
+        if (format == null) {
+            return "image/png";
+        }
+        switch (format.toLowerCase()) {
+            case "jpg":
+            case "jpeg":
+                return "image/jpeg";
+            case "png":
+                return "image/png";
+            case "gif":
+                return "image/gif";
+            case "webp":
+                return "image/webp";
+            default:
+                return "image/png";
+        }
+    }
+}

+ 539 - 0
ruoyi-admin/src/main/java/com/ruoyi/web/controller/robot/LdTaskController.java

@@ -0,0 +1,539 @@
+package com.ruoyi.web.controller.robot;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import javax.servlet.http.HttpServletResponse;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.ResponseEntity;
+import org.springframework.scheduling.support.CronTrigger;
+import org.springframework.scheduling.support.SimpleTriggerContext;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONArray;
+import com.alibaba.fastjson2.JSONObject;
+import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.page.TableDataInfo;
+import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.common.utils.poi.ExcelUtil;
+import com.ruoyi.robot.domain.LdTask;
+import com.ruoyi.robot.domain.LdTaskExecutionLog;
+import com.ruoyi.robot.service.ILdTaskExecutionLogService;
+import com.ruoyi.robot.service.ILdTaskService;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+
+/**
+ * LD导航系统任务管理接口
+ * 提供任务的增删改查及定时任务执行功能
+ * 
+ * 接口前缀: /v1
+ */
+@Api(tags = "LD导航系统任务管理接口")
+@RestController
+@RequestMapping("/v1")
+public class LdTaskController extends BaseController {
+
+    @Autowired
+    private ILdTaskService ldTaskService;
+
+    @Autowired
+    private ILdTaskExecutionLogService ldTaskExecutionLogService;
+
+    /**
+     * 1.1 获取任务列表
+     * GET /v1/path/list?map=地图名
+     */
+    @ApiOperation("获取任务列表")
+    @GetMapping("/path/list")
+    public ResponseEntity<Map<String, Object>> getTaskList(
+            @ApiParam("地图名称") @RequestParam(value = "map", required = false) String mapName) {
+        try {
+            Map<String, Object> response = new HashMap<>();
+            
+            List<LdTask> taskList;
+            if (mapName != null && !mapName.isEmpty()) {
+                taskList = ldTaskService.selectLdTaskListByMapName(mapName);
+            } else {
+                taskList = ldTaskService.selectLdTaskList(new LdTask());
+            }
+            
+            List<Map<String, Object>> paths = taskList.stream().map(task -> {
+                Map<String, Object> path = new HashMap<>();
+                path.put("taskId", task.getId());
+                path.put("taskName", task.getTaskName());
+                path.put("status", task.getStatus());
+                path.put("cron", task.getCronExpression());
+                path.put("executeTime", task.getExecuteTime());
+                path.put("executeWeekdays", task.getExecuteWeekdays());
+                path.put("repeatCount", task.getRepeatCount());
+                
+                // 解析目标点坐标
+                if (task.getWaypointCoords() != null && !task.getWaypointCoords().isEmpty()) {
+                    try {
+                        path.put("coord", JSON.parse(task.getWaypointCoords()));
+                    } catch (Exception e) {
+                        path.put("coord", new java.util.ArrayList<>());
+                    }
+                } else {
+                    path.put("coord", new java.util.ArrayList<>());
+                }
+                
+                // 解析目标点类型
+                if (task.getWaypointTypes() != null && !task.getWaypointTypes().isEmpty()) {
+                    try {
+                        path.put("plan", JSON.parse(task.getWaypointTypes()));
+                    } catch (Exception e) {
+                        path.put("plan", new java.util.ArrayList<>());
+                    }
+                } else {
+                    path.put("plan", new java.util.ArrayList<>());
+                }
+                
+                // 解析目标点动作
+                if (task.getWaypointActions() != null && !task.getWaypointActions().isEmpty()) {
+                    try {
+                        path.put("action", JSON.parse(task.getWaypointActions()));
+                    } catch (Exception e) {
+                        path.put("action", new java.util.ArrayList<>());
+                    }
+                } else {
+                    path.put("action", new java.util.ArrayList<>());
+                }
+                
+                path.put("map", task.getMapName());
+                path.put("lastExecuteTime", task.getLastExecuteTime());
+                path.put("totalExecuteCount", task.getTotalExecuteCount());
+                return path;
+            }).collect(Collectors.toList());
+            
+            response.put("status", true);
+            response.put("paths", paths);
+            response.put("total", paths.size());
+            
+            return ResponseEntity.ok(response);
+        } catch (Exception e) {
+            logger.error("获取任务列表异常", e);
+            return buildErrorResponse(4, "内部错误: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 1.2 获取任务详情
+     * GET /v1/path?map=地图名&path=任务名
+     */
+    @ApiOperation("获取任务详情")
+    @GetMapping("/path")
+    public ResponseEntity<Map<String, Object>> getTask(
+            @ApiParam("地图名称") @RequestParam("map") String mapName,
+            @ApiParam("任务名称") @RequestParam("path") String taskName) {
+        try {
+            LdTask task = ldTaskService.selectLdTaskByMapAndName(mapName, taskName);
+            
+            if (task == null) {
+                return buildErrorResponse(2, "任务不存在");
+            }
+            
+            Map<String, Object> response = new HashMap<>();
+            response.put("status", true);
+            response.put("taskId", task.getId());
+            response.put("taskName", task.getTaskName());
+            response.put("taskDesc", task.getTaskDesc());
+            response.put("status", task.getStatus());
+            response.put("cron", task.getCronExpression());
+            response.put("executeTime", task.getExecuteTime());
+            response.put("executeWeekdays", task.getExecuteWeekdays());
+            response.put("repeatCount", task.getRepeatCount());
+            response.put("coord_type", task.getCoordType());
+            response.put("map", task.getMapName());
+            
+            // 解析目标点坐标
+            if (task.getWaypointCoords() != null && !task.getWaypointCoords().isEmpty()) {
+                response.put("coord", JSON.parse(task.getWaypointCoords()));
+            } else {
+                response.put("coord", new java.util.ArrayList<>());
+            }
+            
+            // 解析目标点类型
+            if (task.getWaypointTypes() != null && !task.getWaypointTypes().isEmpty()) {
+                response.put("plan", JSON.parse(task.getWaypointTypes()));
+            } else {
+                response.put("plan", new java.util.ArrayList<>());
+            }
+            
+            // 解析目标点动作
+            if (task.getWaypointActions() != null && !task.getWaypointActions().isEmpty()) {
+                response.put("action", JSON.parse(task.getWaypointActions()));
+            } else {
+                response.put("action", new java.util.ArrayList<>());
+            }
+            
+            // 解析目标点ID
+            if (task.getWaypointIds() != null && !task.getWaypointIds().isEmpty()) {
+                response.put("waypointIds", JSON.parse(task.getWaypointIds()));
+            } else {
+                response.put("waypointIds", new java.util.ArrayList<>());
+            }
+            
+            response.put("lastExecuteTime", task.getLastExecuteTime());
+            response.put("lastExecuteStatus", task.getLastExecuteStatus());
+            response.put("nextExecuteTime", task.getNextExecuteTime());
+            response.put("totalExecuteCount", task.getTotalExecuteCount());
+            
+            return ResponseEntity.ok(response);
+        } catch (Exception e) {
+            logger.error("获取任务详情异常", e);
+            return buildErrorResponse(4, "内部错误: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 1.3 创建任务
+     * POST /v1/path
+     */
+    @ApiOperation("创建任务")
+    @PostMapping("/path")
+    public ResponseEntity<Map<String, Object>> createTask(@RequestBody JSONObject taskData) {
+        try {
+            String mapName = taskData.getString("map");
+            String pathName = taskData.getString("path");
+            
+            if (mapName == null || mapName.isEmpty() || pathName == null || pathName.isEmpty()) {
+                return buildErrorResponse(1, "请求参数不合法:缺少map或path参数");
+            }
+            
+            // 检查任务是否已存在
+            LdTask existingTask = ldTaskService.selectLdTaskByMapAndName(mapName, pathName);
+            if (existingTask != null) {
+                return buildErrorResponse(2, "任务已存在");
+            }
+            
+            // 构建任务对象
+            LdTask task = new LdTask();
+            task.setMapName(mapName);
+            task.setTaskName(pathName);
+            task.setTaskDesc(taskData.getString("desc"));
+            task.setCronExpression(taskData.getString("cron"));
+            task.setExecuteTime(taskData.getString("executeTime"));
+            task.setExecuteWeekdays(taskData.getString("executeWeekdays"));
+            task.setRepeatCount(taskData.getInteger("repeate") != null ? taskData.getInteger("repeate") : 1);
+            task.setCoordType(taskData.getString("coord_type") != null ? taskData.getString("coord_type") : "local");
+            task.setStatus("idle");
+            task.setTotalExecuteCount(0);
+            
+            // 保存目标点坐标
+            Object coordObj = taskData.get("coord");
+            if (coordObj != null) {
+                task.setWaypointCoords(JSON.toJSONString(coordObj));
+            }
+            
+            // 保存目标点类型
+            Object planObj = taskData.get("plan");
+            if (planObj != null) {
+                task.setWaypointTypes(JSON.toJSONString(planObj));
+            }
+            
+            // 保存目标点动作
+            Object actionObj = taskData.get("action");
+            if (actionObj != null) {
+                task.setWaypointActions(JSON.toJSONString(actionObj));
+            }
+            
+            // 保存目标点ID
+            Object waypointIdsObj = taskData.get("waypointIds");
+            if (waypointIdsObj != null) {
+                task.setWaypointIds(JSON.toJSONString(waypointIdsObj));
+            }
+            
+            // 计算下次执行时间
+            if (task.getCronExpression() != null && !task.getCronExpression().isEmpty()) {
+                task.setNextExecuteTime(calculateNextExecuteTime(task.getCronExpression()));
+            }
+            
+            int result = ldTaskService.insertLdTask(task);
+            
+            Map<String, Object> response = new HashMap<>();
+            if (result > 0) {
+                response.put("status", true);
+                response.put("taskId", task.getId());
+                response.put("msg", "任务创建成功");
+            } else {
+                response.put("status", false);
+                response.put("msg", "任务创建失败");
+            }
+            
+            return ResponseEntity.ok(response);
+        } catch (Exception e) {
+            logger.error("创建任务异常", e);
+            return buildErrorResponse(4, "内部错误: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 1.4 更新任务
+     * PUT /v1/path
+     */
+    @ApiOperation("更新任务")
+    @PutMapping("/path")
+    public ResponseEntity<Map<String, Object>> updateTask(@RequestBody JSONObject taskData) {
+        try {
+            Long taskId = taskData.getLong("taskId");
+            String mapName = taskData.getString("map");
+            String pathName = taskData.getString("path");
+            
+            if (taskId == null && (mapName == null || mapName.isEmpty() || pathName == null || pathName.isEmpty())) {
+                return buildErrorResponse(1, "请求参数不合法:缺少taskId或map/path参数");
+            }
+            
+            LdTask task;
+            if (taskId != null) {
+                task = ldTaskService.selectLdTaskById(taskId);
+            } else {
+                task = ldTaskService.selectLdTaskByMapAndName(mapName, pathName);
+            }
+            
+            if (task == null) {
+                return buildErrorResponse(2, "任务不存在");
+            }
+            
+            // 更新字段
+            if (taskData.containsKey("taskName")) {
+                String newTaskName = taskData.getString("taskName");
+                if (!newTaskName.equals(task.getTaskName())) {
+                    // 检查新名称是否已存在
+                    LdTask existingTask = ldTaskService.selectLdTaskByMapAndName(task.getMapName(), newTaskName);
+                    if (existingTask != null) {
+                        return buildErrorResponse(2, "任务名称已存在");
+                    }
+                    task.setTaskName(newTaskName);
+                }
+            }
+            
+            task.setTaskDesc(taskData.getString("desc"));
+            task.setCronExpression(taskData.getString("cron"));
+            task.setExecuteTime(taskData.getString("executeTime"));
+            task.setExecuteWeekdays(taskData.getString("executeWeekdays"));
+            
+            if (taskData.containsKey("repeate")) {
+                task.setRepeatCount(taskData.getInteger("repeate"));
+            }
+            
+            // 更新目标点数据
+            Object coordObj = taskData.get("coord");
+            if (coordObj != null) {
+                task.setWaypointCoords(JSON.toJSONString(coordObj));
+            }
+            
+            Object planObj = taskData.get("plan");
+            if (planObj != null) {
+                task.setWaypointTypes(JSON.toJSONString(planObj));
+            }
+            
+            Object actionObj = taskData.get("action");
+            if (actionObj != null) {
+                task.setWaypointActions(JSON.toJSONString(actionObj));
+            }
+            
+            Object waypointIdsObj = taskData.get("waypointIds");
+            if (waypointIdsObj != null) {
+                task.setWaypointIds(JSON.toJSONString(waypointIdsObj));
+            }
+            
+            // 重新计算下次执行时间
+            if (task.getCronExpression() != null && !task.getCronExpression().isEmpty()) {
+                task.setNextExecuteTime(calculateNextExecuteTime(task.getCronExpression()));
+            }
+            
+            int result = ldTaskService.updateLdTask(task);
+            
+            Map<String, Object> response = new HashMap<>();
+            response.put("status", result > 0);
+            response.put("msg", result > 0 ? "任务更新成功" : "任务更新失败");
+            
+            return ResponseEntity.ok(response);
+        } catch (Exception e) {
+            logger.error("更新任务异常", e);
+            return buildErrorResponse(4, "内部错误: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 1.5 删除任务
+     * DELETE /v1/path
+     */
+    @ApiOperation("删除任务")
+    @DeleteMapping("/path")
+    public ResponseEntity<Map<String, Object>> deleteTask(@RequestBody JSONObject params) {
+        try {
+            String mapName = params.getString("map");
+            String taskName = params.getString("path");
+            Long taskId = params.getLong("taskId");
+            
+            if (taskId == null && (mapName == null || taskName == null)) {
+                return buildErrorResponse(1, "请求参数不合法:缺少taskId或map/path参数");
+            }
+            
+            LdTask task;
+            if (taskId != null) {
+                task = ldTaskService.selectLdTaskById(taskId);
+            } else {
+                task = ldTaskService.selectLdTaskByMapAndName(mapName, taskName);
+            }
+            
+            if (task == null) {
+                return buildErrorResponse(2, "任务不存在");
+            }
+            
+            int result = ldTaskService.deleteLdTaskById(task.getId());
+            
+            Map<String, Object> response = new HashMap<>();
+            response.put("status", result > 0);
+            response.put("msg", result > 0 ? "任务删除成功" : "任务删除失败");
+            
+            return ResponseEntity.ok(response);
+        } catch (Exception e) {
+            logger.error("删除任务异常", e);
+            return buildErrorResponse(4, "内部错误: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 1.6 更新任务状态
+     * PUT /v1/path/status
+     */
+    @ApiOperation("更新任务状态")
+    @PutMapping("/path/status")
+    public ResponseEntity<Map<String, Object>> updateTaskStatus(
+            @ApiParam("任务ID") @RequestParam("taskId") Long taskId,
+            @ApiParam("新状态") @RequestParam("status") String status) {
+        try {
+            LdTask task = ldTaskService.selectLdTaskById(taskId);
+            if (task == null) {
+                return buildErrorResponse(2, "任务不存在");
+            }
+            
+            int result = ldTaskService.updateTaskStatus(taskId, status);
+            
+            Map<String, Object> response = new HashMap<>();
+            response.put("status", result > 0);
+            response.put("msg", result > 0 ? "状态更新成功" : "状态更新失败");
+            
+            return ResponseEntity.ok(response);
+        } catch (Exception e) {
+            logger.error("更新任务状态异常", e);
+            return buildErrorResponse(4, "内部错误: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 1.7 获取任务执行记录
+     * GET /v1/path/logs
+     */
+    @ApiOperation("获取任务执行记录")
+    @GetMapping("/path/logs")
+    public ResponseEntity<Map<String, Object>> getTaskLogs(
+            @ApiParam("任务ID") @RequestParam(value = "taskId", required = false) Long taskId,
+            @ApiParam("设备ID") @RequestParam(value = "deviceId", required = false) String deviceId,
+            @ApiParam("地图名称") @RequestParam(value = "map", required = false) String mapName) {
+        try {
+            LdTaskExecutionLog logQuery = new LdTaskExecutionLog();
+            if (taskId != null) {
+                logQuery.setTaskId(taskId);
+            }
+            if (deviceId != null) {
+                logQuery.setDeviceId(deviceId);
+            }
+            if (mapName != null) {
+                logQuery.setMapName(mapName);
+            }
+            
+            List<LdTaskExecutionLog> logs = ldTaskExecutionLogService.selectLdTaskExecutionLogList(logQuery);
+            
+            List<Map<String, Object>> logList = logs.stream().map(log -> {
+                Map<String, Object> logMap = new HashMap<>();
+                logMap.put("id", log.getId());
+                logMap.put("taskId", log.getTaskId());
+                logMap.put("taskName", log.getTaskName());
+                logMap.put("mapName", log.getMapName());
+                logMap.put("executeTime", log.getExecuteTime());
+                logMap.put("endTime", log.getEndTime());
+                logMap.put("status", log.getStatus());
+                logMap.put("startWaypoint", log.getStartWaypoint());
+                logMap.put("endWaypoint", log.getEndWaypoint());
+                logMap.put("completedWaypoints", log.getCompletedWaypoints());
+                logMap.put("totalWaypoints", log.getTotalWaypoints());
+                logMap.put("errorCode", log.getErrorCode());
+                logMap.put("errorMessage", log.getErrorMessage());
+                logMap.put("deviceId", log.getDeviceId());
+                return logMap;
+            }).collect(Collectors.toList());
+            
+            Map<String, Object> response = new HashMap<>();
+            response.put("status", true);
+            response.put("logs", logList);
+            response.put("total", logList.size());
+            
+            return ResponseEntity.ok(response);
+        } catch (Exception e) {
+            logger.error("获取任务执行记录异常", e);
+            return buildErrorResponse(4, "内部错误: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 1.8 获取设备今日执行记录
+     * GET /v1/path/logs/today
+     */
+    @ApiOperation("获取设备今日执行记录")
+    @GetMapping("/path/logs/today")
+    public ResponseEntity<Map<String, Object>> getTodayExecutionLogs(
+            @ApiParam("设备ID") @RequestParam("deviceId") String deviceId) {
+        try {
+            List<LdTaskExecutionLog> logs = ldTaskExecutionLogService.selectTodayExecutionLogsByDeviceId(deviceId);
+            
+            Map<String, Object> response = new HashMap<>();
+            response.put("status", true);
+            response.put("logs", logs);
+            response.put("total", logs.size());
+            
+            return ResponseEntity.ok(response);
+        } catch (Exception e) {
+            logger.error("获取今日执行记录异常", e);
+            return buildErrorResponse(4, "内部错误: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 辅助方法:根据Cron表达式计算下次执行时间
+     */
+    private java.util.Date calculateNextExecuteTime(String cronExpression) {
+        try {
+            CronTrigger cronTrigger = new CronTrigger(cronExpression);
+            SimpleTriggerContext triggerContext = new SimpleTriggerContext();
+
+            return cronTrigger.nextExecutionTime(triggerContext);
+        } catch (Exception e) {
+            logger.warn("解析Cron表达式失败: {}", cronExpression, e);
+            return null;
+        }
+    }
+
+    /**
+     * 构建错误响应
+     */
+    private ResponseEntity<Map<String, Object>> buildErrorResponse(int code, String msg) {
+        Map<String, Object> response = new HashMap<>();
+        response.put("status", false);
+        response.put("code", code);
+        response.put("msg", msg);
+        return ResponseEntity.ok(response);
+    }
+}

+ 417 - 0
ruoyi-admin/src/main/java/com/ruoyi/web/controller/robot/RobotMapController.java

@@ -0,0 +1,417 @@
+package com.ruoyi.web.controller.robot;
+
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONArray;
+import com.alibaba.fastjson2.JSONObject;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.redis.RedisCache;
+import com.ruoyi.mqtt.service.MqttSendService;
+import com.ruoyi.mqtt.util.MqttTopicUtil;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.*;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.client.RestTemplate;
+
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 地图管理Controller
+ * 提供地图列表、缩略图、路网数据、点云等API
+ * 数据源:LD导航系统后端(http://192.168.0.102:8086)
+ */
+@Api(tags = "地图管理")
+@RestController
+@RequestMapping("/robot/map")
+public class RobotMapController {
+
+    @Value("${ld-nav.url:http://192.168.0.102:8086}")
+    private String ldNavUrl;
+
+    @Autowired
+    private MqttSendService mqttSendService;
+
+    @Autowired
+    private RedisCache redisCache;
+
+    private final RestTemplate restTemplate = new RestTemplate();
+
+
+    // ==================== 导入导出API ====================
+
+    @ApiOperation("压缩导出地图")
+    @PostMapping("/export/compress")
+    public AjaxResult compressMapExport(@RequestBody Map<String, Object> params) {
+        try {
+            String url = ldNavUrl + "/v1/map/export/compress";
+            HttpHeaders headers = new HttpHeaders();
+            headers.setContentType(MediaType.APPLICATION_JSON);
+            HttpEntity<Map<String, Object>> request = new HttpEntity<>(params, headers);
+            
+            ResponseEntity<String> response = restTemplate.postForEntity(url, request, String.class);
+            
+            if (response.getStatusCode() == HttpStatus.OK) {
+                return AjaxResult.success("压缩成功", JSON.parse(response.getBody()));
+            }
+            return AjaxResult.error("压缩失败");
+        } catch (Exception e) {
+            return AjaxResult.error("压缩异常: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation("下载地图导出包")
+    @GetMapping("/export")
+    public ResponseEntity<byte[]> downloadMapExport(
+            @ApiParam("地图名称") @RequestParam("map") String mapName
+    ) {
+        try {
+            String url = ldNavUrl + "/v1/map/export?map=" + mapName;
+            ResponseEntity<byte[]> response = restTemplate.getForEntity(url, byte[].class);
+            
+            HttpHeaders headers = new HttpHeaders();
+            headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
+            headers.setContentDisposition(ContentDisposition.builder("attachment")
+                    .filename(mapName + ".zip")
+                    .build());
+            
+            return new ResponseEntity<>(response.getBody(), headers, HttpStatus.OK);
+        } catch (Exception e) {
+            return ResponseEntity.notFound().build();
+        }
+    }
+
+    @ApiOperation("导入地图")
+    @PostMapping("/import")
+    public AjaxResult importMap(
+            @ApiParam("地图文件") @RequestParam("file") org.springframework.web.multipart.MultipartFile file
+    ) {
+        try {
+            String url = ldNavUrl + "/v1/map/import";
+            
+            MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
+            body.add("filedata", file.getResource());
+            
+            HttpHeaders headers = new HttpHeaders();
+            headers.setContentType(MediaType.MULTIPART_FORM_DATA);
+            
+            HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers);
+            
+            ResponseEntity<String> response = restTemplate.postForEntity(url, requestEntity, String.class);
+            
+            if (response.getStatusCode() == HttpStatus.OK) {
+                return AjaxResult.success("导入成功", JSON.parse(response.getBody()));
+            }
+            return AjaxResult.error("导入失败");
+        } catch (Exception e) {
+            return AjaxResult.error("导入异常: " + e.getMessage());
+        }
+    }
+
+    // ==================== 路网数据API ====================
+
+    @ApiOperation("获取路网GeoJSON数据")
+    @GetMapping("/roadmap/geojson")
+    public AjaxResult getRoadMapGeoJson(
+            @ApiParam("地图名称") @RequestParam("map") String mapName
+    ) {
+        try {
+            String cacheKey = "ld:roadmap:" + mapName;
+            Object cached = redisCache.getCacheObject(cacheKey);
+            if (cached != null) {
+                return AjaxResult.success("获取成功(缓存)", cached);
+            }
+
+            String url = ldNavUrl + "/v1/roadmap/geojson?map=" + mapName;
+            ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
+            
+            if (response.getStatusCode() == HttpStatus.OK) {
+                JSONObject geojson = JSON.parseObject(response.getBody());
+                // 缓存10分钟
+                redisCache.setCacheObject(cacheKey, geojson, 10, TimeUnit.MINUTES);
+                return AjaxResult.success("获取成功", geojson);
+            }
+            return AjaxResult.error("获取路网数据失败");
+        } catch (Exception e) {
+            return AjaxResult.error("获取路网数据异常: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation("保存路网GeoJSON数据")
+    @PostMapping("/roadmap/geojson")
+    public AjaxResult saveRoadMapGeoJson(@RequestBody JSONObject geoJsonData) {
+        try {
+            String mapName = geoJsonData.getString("map");
+            if (mapName == null) {
+                return AjaxResult.error("缺少地图名称");
+            }
+
+            String url = ldNavUrl + "/v1/roadmap/geojson";
+            HttpHeaders headers = new HttpHeaders();
+            headers.setContentType(MediaType.APPLICATION_JSON);
+            HttpEntity<JSONObject> request = new HttpEntity<>(geoJsonData, headers);
+            
+            ResponseEntity<String> response = restTemplate.postForEntity(url, request, String.class);
+            
+            if (response.getStatusCode() == HttpStatus.OK) {
+                // 清除路网缓存
+                redisCache.deleteObject("ld:roadmap:" + mapName);
+                return AjaxResult.success("保存成功");
+            }
+            return AjaxResult.error("保存失败");
+        } catch (Exception e) {
+            return AjaxResult.error("保存异常: " + e.getMessage());
+        }
+    }
+
+    // ==================== 任务路线API ====================
+
+    @ApiOperation("获取任务路线列表")
+    @GetMapping("/path/list")
+    public AjaxResult listPath(
+            @ApiParam("地图名称") @RequestParam("map") String mapName
+    ) {
+        try {
+            String url = ldNavUrl + "/v1/path/list?map=" + mapName;
+            ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
+            
+            if (response.getStatusCode() == HttpStatus.OK) {
+                return AjaxResult.success("获取成功", JSON.parse(response.getBody()));
+            }
+            return AjaxResult.error("获取路线列表失败");
+        } catch (Exception e) {
+            return AjaxResult.error("获取路线列表异常: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation("获取任务路线详情")
+    @GetMapping("/path")
+    public AjaxResult getPath(
+            @ApiParam("地图名称") @RequestParam("map") String mapName,
+            @ApiParam("路线名称") @RequestParam("path") String pathName
+    ) {
+        try {
+            String url = ldNavUrl + "/v1/path?map=" + mapName + "&path=" + pathName;
+            ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
+            
+            if (response.getStatusCode() == HttpStatus.OK) {
+                return AjaxResult.success("获取成功", JSON.parse(response.getBody()));
+            }
+            return AjaxResult.error("获取路线详情失败");
+        } catch (Exception e) {
+            return AjaxResult.error("获取路线详情异常: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation("添加任务路线")
+    @PostMapping("/path")
+    public AjaxResult addPath(@RequestBody Map<String, Object> params) {
+        try {
+            String url = ldNavUrl + "/v1/path";
+            HttpHeaders headers = new HttpHeaders();
+            headers.setContentType(MediaType.APPLICATION_JSON);
+            HttpEntity<Map<String, Object>> request = new HttpEntity<>(params, headers);
+            
+            ResponseEntity<String> response = restTemplate.postForEntity(url, request, String.class);
+            
+            if (response.getStatusCode() == HttpStatus.OK) {
+                return AjaxResult.success("添加成功");
+            }
+            return AjaxResult.error("添加失败");
+        } catch (Exception e) {
+            return AjaxResult.error("添加异常: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation("删除任务路线")
+    @DeleteMapping("/path")
+    public AjaxResult deletePath(@RequestBody Map<String, String> params) {
+        try {
+            String map = params.get("map");
+            String path = params.get("path");
+            
+            String url = ldNavUrl + "/v1/path?map=" + map + "&path=" + path;
+            HttpHeaders headers = new HttpHeaders();
+            HttpEntity<String> request = new HttpEntity<>(headers);
+            
+            ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.DELETE, request, String.class);
+            
+            if (response.getStatusCode() == HttpStatus.OK) {
+                return AjaxResult.success("删除成功");
+            }
+            return AjaxResult.error("删除失败");
+        } catch (Exception e) {
+            return AjaxResult.error("删除异常: " + e.getMessage());
+        }
+    }
+
+    // ==================== 标定API ====================
+
+    @ApiOperation("获取标定历史")
+    @GetMapping("/calibration/history")
+    public AjaxResult getCalibrationHistory(
+            @ApiParam("地图名称") @RequestParam("map") String mapName
+    ) {
+        try {
+            String url = ldNavUrl + "/v1/map/file/" + mapName + "/shapefile/calibration.json";
+            ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
+            
+            if (response.getStatusCode() == HttpStatus.OK) {
+                return AjaxResult.success("获取成功", JSON.parse(response.getBody()));
+            }
+            return AjaxResult.error("获取标定历史失败");
+        } catch (Exception e) {
+            return AjaxResult.error("获取标定历史异常: " + e.getMessage());
+        }
+    }
+
+    // ==================== VSLAM API ====================
+
+    @ApiOperation("获取VSLAM统计信息")
+    @GetMapping("/vslam/statistics")
+    public AjaxResult getVSlamStatistics(
+            @ApiParam("地图名称") @RequestParam("map") String mapName
+    ) {
+        try {
+            String url = ldNavUrl + "/v1/vslam/statistics?map=" + mapName;
+            ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
+            
+            if (response.getStatusCode() == HttpStatus.OK) {
+                return AjaxResult.success("获取成功", JSON.parse(response.getBody()));
+            }
+            return AjaxResult.error("获取VSLAM统计信息失败");
+        } catch (Exception e) {
+            return AjaxResult.error("获取VSLAM统计信息异常: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation("获取关键帧点云")
+    @GetMapping("/vslam/keyframe/cloud")
+    public ResponseEntity<byte[]> getKeyframeCloud(
+            @ApiParam("地图名称") @RequestParam("map") String mapName,
+            @ApiParam("关键帧索引") @RequestParam("idx") int idx
+    ) {
+        try {
+            String url = ldNavUrl + "/v1/vslam/keyframe/cloud?map=" + mapName + "&idx=" + idx;
+            ResponseEntity<byte[]> response = restTemplate.getForEntity(url, byte[].class);
+            return ResponseEntity.ok()
+                    .headers(headers -> headers.setContentType(MediaType.APPLICATION_OCTET_STREAM))
+                    .body(response.getBody());
+        } catch (Exception e) {
+            return ResponseEntity.notFound().build();
+        }
+    }
+
+    @ApiOperation("获取关键帧变换矩阵")
+    @GetMapping("/vslam/keyframe/trans")
+    public ResponseEntity<byte[]> getKeyframeTrans(
+            @ApiParam("地图名称") @RequestParam("map") String mapName,
+            @ApiParam("关键帧索引") @RequestParam("idx") int idx
+    ) {
+        try {
+            String url = ldNavUrl + "/v1/vslam/keyframe/trans?map=" + mapName + "&idx=" + idx;
+            ResponseEntity<byte[]> response = restTemplate.getForEntity(url, byte[].class);
+            return ResponseEntity.ok()
+                    .headers(headers -> headers.setContentType(MediaType.APPLICATION_OCTET_STREAM))
+                    .body(response.getBody());
+        } catch (Exception e) {
+            return ResponseEntity.notFound().build();
+        }
+    }
+
+    @ApiOperation("获取闭环详情")
+    @GetMapping("/vslam/closure/details")
+    public AjaxResult getClosureDetails(
+            @ApiParam("地图名称") @RequestParam("map") String mapName,
+            @ApiParam("闭环索引") @RequestParam("idx") int idx
+    ) {
+        try {
+            String url = ldNavUrl + "/v1/vslam/closure/details?map=" + mapName + "&idx=" + idx;
+            ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
+            
+            if (response.getStatusCode() == HttpStatus.OK) {
+                return AjaxResult.success("获取成功", JSON.parse(response.getBody()));
+            }
+            return AjaxResult.error("获取闭环详情失败");
+        } catch (Exception e) {
+            return AjaxResult.error("获取闭环详情异常: " + e.getMessage());
+        }
+    }
+
+    // ==================== 传感器API ====================
+
+    @ApiOperation("获取点云数据")
+    @GetMapping("/sensor/pointcloud")
+    public ResponseEntity<byte[]> getPointcloud() {
+        try {
+            String url = ldNavUrl + "/v1/sensor/pointcloud/3d";
+            ResponseEntity<byte[]> response = restTemplate.getForEntity(url, byte[].class);
+            return ResponseEntity.ok()
+                    .headers(headers -> headers.setContentType(MediaType.APPLICATION_OCTET_STREAM))
+                    .body(response.getBody());
+        } catch (Exception e) {
+            return ResponseEntity.notFound().build();
+        }
+    }
+
+    // ==================== 地图瓦片API ====================
+
+    @ApiOperation("获取地图配置信息")
+    @GetMapping("/tilemap/details")
+    public AjaxResult getTilemapDetails(
+            @ApiParam("地图名称") @RequestParam("map") String mapName
+    ) {
+        try {
+            String url = ldNavUrl + "/v1/tilemap/details?map=" + mapName;
+            ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
+            
+            if (response.getStatusCode() == HttpStatus.OK) {
+                return AjaxResult.success("获取成功", JSON.parse(response.getBody()));
+            }
+            return AjaxResult.error("获取地图配置失败");
+        } catch (Exception e) {
+            return AjaxResult.error("获取地图配置异常: " + e.getMessage());
+        }
+    }
+
+    // ==================== 设备状态API ====================
+
+    @ApiOperation("获取机器人实时位姿(从Redis缓存)")
+    @GetMapping("/robot/pose")
+    public AjaxResult getRobotPose(
+            @ApiParam("设备ID") @RequestParam(required = false) String deviceId
+    ) {
+        try {
+            String cacheKey = "ld:pose:" + (deviceId != null ? deviceId : "default");
+            Object cached = redisCache.getCacheObject(cacheKey);
+            if (cached != null) {
+                return AjaxResult.success("获取成功(实时)", cached);
+            }
+            return AjaxResult.error("未获取到位姿数据");
+        } catch (Exception e) {
+            return AjaxResult.error("获取位姿异常: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation("获取任务实时信息(从Redis缓存)")
+    @GetMapping("/robot/task")
+    public AjaxResult getTaskRealtimeInfo(
+            @ApiParam("设备ID") @RequestParam(required = false) String deviceId
+    ) {
+        try {
+            String cacheKey = "ld:task:realtime:" + (deviceId != null ? deviceId : "default");
+            Object cached = redisCache.getCacheObject(cacheKey);
+            if (cached != null) {
+                return AjaxResult.success("获取成功(实时)", cached);
+            }
+            return AjaxResult.error("未获取到任务数据");
+        } catch (Exception e) {
+            return AjaxResult.error("获取任务信息异常: " + e.getMessage());
+        }
+    }
+}

+ 41 - 0
ruoyi-admin/src/main/java/com/ruoyi/websocket/config/WebSocketConfig.java

@@ -0,0 +1,41 @@
+package com.ruoyi.websocket.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.messaging.simp.config.MessageBrokerRegistry;
+import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
+import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
+import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
+
+/**
+ * WebSocket配置类
+ * 用于实时推送机器人状态数据到前端
+ */
+@Configuration
+@EnableWebSocketMessageBroker
+public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
+
+    @Override
+    public void configureMessageBroker(MessageBrokerRegistry config) {
+        // 配置消息代理前缀
+        // /topic 用于广播消息(服务器主动推送)
+        // /queue 用于点对点消息
+        config.enableSimpleBroker("/topic", "/queue");
+        // 应用目标前缀(客户端发送消息时使用)
+        config.setApplicationDestinationPrefixes("/app");
+        // 用户目标前缀(用于点对点消息)
+        config.setUserDestinationPrefix("/user");
+    }
+
+    @Override
+    public void registerStompEndpoints(StompEndpointRegistry registry) {
+        // 注册STOMP端点,客户端通过该端点连接WebSocket
+        // withSockJS() 启用SockJS备用方案(浏览器不支持WebSocket时自动降级)
+        registry.addEndpoint("/ws/robot")
+                .setAllowedOriginPatterns("*")
+                .withSockJS();
+        
+        // 也支持原生WebSocket连接(无降级)
+        registry.addEndpoint("/ws/robot")
+                .setAllowedOriginPatterns("*");
+    }
+}

+ 107 - 0
ruoyi-admin/src/main/java/com/ruoyi/websocket/controller/WebSocketController.java

@@ -0,0 +1,107 @@
+package com.ruoyi.websocket.controller;
+
+import com.ruoyi.common.core.domain.AjaxResult;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import org.springframework.messaging.handler.annotation.MessageMapping;
+import org.springframework.messaging.handler.annotation.Payload;
+import org.springframework.messaging.handler.annotation.SendTo;
+import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
+import org.springframework.web.bind.annotation.*;
+import com.alibaba.fastjson2.JSONObject;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * WebSocket控制器
+ * 处理前端通过WebSocket发送的消息(如订阅设备、发送指令等)
+ */
+@Api(tags = "WebSocket机器人控制")
+@RestController
+@RequestMapping("/ws/robot")
+public class WebSocketController {
+
+    /**
+     * 记录当前连接的设备ID(设备ID -> SessionID)
+     */
+    private final Map<String, String> deviceSessions = new ConcurrentHashMap<>();
+
+    /**
+     * 测试WebSocket连接是否正常
+     */
+    @ApiOperation("测试WebSocket连接")
+    @GetMapping("/test")
+    public AjaxResult test() {
+        return AjaxResult.success("WebSocket连接正常", null);
+    }
+
+    /**
+     * 客户端通过WebSocket发送消息(STOMP协议)
+     * 客户端发送格式:/app/message
+     * { "deviceId": "ld000001", "action": "subscribe", "data": {} }
+     */
+    @ApiOperation("处理WebSocket消息")
+    @MessageMapping("/message")
+    @SendTo("/topic/robot/response")
+    public Map<String, Object> handleMessage(
+            @Payload Map<String, Object> message,
+            SimpMessageHeaderAccessor headerAccessor
+    ) {
+        Map<String, Object> response = new HashMap<>();
+        String deviceId = (String) message.get("deviceId");
+        String action = (String) message.get("action");
+        
+        response.put("success", true);
+        response.put("deviceId", deviceId);
+        response.put("action", action);
+        response.put("timestamp", System.currentTimeMillis());
+        
+        // 记录设备会话
+        if (deviceId != null && headerAccessor.getSessionId() != null) {
+            deviceSessions.put(deviceId, headerAccessor.getSessionId());
+            response.put("message", "设备 " + deviceId + " 已注册");
+        }
+        
+        return response;
+    }
+
+    /**
+     * 注册设备(HTTP接口,用于前端主动注册)
+     */
+    @ApiOperation("注册设备连接")
+    @PostMapping("/register")
+    public AjaxResult registerDevice(
+            @ApiParam("设备ID") @RequestParam String deviceId,
+            @ApiParam("会话ID") @RequestParam(required = false) String sessionId
+    ) {
+        if (sessionId != null) {
+            deviceSessions.put(deviceId, sessionId);
+        }
+        return AjaxResult.success("设备注册成功", deviceId);
+    }
+
+    /**
+     * 获取当前在线的设备列表
+     */
+    @ApiOperation("获取在线设备列表")
+    @GetMapping("/online")
+    public AjaxResult getOnlineDevices() {
+        return AjaxResult.success(deviceSessions.keySet());
+    }
+
+    /**
+     * 心跳检测
+     */
+    @ApiOperation("心跳检测")
+    @PostMapping("/ping")
+    public AjaxResult ping(@ApiParam("设备ID") @RequestParam String deviceId) {
+        Map<String, Object> data = new HashMap<>();
+        data.put("deviceId", deviceId);
+        data.put("status", "online");
+        data.put("timestamp", System.currentTimeMillis());
+        return AjaxResult.success("心跳正常", data);
+    }
+}

+ 384 - 0
ruoyi-admin/src/main/java/com/ruoyi/websocket/service/WebSocketPushService.java

@@ -0,0 +1,384 @@
+package com.ruoyi.websocket.service;
+
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONArray;
+import com.alibaba.fastjson2.JSONObject;
+import com.ruoyi.mqtt.constant.MqttTopicConstants;
+import com.ruoyi.mqtt.util.MqttTopicUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.messaging.simp.SimpMessagingTemplate;
+import org.springframework.stereotype.Service;
+
+/**
+ * WebSocket消息推送服务
+ * 将MQTT接收到的机器人数据实时推送给WebSocket客户端
+ */
+@Service
+public class WebSocketPushService {
+
+    private static final Logger log = LoggerFactory.getLogger(WebSocketPushService.class);
+
+    @Autowired
+    private SimpMessagingTemplate messagingTemplate;
+
+    /**
+     * 推送机器人位姿数据到WebSocket
+     * @param deviceId 设备ID
+     * @param payload MQTT消息体
+     */
+    public void pushRobotPose(String deviceId, String payload) {
+        try {
+            JSONObject json = JSON.parseObject(payload);
+            JSONArray argsArray = json.getJSONArray("args");
+            if (argsArray != null && !argsArray.isEmpty()) {
+                JSONObject poseData = argsArray.getJSONObject(0);
+                JSONObject pose = poseData.getJSONObject("pose");
+                JSONObject vel = poseData.getJSONObject("vel");
+                JSONObject rtk = poseData.getJSONObject("rtk");
+                
+                JSONObject pushData = new JSONObject();
+                pushData.put("deviceId", deviceId);
+                pushData.put("type", "pose");
+                pushData.put("timestamp", System.currentTimeMillis());
+                
+                JSONObject data = new JSONObject();
+                if (pose != null) {
+                    data.put("xyz", pose.getJSONArray("xyz"));
+                    data.put("rpy", pose.getJSONArray("rpy"));
+                    data.put("heading", pose.getDouble("heading"));
+                }
+                if (vel != null) {
+                    data.put("velocity", vel.getDouble("enu"));
+                    data.put("heading", vel.getDouble("heading"));
+                }
+                data.put("confidence", poseData.getDouble("score"));
+                data.put("runState", poseData.getInteger("runState"));
+                data.put("odometer", poseData.getDouble("odometer"));
+                data.put("replan", poseData.getDouble("replan"));
+
+                if (rtk != null) {
+                    JSONObject rtkInfo = new JSONObject();
+                    rtkInfo.put("star", rtk.getInteger("star"));
+                    rtkInfo.put("status", rtk.getInteger("status"));
+                    data.put("rtk", rtkInfo);
+                }
+                
+                pushData.put("data", data);
+                
+                // 推送到设备专属频道
+                messagingTemplate.convertAndSend("/topic/robot/" + deviceId + "/pose", pushData);
+                // 也推送到通用频道
+                messagingTemplate.convertAndSend("/topic/robot/all/pose", pushData);
+            }
+        } catch (Exception e) {
+            log.error("推送机器人位姿数据失败", e);
+        }
+    }
+
+    /**
+     * 推送任务实时信息
+     * @param deviceId 设备ID
+     * @param payload MQTT消息体
+     */
+    public void pushTaskRealtimeInfo(String deviceId, String payload) {
+        try {
+            JSONObject json = JSON.parseObject(payload);
+            JSONArray argsArray = json.getJSONArray("args");
+            if (argsArray != null && !argsArray.isEmpty()) {
+                JSONObject taskInfo = argsArray.getJSONObject(0);
+                
+                JSONObject pushData = new JSONObject();
+                pushData.put("deviceId", deviceId);
+                pushData.put("type", "task_realtime");
+                pushData.put("timestamp", System.currentTimeMillis());
+                pushData.put("data", taskInfo);
+                
+                messagingTemplate.convertAndSend("/topic/robot/" + deviceId + "/task", pushData);
+            }
+        } catch (Exception e) {
+            log.error("推送任务实时信息失败", e);
+        }
+    }
+
+    /**
+     * 推送轨迹数据
+     * @param deviceId 设备ID
+     * @param shortTopic 主题类型(trajectory 或 trajectory/2d/compact)
+     * @param payload MQTT消息体
+     */
+    public void pushTrajectory(String deviceId, String shortTopic, String payload) {
+        try {
+            JSONObject pushData = new JSONObject();
+            pushData.put("deviceId", deviceId);
+            pushData.put("type", "trajectory");
+            pushData.put("subType", shortTopic);
+            pushData.put("timestamp", System.currentTimeMillis());
+            pushData.put("data", payload);
+            
+            messagingTemplate.convertAndSend("/topic/robot/" + deviceId + "/trajectory", pushData);
+        } catch (Exception e) {
+            log.error("推送轨迹数据失败", e);
+        }
+    }
+
+    /**
+     * 推送到达事件
+     * @param deviceId 设备ID
+     * @param payload MQTT消息体
+     */
+    public void pushArriveEvent(String deviceId, String payload) {
+        try {
+            JSONObject json = JSON.parseObject(payload);
+            JSONArray argsArray = json.getJSONArray("args");
+            if (argsArray != null && !argsArray.isEmpty()) {
+                JSONObject arriveInfo = argsArray.getJSONObject(0);
+                
+                JSONObject pushData = new JSONObject();
+                pushData.put("deviceId", deviceId);
+                pushData.put("type", "arrive");
+                pushData.put("timestamp", System.currentTimeMillis());
+                pushData.put("data", arriveInfo);
+                
+                messagingTemplate.convertAndSend("/topic/robot/" + deviceId + "/event", pushData);
+            }
+        } catch (Exception e) {
+            log.error("推送到达事件失败", e);
+        }
+    }
+
+    /**
+     * 推送地图列表响应
+     * @param deviceId 设备ID
+     * @param payload MQTT消息体
+     */
+    public void pushMapListResponse(String deviceId, String payload) {
+        try {
+            JSONObject pushData = new JSONObject();
+            pushData.put("deviceId", deviceId);
+            pushData.put("type", "map_list");
+            pushData.put("timestamp", System.currentTimeMillis());
+            pushData.put("data", payload);
+            
+            messagingTemplate.convertAndSend("/topic/robot/" + deviceId + "/map", pushData);
+        } catch (Exception e) {
+            log.error("推送地图列表响应失败", e);
+        }
+    }
+
+    /**
+     * 推送当前地图
+     * @param deviceId 设备ID
+     * @param payload MQTT消息体
+     */
+    public void pushCurrentMap(String deviceId, String payload) {
+        try {
+            JSONObject pushData = new JSONObject();
+            pushData.put("deviceId", deviceId);
+            pushData.put("type", "current_map");
+            pushData.put("timestamp", System.currentTimeMillis());
+            pushData.put("data", payload);
+            
+            messagingTemplate.convertAndSend("/topic/robot/" + deviceId + "/map", pushData);
+        } catch (Exception e) {
+            log.error("推送当前地图失败", e);
+        }
+    }
+
+    /**
+     * 推送导航操作响应
+     * @param deviceId 设备ID
+     * @param shortTopic 操作类型
+     * @param payload MQTT消息体
+     */
+    public void pushNavigationReply(String deviceId, String shortTopic, String payload) {
+        try {
+            JSONObject pushData = new JSONObject();
+            pushData.put("deviceId", deviceId);
+            pushData.put("type", "navigation_reply");
+            pushData.put("action", shortTopic);
+            pushData.put("timestamp", System.currentTimeMillis());
+            pushData.put("data", payload);
+            
+            messagingTemplate.convertAndSend("/topic/robot/" + deviceId + "/navigation", pushData);
+        } catch (Exception e) {
+            log.error("推送导航响应失败", e);
+        }
+    }
+
+    /**
+     * 推送任务控制响应
+     * @param deviceId 设备ID
+     * @param shortTopic 操作类型
+     * @param payload MQTT消息体
+     */
+    public void pushTaskProcedureReply(String deviceId, String shortTopic, String payload) {
+        try {
+            JSONObject pushData = new JSONObject();
+            pushData.put("deviceId", deviceId);
+            pushData.put("type", "task_procedure_reply");
+            pushData.put("action", shortTopic);
+            pushData.put("timestamp", System.currentTimeMillis());
+            pushData.put("data", payload);
+            
+            messagingTemplate.convertAndSend("/topic/robot/" + deviceId + "/task", pushData);
+        } catch (Exception e) {
+            log.error("推送任务控制响应失败", e);
+        }
+    }
+
+    /**
+     * 推送规划响应
+     * @param deviceId 设备ID
+     * @param shortTopic 主题
+     * @param payload MQTT消息体
+     */
+    public void pushPlanningResponse(String deviceId, String shortTopic, String payload) {
+        try {
+            JSONObject pushData = new JSONObject();
+            pushData.put("deviceId", deviceId);
+            pushData.put("type", "planning_reply");
+            pushData.put("action", shortTopic);
+            pushData.put("timestamp", System.currentTimeMillis());
+            pushData.put("data", payload);
+            
+            messagingTemplate.convertAndSend("/topic/robot/" + deviceId + "/planning", pushData);
+        } catch (Exception e) {
+            log.error("推送规划响应失败", e);
+        }
+    }
+
+    /**
+     * 推送原始MQTT消息(用于调试)
+     * @param deviceId 设备ID
+     * @param topic MQTT主题
+     * @param payload MQTT消息体
+     */
+    public void pushRawMessage(String deviceId, String topic, String payload) {
+        try {
+            JSONObject pushData = new JSONObject();
+            pushData.put("deviceId", deviceId);
+            pushData.put("topic", topic);
+            pushData.put("timestamp", System.currentTimeMillis());
+            pushData.put("data", payload);
+
+            messagingTemplate.convertAndSend("/topic/robot/" + deviceId + "/raw", pushData);
+        } catch (Exception e) {
+            log.error("推送原始消息失败", e);
+        }
+    }
+
+    /**
+     * 推送ASM远程调用响应
+     * @param deviceId 设备ID
+     * @param payload MQTT消息体
+     */
+    public void pushAsmReply(String deviceId, String payload) {
+        try {
+            JSONObject json = JSON.parseObject(payload);
+            JSONObject pushData = new JSONObject();
+            pushData.put("deviceId", deviceId);
+            pushData.put("type", "asm_reply");
+            pushData.put("function", json.getString("function"));
+            pushData.put("status", json.getString("status"));
+            pushData.put("pubTimestamp", json.getLong("pub_timestamp"));
+            pushData.put("timestamp", System.currentTimeMillis());
+            pushData.put("data", payload);
+
+            messagingTemplate.convertAndSend("/topic/robot/" + deviceId + "/asm", pushData);
+        } catch (Exception e) {
+            log.error("推送ASM响应失败", e);
+        }
+    }
+
+    /**
+     * 推送ASM远程调用进度反馈
+     * @param deviceId 设备ID
+     * @param payload MQTT消息体
+     */
+    public void pushAsmProgress(String deviceId, String payload) {
+        try {
+            JSONObject json = JSON.parseObject(payload);
+            JSONArray argsArray = json.getJSONArray("args");
+            if (argsArray != null && !argsArray.isEmpty()) {
+                JSONObject argObj = argsArray.getJSONObject(0);
+
+                JSONObject pushData = new JSONObject();
+                pushData.put("deviceId", deviceId);
+                pushData.put("type", "asm_progress");
+                pushData.put("function", argObj.getString("function"));
+                pushData.put("progress", argObj.get("progress"));
+                pushData.put("timestamp", System.currentTimeMillis());
+                pushData.put("data", payload);
+
+                messagingTemplate.convertAndSend("/topic/robot/" + deviceId + "/asm", pushData);
+            }
+        } catch (Exception e) {
+            log.error("推送ASM进度失败", e);
+        }
+    }
+
+    /**
+     * 推送ASM远程调用状态反馈
+     * @param deviceId 设备ID
+     * @param payload MQTT消息体
+     */
+    public void pushAsmState(String deviceId, String payload) {
+        try {
+            JSONObject json = JSON.parseObject(payload);
+            JSONArray argsArray = json.getJSONArray("args");
+            if (argsArray != null && !argsArray.isEmpty()) {
+                JSONObject argObj = argsArray.getJSONObject(0);
+
+                JSONObject pushData = new JSONObject();
+                pushData.put("deviceId", deviceId);
+                pushData.put("type", "asm_state");
+                pushData.put("function", argObj.getString("function"));
+                pushData.put("state", argObj.getInteger("state"));
+                pushData.put("timestamp", System.currentTimeMillis());
+                pushData.put("data", payload);
+
+                messagingTemplate.convertAndSend("/topic/robot/" + deviceId + "/asm", pushData);
+            }
+        } catch (Exception e) {
+            log.error("推送ASM状态失败", e);
+        }
+    }
+
+    /**
+     * 推送电池信息
+     * @param deviceId 设备ID
+     * @param payload MQTT消息体
+     */
+    public void pushBatteryInfo(String deviceId, String payload) {
+        try {
+            JSONObject json = JSON.parseObject(payload);
+            JSONArray argsArray = json.getJSONArray("args");
+            if (argsArray != null && !argsArray.isEmpty()) {
+                JSONObject batteryData = argsArray.getJSONObject(0);
+
+                JSONObject pushData = new JSONObject();
+                pushData.put("deviceId", deviceId);
+                pushData.put("type", "battery");
+                pushData.put("timestamp", System.currentTimeMillis());
+
+                // 提取电池信息
+                JSONObject battery = new JSONObject();
+                battery.put("capacity", batteryData.getDouble("capacity"));      // 电量百分比 0.0~1.0
+                battery.put("temperature", batteryData.getDouble("temperature")); // 温度
+                battery.put("voltage", batteryData.getDouble("voltage"));      // 电压
+                battery.put("current", batteryData.getDouble("current"));      // 电流
+                battery.put("charging", batteryData.getBoolean("charging"));   // 充电状态
+                battery.put("other", batteryData.getJSONArray("other"));      // 其他设备电量
+
+                pushData.put("data", battery);
+
+                messagingTemplate.convertAndSend("/topic/robot/" + deviceId + "/battery", pushData);
+                log.debug("电池信息已推送: {}", battery);
+            }
+        } catch (Exception e) {
+            log.error("推送电池信息失败", e);
+        }
+    }
+}

+ 32 - 4
ruoyi-admin/src/main/resources/application.yml

@@ -64,7 +64,7 @@ spring:
   devtools:
     restart:
       # 热部署开关
-      enabled: true
+      enabled: false
   # redis 配置
   redis:
     # 地址
@@ -144,9 +144,14 @@ mqtt:
   # EMQX 密码
   password: gbd2025!
   # 客户端ID
-  clientId: mqttx_1e1f7a7e
-  # 订阅主题(多个用逗号分隔)
-  subscribeTopics: device/+/report/job/create_result,device/+/report/job/status,log/+/event,device/+/cmd/jc/execute
+  clientId: mqttx_1e1f7a7e-inbound
+  # 产品ID (LD导航系统主题前缀: /${productId}/${deviceId}/xxx)
+  productId: robot4inspection
+  # 设备ID (必填,用于构建MQTT主题: /${productId}/${deviceId}/xxx)
+  deviceId: ld000001
+  # 订阅主题(多个用逗号分隔,留空则使用LD导航系统默认订阅)
+  # LD导航系统会自动订阅: /${productId}/${deviceId}/localization/pose, /${productId}/${deviceId}/task/target/event/arrive 等
+  subscribeTopics: 
   # QoS 级别
   qos: 1
   # 心跳间隔(秒)
@@ -157,3 +162,26 @@ mqtt:
   automaticReconnect: true
   # 清除会话
   cleanSession: false
+
+# LD导航系统 HTTP API 地址
+ld-nav:
+  # LD导航系统后端地址(用于获取地图、路网等数据)
+  url: http://192.168.0.30
+
+# 瓦片地图配置
+tilemap:
+  # 本地瓦片存储路径(服务器上存放瓦片文件的目录)
+  local-path: D:/ruoyi/tilemap
+  # 瓦片访问基础URL(用于构建瓦片URL,可配置为Nginx访问地址)
+  base-url: http://192.168.0.30
+
+# WebSocket跨域配置
+websocket:
+  # 允许的源(前端地址)
+  allowedOrigins: "*"
+
+# CORS跨域配置
+cors:
+  allowed-origins: "*"
+  allowed-methods: GET,POST,PUT,DELETE,OPTIONS
+  allowed-headers: "*"

+ 5 - 0
ruoyi-common/pom.xml

@@ -113,6 +113,11 @@
             <groupId>javax.servlet</groupId>
             <artifactId>javax.servlet-api</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <scope>provided</scope>
+        </dependency>
 
     </dependencies>
 

+ 97 - 0
ruoyi-common/src/main/java/com/ruoyi/common/core/domain/LdMapProject.java

@@ -0,0 +1,97 @@
+package com.ruoyi.robot.domain;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 地图工程对象 ld_map_project
+ */
+@Data
+public class LdMapProject {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键ID
+     */
+    private Long id;
+
+    /**
+     * 地图名称(唯一标识)
+     */
+    private String mapName;
+
+    /**
+     * 地图显示名称
+     */
+    private String displayName;
+
+    /**
+     * 地图状态(unavailable/recording/building/available)
+     */
+    private String status;
+
+    /**
+     * 缩略图路径
+     */
+    private String thumbnailPath;
+
+    /**
+     * 缩略图访问URL
+     */
+    private String thumbnailUrl;
+
+    /**
+     * 地图描述
+     */
+    private String description;
+
+    /**
+     * 地图文件大小(字节)
+     */
+    private Long fileSize;
+
+    /**
+     * 是否为当前使用地图(0否 1是)
+     */
+    private Integer isCurrent;
+
+    /**
+     * 所属设备ID
+     */
+    private String deviceId;
+
+    /**
+     * 数据来源(ld_nav/import)
+     */
+    private String source;
+
+    /**
+     * 创建者
+     */
+    private String createBy;
+
+    /**
+     * 创建时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+
+    /**
+     * 更新者
+     */
+    private String updateBy;
+
+    /**
+     * 更新时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date updateTime;
+
+    /**
+     * 备注
+     */
+    private String remark;
+}

+ 73 - 0
ruoyi-common/src/main/java/com/ruoyi/common/core/domain/LdRoadmap.java

@@ -0,0 +1,73 @@
+package com.ruoyi.robot.domain;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.Date;
+
+/**
+ * 路网数据对象 ld_roadmap
+ */
+@Data
+public class LdRoadmap{
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键ID
+     */
+    private Long id;
+
+    /**
+     * 所属地图名称
+     */
+    private String mapName;
+
+    /**
+     * 路网GeoJSON数据
+     */
+    private String geojsonData;
+
+    /**
+     * 版本号
+     */
+    private Integer version;
+
+    /**
+     * 节点数量
+     */
+    private Integer pointCount;
+
+    /**
+     * 线路数量
+     */
+    private Integer lineCount;
+
+    /**
+     * 是否有效
+     */
+    private Integer isValid;
+
+    /**
+     * 创建者
+     */
+    private String createBy;
+
+    /**
+     * 创建时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+
+    /**
+     * 更新者
+     */
+    private String updateBy;
+
+    /**
+     * 更新时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date updateTime;
+}

+ 128 - 0
ruoyi-common/src/main/java/com/ruoyi/common/core/domain/LdTilemapConfig.java

@@ -0,0 +1,128 @@
+package com.ruoyi.robot.domain;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 瓦片地图配置对象 ld_tilemap_config
+ */
+@Data
+public class LdTilemapConfig {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键ID
+     */
+    private Long id;
+
+    /**
+     * 所属地图名称
+     */
+    private String mapName;
+
+    /**
+     * 瓦片层级数
+     */
+    private Integer layerCnt;
+
+    /**
+     * 起始层级
+     */
+    private Integer minZoom;
+
+    /**
+     * 最大层级
+     */
+    private Integer maxZoom;
+
+    /**
+     * 瓦片尺寸(像素)
+     */
+    private Integer tileSize;
+
+    /**
+     * 投影类型
+     */
+    private String projectionType;
+
+    /**
+     * extent左边界
+     */
+    private BigDecimal extentLeft;
+
+    /**
+     * extent下边界
+     */
+    private BigDecimal extentBottom;
+
+    /**
+     * extent右边界
+     */
+    private BigDecimal extentRight;
+
+    /**
+     * extent上边界
+     */
+    private BigDecimal extentTop;
+
+    /**
+     * 瓦片存储根路径
+     */
+    private String tileStoragePath;
+
+    /**
+     * 瓦片访问基础URL
+     */
+    private String tileBaseUrl;
+
+    /**
+     * 原点X坐标
+     */
+    private BigDecimal originX;
+
+    /**
+     * 原点Y坐标
+     */
+    private BigDecimal originY;
+
+    /**
+     * 分辨率(米/像素)
+     */
+    private BigDecimal resolution;
+
+    /**
+     * 瓦片图片格式
+     */
+    private String format;
+
+    /**
+     * 原始配置JSON
+     */
+    private String configJson;
+
+    /**
+     * 创建者
+     */
+    private String createBy;
+
+    /**
+     * 创建时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+
+    /**
+     * 更新者
+     */
+    private String updateBy;
+
+    /**
+     * 更新时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date updateTime;
+}

+ 5 - 3
ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java

@@ -101,7 +101,7 @@ public class SecurityConfig
             .csrf(csrf -> csrf.disable())
             // 禁用HTTP响应标头
             .headers((headersCustomizer) -> {
-                headersCustomizer.cacheControl(cache -> cache.disable()).frameOptions(options -> options.sameOrigin());
+                headersCustomizer.cacheControl(cache -> cache.disable()).frameOptions(options -> options.disable());
             })
             // 认证失败处理类
             .exceptionHandling(exception -> exception.authenticationEntryPoint(unauthorizedHandler))
@@ -110,8 +110,10 @@ public class SecurityConfig
             // 注解标记允许匿名访问的url
             .authorizeHttpRequests((requests) -> {
                 permitAllUrl.getUrls().forEach(url -> requests.antMatchers(url).permitAll());
-                // 对于登录login 注册register 验证码captchaImage 允许匿名访问
-                requests.antMatchers("/login", "/register", "/captchaImage").permitAll()
+                // WebSocket/STOMP/SockJS端点放行
+                requests.antMatchers("/ws/**", "/ws/robot/**","/v1/**").permitAll()
+                    // 对于登录login 注册register 验证码captchaImage 允许匿名访问
+                    .antMatchers("/login", "/register", "/captchaImage").permitAll()
                     // 静态资源,可匿名访问
                     .antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll()
                     .antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll()

+ 308 - 0
ruoyi-system/src/main/java/com/ruoyi/robot/domain/LdTask.java

@@ -0,0 +1,308 @@
+package com.ruoyi.robot.domain;
+
+import java.util.Date;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+import com.ruoyi.common.annotation.Excel;
+import com.ruoyi.common.core.domain.BaseEntity;
+
+/**
+ * 任务对象 ld_task
+ * 
+ * @author ruoyi
+ */
+public class LdTask extends BaseEntity
+{
+    private static final long serialVersionUID = 1L;
+
+    /** 主键ID */
+    private Long id;
+
+    /** 所属地图名称 */
+    @Excel(name = "所属地图名称")
+    private String mapName;
+
+    /** 任务名称 */
+    @Excel(name = "任务名称")
+    private String taskName;
+
+    /** 任务描述 */
+    @Excel(name = "任务描述")
+    private String taskDesc;
+
+    /** Cron表达式 */
+    @Excel(name = "Cron表达式")
+    private String cronExpression;
+
+    /** 执行时间 */
+    @JsonFormat(pattern = "HH:mm:ss")
+    @Excel(name = "执行时间", width = 30, dateFormat = "HH:mm:ss")
+    private String executeTime;
+
+    /** 执行日期(逗号分隔的周几) */
+    @Excel(name = "执行日期")
+    private String executeWeekdays;
+
+    /** 重复次数 */
+    @Excel(name = "重复次数")
+    private Integer repeatCount;
+
+    /** 坐标类型 */
+    @Excel(name = "坐标类型")
+    private String coordType;
+
+    /** 目标点ID列表 */
+    private String waypointIds;
+
+    /** 目标点坐标列表 */
+    private String waypointCoords;
+
+    /** 目标点类型列表 */
+    private String waypointTypes;
+
+    /** 目标点动作列表 */
+    private String waypointActions;
+
+    /** 任务状态 */
+    @Excel(name = "任务状态", readConverterExp = "idle=空闲,running=运行中,paused=已暂停,completed=已完成,failed=失败")
+    private String status;
+
+    /** 上次执行时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "上次执行时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date lastExecuteTime;
+
+    /** 上次执行状态 */
+    @Excel(name = "上次执行状态")
+    private String lastExecuteStatus;
+
+    /** 下次执行时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "下次执行时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date nextExecuteTime;
+
+    /** 累计执行次数 */
+    @Excel(name = "累计执行次数")
+    private Integer totalExecuteCount;
+
+    /** 关联设备ID */
+    @Excel(name = "关联设备ID")
+    private String deviceId;
+
+    public void setId(Long id)
+    {
+        this.id = id;
+    }
+
+    public Long getId()
+    {
+        return id;
+    }
+
+    public void setMapName(String mapName)
+    {
+        this.mapName = mapName;
+    }
+
+    public String getMapName()
+    {
+        return mapName;
+    }
+
+    public void setTaskName(String taskName)
+    {
+        this.taskName = taskName;
+    }
+
+    public String getTaskName()
+    {
+        return taskName;
+    }
+
+    public void setTaskDesc(String taskDesc)
+    {
+        this.taskDesc = taskDesc;
+    }
+
+    public String getTaskDesc()
+    {
+        return taskDesc;
+    }
+
+    public void setCronExpression(String cronExpression)
+    {
+        this.cronExpression = cronExpression;
+    }
+
+    public String getCronExpression()
+    {
+        return cronExpression;
+    }
+
+    public void setExecuteTime(String executeTime)
+    {
+        this.executeTime = executeTime;
+    }
+
+    public String getExecuteTime()
+    {
+        return executeTime;
+    }
+
+    public void setExecuteWeekdays(String executeWeekdays)
+    {
+        this.executeWeekdays = executeWeekdays;
+    }
+
+    public String getExecuteWeekdays()
+    {
+        return executeWeekdays;
+    }
+
+    public void setRepeatCount(Integer repeatCount)
+    {
+        this.repeatCount = repeatCount;
+    }
+
+    public Integer getRepeatCount()
+    {
+        return repeatCount;
+    }
+
+    public void setCoordType(String coordType)
+    {
+        this.coordType = coordType;
+    }
+
+    public String getCoordType()
+    {
+        return coordType;
+    }
+
+    public void setWaypointIds(String waypointIds)
+    {
+        this.waypointIds = waypointIds;
+    }
+
+    public String getWaypointIds()
+    {
+        return waypointIds;
+    }
+
+    public void setWaypointCoords(String waypointCoords)
+    {
+        this.waypointCoords = waypointCoords;
+    }
+
+    public String getWaypointCoords()
+    {
+        return waypointCoords;
+    }
+
+    public void setWaypointTypes(String waypointTypes)
+    {
+        this.waypointTypes = waypointTypes;
+    }
+
+    public String getWaypointTypes()
+    {
+        return waypointTypes;
+    }
+
+    public void setWaypointActions(String waypointActions)
+    {
+        this.waypointActions = waypointActions;
+    }
+
+    public String getWaypointActions()
+    {
+        return waypointActions;
+    }
+
+    public void setStatus(String status)
+    {
+        this.status = status;
+    }
+
+    public String getStatus()
+    {
+        return status;
+    }
+
+    public void setLastExecuteTime(Date lastExecuteTime)
+    {
+        this.lastExecuteTime = lastExecuteTime;
+    }
+
+    public Date getLastExecuteTime()
+    {
+        return lastExecuteTime;
+    }
+
+    public void setLastExecuteStatus(String lastExecuteStatus)
+    {
+        this.lastExecuteStatus = lastExecuteStatus;
+    }
+
+    public String getLastExecuteStatus()
+    {
+        return lastExecuteStatus;
+    }
+
+    public void setNextExecuteTime(Date nextExecuteTime)
+    {
+        this.nextExecuteTime = nextExecuteTime;
+    }
+
+    public Date getNextExecuteTime()
+    {
+        return nextExecuteTime;
+    }
+
+    public void setTotalExecuteCount(Integer totalExecuteCount)
+    {
+        this.totalExecuteCount = totalExecuteCount;
+    }
+
+    public Integer getTotalExecuteCount()
+    {
+        return totalExecuteCount;
+    }
+
+    public void setDeviceId(String deviceId)
+    {
+        this.deviceId = deviceId;
+    }
+
+    public String getDeviceId()
+    {
+        return deviceId;
+    }
+
+    @Override
+    public String toString() {
+        return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
+            .append("id", getId())
+            .append("mapName", getMapName())
+            .append("taskName", getTaskName())
+            .append("taskDesc", getTaskDesc())
+            .append("cronExpression", getCronExpression())
+            .append("executeTime", getExecuteTime())
+            .append("executeWeekdays", getExecuteWeekdays())
+            .append("repeatCount", getRepeatCount())
+            .append("coordType", getCoordType())
+            .append("status", getStatus())
+            .append("lastExecuteTime", getLastExecuteTime())
+            .append("lastExecuteStatus", getLastExecuteStatus())
+            .append("nextExecuteTime", getNextExecuteTime())
+            .append("totalExecuteCount", getTotalExecuteCount())
+            .append("deviceId", getDeviceId())
+            .append("createBy", getCreateBy())
+            .append("createTime", getCreateTime())
+            .append("updateBy", getUpdateBy())
+            .append("updateTime", getUpdateTime())
+            .append("remark", getRemark())
+            .toString();
+    }
+}

+ 246 - 0
ruoyi-system/src/main/java/com/ruoyi/robot/domain/LdTaskExecutionLog.java

@@ -0,0 +1,246 @@
+package com.ruoyi.robot.domain;
+
+import java.util.Date;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.ruoyi.common.annotation.Excel;
+import com.ruoyi.common.core.domain.BaseEntity;
+
+/**
+ * 任务执行记录对象 ld_task_execution_log
+ * 
+ * @author ruoyi
+ */
+public class LdTaskExecutionLog extends BaseEntity
+{
+    private static final long serialVersionUID = 1L;
+
+    /** 主键ID */
+    private Long id;
+
+    /** 任务ID */
+    @Excel(name = "任务ID")
+    private Long taskId;
+
+    /** 任务名称 */
+    @Excel(name = "任务名称")
+    private String taskName;
+
+    /** 地图名称 */
+    @Excel(name = "地图名称")
+    private String mapName;
+
+    /** 执行时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "执行时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date executeTime;
+
+    /** 结束时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "结束时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date endTime;
+
+    /** 执行状态 */
+    @Excel(name = "执行状态", readConverterExp = "running=运行中,completed=完成,failed=失败,cancelled=取消")
+    private String status;
+
+    /** 起始目标点 */
+    @Excel(name = "起始目标点")
+    private String startWaypoint;
+
+    /** 结束目标点 */
+    @Excel(name = "结束目标点")
+    private String endWaypoint;
+
+    /** 已完成目标点数 */
+    @Excel(name = "已完成目标点数")
+    private Integer completedWaypoints;
+
+    /** 总目标点数 */
+    @Excel(name = "总目标点数")
+    private Integer totalWaypoints;
+
+    /** 错误码 */
+    @Excel(name = "错误码")
+    private String errorCode;
+
+    /** 错误信息 */
+    @Excel(name = "错误信息")
+    private String errorMessage;
+
+    /** 执行结果详情 */
+    private String executeResult;
+
+    /** 执行设备ID */
+    @Excel(name = "执行设备ID")
+    private String deviceId;
+
+    public void setId(Long id)
+    {
+        this.id = id;
+    }
+
+    public Long getId()
+    {
+        return id;
+    }
+
+    public void setTaskId(Long taskId)
+    {
+        this.taskId = taskId;
+    }
+
+    public Long getTaskId()
+    {
+        return taskId;
+    }
+
+    public void setTaskName(String taskName)
+    {
+        this.taskName = taskName;
+    }
+
+    public String getTaskName()
+    {
+        return taskName;
+    }
+
+    public void setMapName(String mapName)
+    {
+        this.mapName = mapName;
+    }
+
+    public String getMapName()
+    {
+        return mapName;
+    }
+
+    public void setExecuteTime(Date executeTime)
+    {
+        this.executeTime = executeTime;
+    }
+
+    public Date getExecuteTime()
+    {
+        return executeTime;
+    }
+
+    public void setEndTime(Date endTime)
+    {
+        this.endTime = endTime;
+    }
+
+    public Date getEndTime()
+    {
+        return endTime;
+    }
+
+    public void setStatus(String status)
+    {
+        this.status = status;
+    }
+
+    public String getStatus()
+    {
+        return status;
+    }
+
+    public void setStartWaypoint(String startWaypoint)
+    {
+        this.startWaypoint = startWaypoint;
+    }
+
+    public String getStartWaypoint()
+    {
+        return startWaypoint;
+    }
+
+    public void setEndWaypoint(String endWaypoint)
+    {
+        this.endWaypoint = endWaypoint;
+    }
+
+    public String getEndWaypoint()
+    {
+        return endWaypoint;
+    }
+
+    public void setCompletedWaypoints(Integer completedWaypoints)
+    {
+        this.completedWaypoints = completedWaypoints;
+    }
+
+    public Integer getCompletedWaypoints()
+    {
+        return completedWaypoints;
+    }
+
+    public void setTotalWaypoints(Integer totalWaypoints)
+    {
+        this.totalWaypoints = totalWaypoints;
+    }
+
+    public Integer getTotalWaypoints()
+    {
+        return totalWaypoints;
+    }
+
+    public void setErrorCode(String errorCode)
+    {
+        this.errorCode = errorCode;
+    }
+
+    public String getErrorCode()
+    {
+        return errorCode;
+    }
+
+    public void setErrorMessage(String errorMessage)
+    {
+        this.errorMessage = errorMessage;
+    }
+
+    public String getErrorMessage()
+    {
+        return errorMessage;
+    }
+
+    public void setExecuteResult(String executeResult)
+    {
+        this.executeResult = executeResult;
+    }
+
+    public String getExecuteResult()
+    {
+        return executeResult;
+    }
+
+    public void setDeviceId(String deviceId)
+    {
+        this.deviceId = deviceId;
+    }
+
+    public String getDeviceId()
+    {
+        return deviceId;
+    }
+
+    @Override
+    public String toString() {
+        return "LdTaskExecutionLog{" +
+            "id=" + id +
+            ", taskId=" + taskId +
+            ", taskName='" + taskName + '\'' +
+            ", mapName='" + mapName + '\'' +
+            ", executeTime=" + executeTime +
+            ", endTime=" + endTime +
+            ", status='" + status + '\'' +
+            ", startWaypoint='" + startWaypoint + '\'' +
+            ", endWaypoint='" + endWaypoint + '\'' +
+            ", completedWaypoints=" + completedWaypoints +
+            ", totalWaypoints=" + totalWaypoints +
+            ", errorCode='" + errorCode + '\'' +
+            ", errorMessage='" + errorMessage + '\'' +
+            ", deviceId='" + deviceId + '\'' +
+            '}';
+    }
+}

+ 48 - 0
ruoyi-system/src/main/java/com/ruoyi/robot/domain/vo/LdMapListVo.java

@@ -0,0 +1,48 @@
+package com.ruoyi.robot.domain.vo;
+
+
+import java.util.Objects;
+
+/**
+ * 地图列表VO(用于/v1/map/list接口返回)
+ */
+public class LdMapListVo {
+
+    /**
+     * 地图名称
+     */
+    private String mapName;
+
+    public String getStatus() {
+        return status;
+    }
+
+    public void setStatus(String status) {
+        this.status = status;
+    }
+
+    public String getMapName() {
+        return mapName;
+    }
+
+    public void setMapName(String mapName) {
+        this.mapName = mapName;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (o == null || getClass() != o.getClass()) return false;
+        LdMapListVo that = (LdMapListVo) o;
+        return Objects.equals(mapName, that.mapName) && Objects.equals(status, that.status);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mapName, status);
+    }
+
+    /**
+     * 地图状态
+     */
+    private String status;
+}

+ 46 - 0
ruoyi-system/src/main/java/com/ruoyi/robot/mapper/LdMapProjectMapper.java

@@ -0,0 +1,46 @@
+package com.ruoyi.robot.mapper;
+
+import com.ruoyi.robot.domain.LdMapProject;
+import com.ruoyi.robot.domain.vo.LdMapListVo;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 地图工程Mapper接口
+ */
+public interface LdMapProjectMapper{
+
+    /**
+     * 查询所有可用的地图列表(名称和状态)
+     */
+    List<LdMapListVo> selectMapListVo();
+
+    /**
+     * 根据设备ID查询当前使用的地图
+     */
+    LdMapProject selectCurrentMapByDeviceId(@Param("deviceId") String deviceId);
+
+    /**
+     * 根据地图名称查询
+     */
+    LdMapProject selectByMapName(@Param("mapName") String mapName);
+
+    int updateById(LdMapProject map);
+
+    /**
+     * 新增地图工程
+     *
+     * @param ldMapProject 地图工程
+     * @return 结果
+     */
+    public int insertLdMapProject(LdMapProject ldMapProject);
+
+    /**
+     * 修改地图工程
+     *
+     * @param ldMapProject 地图工程
+     * @return 结果
+     */
+    public int updateLdMapProject(LdMapProject ldMapProject);
+}

+ 19 - 0
ruoyi-system/src/main/java/com/ruoyi/robot/mapper/LdRoadmapMapper.java

@@ -0,0 +1,19 @@
+package com.ruoyi.robot.mapper;
+
+import com.ruoyi.robot.domain.LdRoadmap;
+import org.apache.ibatis.annotations.Param;
+
+/**
+ * 路网数据Mapper接口
+ */
+public interface LdRoadmapMapper{
+
+    /**
+     * 根据地图名称查询路网数据
+     */
+    LdRoadmap selectByMapName(@Param("mapName") String mapName);
+
+    int insert(LdRoadmap roadmap);
+
+    int updateById(LdRoadmap roadmap);
+}

+ 86 - 0
ruoyi-system/src/main/java/com/ruoyi/robot/mapper/LdTaskExecutionLogMapper.java

@@ -0,0 +1,86 @@
+package com.ruoyi.robot.mapper;
+
+import java.util.List;
+import com.ruoyi.robot.domain.LdTaskExecutionLog;
+import org.apache.ibatis.annotations.Param;
+
+/**
+ * 任务执行记录Mapper接口
+ * 
+ * @author ruoyi
+ */
+public interface LdTaskExecutionLogMapper
+{
+    /**
+     * 查询执行记录
+     * 
+     * @param id 执行记录主键
+     * @return 执行记录
+     */
+    public LdTaskExecutionLog selectLdTaskExecutionLogById(Long id);
+
+    /**
+     * 查询执行记录列表
+     * 
+     * @param log 执行记录
+     * @return 执行记录集合
+     */
+    public List<LdTaskExecutionLog> selectLdTaskExecutionLogList(LdTaskExecutionLog log);
+
+    /**
+     * 根据任务ID查询执行记录
+     * 
+     * @param taskId 任务ID
+     * @return 执行记录列表
+     */
+    public List<LdTaskExecutionLog> selectLdTaskExecutionLogByTaskId(Long taskId);
+
+    /**
+     * 查询某设备今日的执行记录
+     * 
+     * @param deviceId 设备ID
+     * @param executeDate 执行日期
+     * @return 执行记录列表
+     */
+    public List<LdTaskExecutionLog> selectTodayExecutionLogsByDeviceId(@Param("deviceId") String deviceId, @Param("executeDate") String executeDate);
+
+    /**
+     * 新增执行记录
+     * 
+     * @param log 执行记录
+     * @return 结果
+     */
+    public int insertLdTaskExecutionLog(LdTaskExecutionLog log);
+
+    /**
+     * 修改执行记录
+     * 
+     * @param log 执行记录
+     * @return 结果
+     */
+    public int updateLdTaskExecutionLog(LdTaskExecutionLog log);
+
+    /**
+     * 删除执行记录
+     * 
+     * @param id 执行记录主键
+     * @return 结果
+     */
+    public int deleteLdTaskExecutionLogById(Long id);
+
+    /**
+     * 批量删除执行记录
+     * 
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    public int deleteLdTaskExecutionLogByIds(Long[] ids);
+
+    /**
+     * 根据任务ID删除执行记录
+     * 
+     * @param taskId 任务ID
+     * @return 结果
+     */
+    public int deleteLdTaskExecutionLogByTaskId(Long taskId);
+}

+ 119 - 0
ruoyi-system/src/main/java/com/ruoyi/robot/mapper/LdTaskMapper.java

@@ -0,0 +1,119 @@
+package com.ruoyi.robot.mapper;
+
+import java.util.List;
+import com.ruoyi.robot.domain.LdTask;
+import org.apache.ibatis.annotations.Param;
+
+/**
+ * 任务Mapper接口
+ * 
+ * @author ruoyi
+ */
+public interface LdTaskMapper
+{
+    /**
+     * 查询任务
+     * 
+     * @param id 任务主键
+     * @return 任务
+     */
+    public LdTask selectLdTaskById(Long id);
+
+    /**
+     * 查询任务列表
+     * 
+     * @param ldTask 任务
+     * @return 任务集合
+     */
+    public List<LdTask> selectLdTaskList(LdTask ldTask);
+
+    /**
+     * 根据地图名称查询任务列表
+     * 
+     * @param mapName 地图名称
+     * @return 任务集合
+     */
+    public List<LdTask> selectLdTaskListByMapName(String mapName);
+
+    /**
+     * 根据地图名称和任务名称查询任务
+     * 
+     * @param mapName 地图名称
+     * @param taskName 任务名称
+     * @return 任务
+     */
+    public LdTask selectLdTaskByMapAndName(@Param("mapName") String mapName, @Param("taskName") String taskName);
+
+    /**
+     * 新增任务
+     * 
+     * @param ldTask 任务
+     * @return 结果
+     */
+    public int insertLdTask(LdTask ldTask);
+
+    /**
+     * 修改任务
+     * 
+     * @param ldTask 任务
+     * @return 结果
+     */
+    public int updateLdTask(LdTask ldTask);
+
+    /**
+     * 删除任务
+     * 
+     * @param id 任务主键
+     * @return 结果
+     */
+    public int deleteLdTaskById(Long id);
+
+    /**
+     * 批量删除任务
+     * 
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    public int deleteLdTaskByIds(Long[] ids);
+
+    /**
+     * 根据地图名称删除任务
+     * 
+     * @param mapName 地图名称
+     * @return 结果
+     */
+    public int deleteLdTaskByMapName(String mapName);
+
+    /**
+     * 更新任务状态
+     * 
+     * @param id 任务ID
+     * @param status 新状态
+     * @return 结果
+     */
+    public int updateTaskStatus(@Param("id") Long id, @Param("status") String status);
+
+    /**
+     * 查询待执行的任务(cron表达式不为空且状态为idle的任务)
+     * 
+     * @return 待执行任务列表
+     */
+    public List<LdTask> selectPendingScheduledTasks();
+
+    /**
+     * 根据设备ID查询正在运行的任务
+     * 
+     * @param deviceId 设备ID
+     * @return 正在运行的任务列表
+     */
+    public List<LdTask> selectRunningTasksByDeviceId(String deviceId);
+
+    /**
+     * 根据设备ID更新任务状态
+     * 
+     * @param deviceId 设备ID
+     * @param status 新状态
+     * @return 更新数量
+     */
+    public int updateTaskStatusByDeviceId(@Param("deviceId") String deviceId, @Param("status") String status);
+}

+ 15 - 0
ruoyi-system/src/main/java/com/ruoyi/robot/mapper/LdTilemapConfigMapper.java

@@ -0,0 +1,15 @@
+package com.ruoyi.robot.mapper;
+
+import com.ruoyi.robot.domain.LdTilemapConfig;
+import org.apache.ibatis.annotations.Param;
+
+/**
+ * 瓦片地图配置Mapper接口
+ */
+public interface LdTilemapConfigMapper{
+
+    /**
+     * 根据地图名称查询瓦片配置
+     */
+    LdTilemapConfig selectByMapName(@Param("mapName") String mapName);
+}

+ 53 - 0
ruoyi-system/src/main/java/com/ruoyi/robot/service/ILdMapProjectService.java

@@ -0,0 +1,53 @@
+package com.ruoyi.robot.service;
+
+import com.ruoyi.robot.domain.LdMapProject;
+import com.ruoyi.robot.domain.vo.LdMapListVo;
+
+import java.util.List;
+
+/**
+ * 地图工程Service接口
+ */
+public interface ILdMapProjectService {
+
+    /**
+     * 查询地图列表(名称和状态)
+     */
+    List<LdMapListVo> selectMapList();
+
+    /**
+     * 获取当前使用的地图
+     */
+    LdMapProject selectCurrentMap(String deviceId);
+
+    /**
+     * 根据地图名称查询地图详情
+     */
+    LdMapProject selectMapByName(String mapName);
+
+    /**
+     * 重命名地图
+     */
+    int renameMap(String oldName, String newName);
+
+    /**
+     * 获取地图缩略图URL
+     */
+    String getThumbnailUrl(String mapName);
+
+    /**
+     * 新增地图工程
+     *
+     * @param ldMapProject 地图工程
+     * @return 结果
+     */
+    int insertLdMapProject(LdMapProject ldMapProject);
+
+    /**
+     * 修改地图工程
+     *
+     * @param ldMapProject 地图工程
+     * @return 结果
+     */
+    public int updateLdMapProject(LdMapProject ldMapProject);
+}

+ 24 - 0
ruoyi-system/src/main/java/com/ruoyi/robot/service/ILdRoadmapService.java

@@ -0,0 +1,24 @@
+package com.ruoyi.robot.service;
+
+import com.ruoyi.robot.domain.LdRoadmap;
+
+/**
+ * 路网数据Service接口
+ */
+public interface ILdRoadmapService {
+
+    /**
+     * 根据地图名称查询路网数据
+     */
+    LdRoadmap selectRoadmapByMapName(String mapName);
+
+    /**
+     * 获取路网GeoJSON数据
+     */
+    String getGeoJsonByMapName(String mapName);
+
+    /**
+     * 保存路网GeoJSON数据
+     */
+    int saveRoadmap(String mapName, String geoJsonData);
+}

+ 84 - 0
ruoyi-system/src/main/java/com/ruoyi/robot/service/ILdTaskExecutionLogService.java

@@ -0,0 +1,84 @@
+package com.ruoyi.robot.service;
+
+import java.util.List;
+import com.ruoyi.robot.domain.LdTaskExecutionLog;
+
+/**
+ * 任务执行记录Service接口
+ * 
+ * @author ruoyi
+ */
+public interface ILdTaskExecutionLogService
+{
+    /**
+     * 查询执行记录
+     * 
+     * @param id 执行记录主键
+     * @return 执行记录
+     */
+    public LdTaskExecutionLog selectLdTaskExecutionLogById(Long id);
+
+    /**
+     * 查询执行记录列表
+     * 
+     * @param log 执行记录
+     * @return 执行记录集合
+     */
+    public List<LdTaskExecutionLog> selectLdTaskExecutionLogList(LdTaskExecutionLog log);
+
+    /**
+     * 根据任务ID查询执行记录
+     * 
+     * @param taskId 任务ID
+     * @return 执行记录列表
+     */
+    public List<LdTaskExecutionLog> selectLdTaskExecutionLogByTaskId(Long taskId);
+
+    /**
+     * 查询某设备今日的执行记录
+     * 
+     * @param deviceId 设备ID
+     * @return 执行记录列表
+     */
+    public List<LdTaskExecutionLog> selectTodayExecutionLogsByDeviceId(String deviceId);
+
+    /**
+     * 新增执行记录
+     * 
+     * @param log 执行记录
+     * @return 结果
+     */
+    public int insertLdTaskExecutionLog(LdTaskExecutionLog log);
+
+    /**
+     * 修改执行记录
+     * 
+     * @param log 执行记录
+     * @return 结果
+     */
+    public int updateLdTaskExecutionLog(LdTaskExecutionLog log);
+
+    /**
+     * 删除执行记录
+     * 
+     * @param id 执行记录主键
+     * @return 结果
+     */
+    public int deleteLdTaskExecutionLogById(Long id);
+
+    /**
+     * 批量删除执行记录
+     * 
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    public int deleteLdTaskExecutionLogByIds(Long[] ids);
+
+    /**
+     * 根据任务ID删除执行记录
+     * 
+     * @param taskId 任务ID
+     * @return 结果
+     */
+    public int deleteLdTaskExecutionLogByTaskId(Long taskId);
+}

+ 110 - 0
ruoyi-system/src/main/java/com/ruoyi/robot/service/ILdTaskService.java

@@ -0,0 +1,110 @@
+package com.ruoyi.robot.service;
+
+import java.util.List;
+import com.ruoyi.robot.domain.LdTask;
+
+/**
+ * 任务Service接口
+ * 
+ * @author ruoyi
+ */
+public interface ILdTaskService
+{
+    /**
+     * 查询任务
+     * 
+     * @param id 任务主键
+     * @return 任务
+     */
+    public LdTask selectLdTaskById(Long id);
+
+    /**
+     * 查询任务列表
+     * 
+     * @param ldTask 任务
+     * @return 任务集合
+     */
+    public List<LdTask> selectLdTaskList(LdTask ldTask);
+
+    /**
+     * 根据地图名称查询任务列表
+     * 
+     * @param mapName 地图名称
+     * @return 任务集合
+     */
+    public List<LdTask> selectLdTaskListByMapName(String mapName);
+
+    /**
+     * 根据地图名称和任务名称查询任务
+     * 
+     * @param mapName 地图名称
+     * @param taskName 任务名称
+     * @return 任务
+     */
+    public LdTask selectLdTaskByMapAndName(String mapName, String taskName);
+
+    /**
+     * 新增任务
+     * 
+     * @param ldTask 任务
+     * @return 结果
+     */
+    public int insertLdTask(LdTask ldTask);
+
+    /**
+     * 修改任务
+     * 
+     * @param ldTask 任务
+     * @return 结果
+     */
+    public int updateLdTask(LdTask ldTask);
+
+    /**
+     * 删除任务
+     * 
+     * @param id 任务主键
+     * @return 结果
+     */
+    public int deleteLdTaskById(Long id);
+
+    /**
+     * 批量删除任务
+     * 
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    public int deleteLdTaskByIds(Long[] ids);
+
+    /**
+     * 更新任务状态
+     * 
+     * @param id 任务ID
+     * @param status 新状态
+     * @return 结果
+     */
+    public int updateTaskStatus(Long id, String status);
+
+    /**
+     * 查询待执行的任务
+     *
+     * @return 待执行任务列表
+     */
+    public List<LdTask> selectPendingScheduledTasks();
+
+    /**
+     * 根据设备ID查询正在运行的任务
+     *
+     * @param deviceId 设备ID
+     * @return 正在运行的任务列表
+     */
+    public List<LdTask> selectRunningTasksByDeviceId(String deviceId);
+
+    /**
+     * 根据设备ID更新任务状态
+     *
+     * @param deviceId 设备ID
+     * @param status 新状态
+     * @return 更新数量
+     */
+    public int updateTaskStatusByDeviceId(String deviceId, String status);
+}

+ 24 - 0
ruoyi-system/src/main/java/com/ruoyi/robot/service/ILdTilemapConfigService.java

@@ -0,0 +1,24 @@
+package com.ruoyi.robot.service;
+
+import com.ruoyi.robot.domain.LdTilemapConfig;
+
+/**
+ * 瓦片地图配置Service接口
+ */
+public interface ILdTilemapConfigService {
+
+    /**
+     * 根据地图名称查询瓦片配置
+     */
+    LdTilemapConfig selectTilemapConfigByMapName(String mapName);
+
+    /**
+     * 获取瓦片文件路径
+     */
+    String getTilePath(String mapName, int zoom, int x, int y);
+
+    /**
+     * 获取瓦片文件URL
+     */
+    String getTileUrl(String mapName, int zoom, int x, int y);
+}

+ 110 - 0
ruoyi-system/src/main/java/com/ruoyi/robot/service/impl/LdMapProjectServiceImpl.java

@@ -0,0 +1,110 @@
+package com.ruoyi.robot.service.impl;
+
+import com.ruoyi.common.core.redis.RedisCache;
+import com.ruoyi.common.utils.DateUtils;
+import com.ruoyi.robot.domain.LdMapProject;
+import com.ruoyi.robot.domain.vo.LdMapListVo;
+import com.ruoyi.robot.mapper.LdMapProjectMapper;
+import com.ruoyi.robot.service.ILdMapProjectService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * 地图工程Service实现
+ */
+@Service
+public class LdMapProjectServiceImpl implements ILdMapProjectService {
+
+    @Autowired
+    private LdMapProjectMapper mapProjectMapper;
+
+    @Autowired
+    private RedisCache redisCache;
+
+    private static final String MAP_LIST_CACHE_KEY = "ld:map:list";
+    private static final String CURRENT_MAP_CACHE_KEY = "ld:map:current:";
+
+    @Override
+    public List<LdMapListVo> selectMapList() {
+        return mapProjectMapper.selectMapListVo();
+    }
+
+    @Override
+    public LdMapProject selectCurrentMap(String deviceId) {
+        String cacheKey = CURRENT_MAP_CACHE_KEY + (deviceId != null ? deviceId : "default");
+
+        LdMapProject cached = redisCache.getCacheObject(cacheKey);
+        if (cached != null) {
+            return cached;
+        }
+
+        LdMapProject map = mapProjectMapper.selectCurrentMapByDeviceId(deviceId);
+
+        if (map != null) {
+            redisCache.setCacheObject(cacheKey, map);
+        }
+
+        return map;
+    }
+
+    @Override
+    public LdMapProject selectMapByName(String mapName) {
+        return mapProjectMapper.selectByMapName(mapName);
+    }
+
+    @Override
+    public int renameMap(String oldName, String newName) {
+        LdMapProject map = mapProjectMapper.selectByMapName(oldName);
+        if (map == null) {
+            return 0;
+        }
+
+        map.setDisplayName(newName);
+        int result = mapProjectMapper.updateById(map);
+
+        // 清除缓存
+        if (result > 0) {
+            redisCache.deleteObject(MAP_LIST_CACHE_KEY);
+            redisCache.deleteObject(CURRENT_MAP_CACHE_KEY + (map.getDeviceId() != null ? map.getDeviceId() : "default"));
+        }
+
+        return result;
+    }
+
+    @Override
+    public String getThumbnailUrl(String mapName) {
+        LdMapProject map = selectMapByName(mapName);
+        if (map != null && map.getThumbnailUrl() != null) {
+            return map.getThumbnailUrl();
+        }
+        return null;
+    }
+
+    /**
+     * 新增地图工程
+     *
+     * @param ldMapProject 地图工程
+     * @return 结果
+     */
+    @Override
+    public int insertLdMapProject(LdMapProject ldMapProject)
+    {
+        ldMapProject.setCreateTime(DateUtils.getNowDate());
+        return mapProjectMapper.insertLdMapProject(ldMapProject);
+    }
+
+    /**
+     * 修改地图工程
+     *
+     * @param ldMapProject 地图工程
+     * @return 结果
+     */
+    @Override
+    public int updateLdMapProject(LdMapProject ldMapProject)
+    {
+        ldMapProject.setUpdateTime(DateUtils.getNowDate());
+        return mapProjectMapper.updateLdMapProject(ldMapProject);
+    }
+}

+ 76 - 0
ruoyi-system/src/main/java/com/ruoyi/robot/service/impl/LdRoadmapServiceImpl.java

@@ -0,0 +1,76 @@
+package com.ruoyi.robot.service.impl;
+
+import com.alibaba.fastjson2.JSON;
+import com.ruoyi.common.core.redis.RedisCache;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.robot.domain.LdRoadmap;
+import com.ruoyi.robot.mapper.LdRoadmapMapper;
+import com.ruoyi.robot.service.ILdRoadmapService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 路网数据Service实现
+ */
+@Service
+public class LdRoadmapServiceImpl implements ILdRoadmapService {
+
+    @Autowired
+    private LdRoadmapMapper roadmapMapper;
+
+    @Autowired
+    private RedisCache redisCache;
+
+    private static final String ROADMAP_CACHE_KEY = "ld:roadmap:geojson:";
+
+    @Override
+    public LdRoadmap selectRoadmapByMapName(String mapName) {
+        return roadmapMapper.selectByMapName(mapName);
+    }
+
+    @Override
+    public String getGeoJsonByMapName(String mapName) {
+        LdRoadmap roadmap = selectRoadmapByMapName(mapName);
+        if (roadmap != null && StringUtils.isNotBlank(roadmap.getGeojsonData())) {
+            return roadmap.getGeojsonData();
+        }
+        return null;
+    }
+
+    @Override
+    public int saveRoadmap(String mapName, String geoJsonData) {
+        LdRoadmap roadmap = roadmapMapper.selectByMapName(mapName);
+
+        if (roadmap == null) {
+            roadmap = new LdRoadmap();
+            roadmap.setMapName(mapName);
+            roadmap.setGeojsonData(geoJsonData);
+            roadmap.setVersion(1);
+            roadmap.setIsValid(1);
+            int result = roadmapMapper.insert(roadmap);
+
+            if (result > 0) {
+                redisCache.deleteObject(ROADMAP_CACHE_KEY + mapName);
+            }
+            return result;
+        } else {
+            Map<String, Object> saveData = new HashMap<>();
+            saveData.put("type","FeatureCollection");
+            saveData.put("features", JSON.parseArray(geoJsonData));
+            saveData.put("map", mapName);
+            // 转换为 JSON 字符串
+            roadmap.setGeojsonData(JSON.toJSONString(saveData));
+            roadmap.setVersion(roadmap.getVersion() + 1);
+            int result = roadmapMapper.updateById(roadmap);
+
+            if (result > 0) {
+                redisCache.deleteObject(ROADMAP_CACHE_KEY + mapName);
+            }
+            return result;
+        }
+    }
+}

+ 129 - 0
ruoyi-system/src/main/java/com/ruoyi/robot/service/impl/LdTaskExecutionLogServiceImpl.java

@@ -0,0 +1,129 @@
+package com.ruoyi.robot.service.impl;
+
+import java.util.List;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import com.ruoyi.robot.mapper.LdTaskExecutionLogMapper;
+import com.ruoyi.robot.domain.LdTaskExecutionLog;
+import com.ruoyi.robot.service.ILdTaskExecutionLogService;
+
+/**
+ * 任务执行记录Service接口实现
+ * 
+ * @author ruoyi
+ */
+@Service
+public class LdTaskExecutionLogServiceImpl implements ILdTaskExecutionLogService
+{
+    @Autowired
+    private LdTaskExecutionLogMapper ldTaskExecutionLogMapper;
+
+    /**
+     * 查询执行记录
+     * 
+     * @param id 执行记录主键
+     * @return 执行记录
+     */
+    @Override
+    public LdTaskExecutionLog selectLdTaskExecutionLogById(Long id)
+    {
+        return ldTaskExecutionLogMapper.selectLdTaskExecutionLogById(id);
+    }
+
+    /**
+     * 查询执行记录列表
+     * 
+     * @param log 执行记录
+     * @return 执行记录
+     */
+    @Override
+    public List<LdTaskExecutionLog> selectLdTaskExecutionLogList(LdTaskExecutionLog log)
+    {
+        return ldTaskExecutionLogMapper.selectLdTaskExecutionLogList(log);
+    }
+
+    /**
+     * 根据任务ID查询执行记录
+     * 
+     * @param taskId 任务ID
+     * @return 执行记录列表
+     */
+    @Override
+    public List<LdTaskExecutionLog> selectLdTaskExecutionLogByTaskId(Long taskId)
+    {
+        return ldTaskExecutionLogMapper.selectLdTaskExecutionLogByTaskId(taskId);
+    }
+
+    /**
+     * 查询某设备今日的执行记录
+     * 
+     * @param deviceId 设备ID
+     * @return 执行记录列表
+     */
+    @Override
+    public List<LdTaskExecutionLog> selectTodayExecutionLogsByDeviceId(String deviceId)
+    {
+        return ldTaskExecutionLogMapper.selectTodayExecutionLogsByDeviceId(deviceId, 
+            new java.text.SimpleDateFormat("yyyy-MM-dd").format(new java.util.Date()));
+    }
+
+    /**
+     * 新增执行记录
+     * 
+     * @param log 执行记录
+     * @return 结果
+     */
+    @Override
+    public int insertLdTaskExecutionLog(LdTaskExecutionLog log)
+    {
+        return ldTaskExecutionLogMapper.insertLdTaskExecutionLog(log);
+    }
+
+    /**
+     * 修改执行记录
+     * 
+     * @param log 执行记录
+     * @return 结果
+     */
+    @Override
+    public int updateLdTaskExecutionLog(LdTaskExecutionLog log)
+    {
+        return ldTaskExecutionLogMapper.updateLdTaskExecutionLog(log);
+    }
+
+    /**
+     * 删除执行记录
+     * 
+     * @param id 执行记录主键
+     * @return 结果
+     */
+    @Override
+    public int deleteLdTaskExecutionLogById(Long id)
+    {
+        return ldTaskExecutionLogMapper.deleteLdTaskExecutionLogById(id);
+    }
+
+    /**
+     * 批量删除执行记录
+     * 
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    @Override
+    public int deleteLdTaskExecutionLogByIds(Long[] ids)
+    {
+        return ldTaskExecutionLogMapper.deleteLdTaskExecutionLogByIds(ids);
+    }
+
+    /**
+     * 根据任务ID删除执行记录
+     * 
+     * @param taskId 任务ID
+     * @return 结果
+     */
+    @Override
+    public int deleteLdTaskExecutionLogByTaskId(Long taskId)
+    {
+        return ldTaskExecutionLogMapper.deleteLdTaskExecutionLogByTaskId(taskId);
+    }
+}

+ 166 - 0
ruoyi-system/src/main/java/com/ruoyi/robot/service/impl/LdTaskServiceImpl.java

@@ -0,0 +1,166 @@
+package com.ruoyi.robot.service.impl;
+
+import java.util.List;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import com.ruoyi.robot.mapper.LdTaskMapper;
+import com.ruoyi.robot.domain.LdTask;
+import com.ruoyi.robot.service.ILdTaskService;
+
+/**
+ * 任务Service接口实现
+ * 
+ * @author ruoyi
+ */
+@Service
+public class LdTaskServiceImpl implements ILdTaskService
+{
+    @Autowired
+    private LdTaskMapper ldTaskMapper;
+
+    /**
+     * 查询任务
+     * 
+     * @param id 任务主键
+     * @return 任务
+     */
+    @Override
+    public LdTask selectLdTaskById(Long id)
+    {
+        return ldTaskMapper.selectLdTaskById(id);
+    }
+
+    /**
+     * 查询任务列表
+     * 
+     * @param ldTask 任务
+     * @return 任务
+     */
+    @Override
+    public List<LdTask> selectLdTaskList(LdTask ldTask)
+    {
+        return ldTaskMapper.selectLdTaskList(ldTask);
+    }
+
+    /**
+     * 根据地图名称查询任务列表
+     * 
+     * @param mapName 地图名称
+     * @return 任务列表
+     */
+    @Override
+    public List<LdTask> selectLdTaskListByMapName(String mapName)
+    {
+        return ldTaskMapper.selectLdTaskListByMapName(mapName);
+    }
+
+    /**
+     * 根据地图名称和任务名称查询任务
+     * 
+     * @param mapName 地图名称
+     * @param taskName 任务名称
+     * @return 任务
+     */
+    @Override
+    public LdTask selectLdTaskByMapAndName(String mapName, String taskName)
+    {
+        return ldTaskMapper.selectLdTaskByMapAndName(mapName, taskName);
+    }
+
+    /**
+     * 新增任务
+     * 
+     * @param ldTask 任务
+     * @return 结果
+     */
+    @Override
+    public int insertLdTask(LdTask ldTask)
+    {
+        return ldTaskMapper.insertLdTask(ldTask);
+    }
+
+    /**
+     * 修改任务
+     * 
+     * @param ldTask 任务
+     * @return 结果
+     */
+    @Override
+    public int updateLdTask(LdTask ldTask)
+    {
+        return ldTaskMapper.updateLdTask(ldTask);
+    }
+
+    /**
+     * 删除任务
+     * 
+     * @param id 任务主键
+     * @return 结果
+     */
+    @Override
+    public int deleteLdTaskById(Long id)
+    {
+        return ldTaskMapper.deleteLdTaskById(id);
+    }
+
+    /**
+     * 批量删除任务
+     * 
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    @Override
+    public int deleteLdTaskByIds(Long[] ids)
+    {
+        return ldTaskMapper.deleteLdTaskByIds(ids);
+    }
+
+    /**
+     * 更新任务状态
+     * 
+     * @param id 任务ID
+     * @param status 新状态
+     * @return 结果
+     */
+    @Override
+    public int updateTaskStatus(Long id, String status)
+    {
+        return ldTaskMapper.updateTaskStatus(id, status);
+    }
+
+    /**
+     * 查询待执行的任务
+     *
+     * @return 待执行任务列表
+     */
+    @Override
+    public List<LdTask> selectPendingScheduledTasks()
+    {
+        return ldTaskMapper.selectPendingScheduledTasks();
+    }
+
+    /**
+     * 根据设备ID查询正在运行的任务
+     *
+     * @param deviceId 设备ID
+     * @return 正在运行的任务列表
+     */
+    @Override
+    public List<LdTask> selectRunningTasksByDeviceId(String deviceId)
+    {
+        return ldTaskMapper.selectRunningTasksByDeviceId(deviceId);
+    }
+
+    /**
+     * 根据设备ID更新任务状态
+     *
+     * @param deviceId 设备ID
+     * @param status 新状态
+     * @return 更新数量
+     */
+    @Override
+    public int updateTaskStatusByDeviceId(String deviceId, String status)
+    {
+        return ldTaskMapper.updateTaskStatusByDeviceId(deviceId, status);
+    }
+}

+ 81 - 0
ruoyi-system/src/main/java/com/ruoyi/robot/service/impl/LdTilemapConfigServiceImpl.java

@@ -0,0 +1,81 @@
+package com.ruoyi.robot.service.impl;
+
+import com.ruoyi.common.core.redis.RedisCache;
+import com.ruoyi.robot.domain.LdTilemapConfig;
+import com.ruoyi.robot.mapper.LdTilemapConfigMapper;
+import com.ruoyi.robot.service.ILdTilemapConfigService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+/**
+ * 瓦片地图配置Service实现
+ */
+@Service
+public class LdTilemapConfigServiceImpl implements ILdTilemapConfigService {
+
+    @Autowired
+    private LdTilemapConfigMapper tilemapConfigMapper;
+
+    @Autowired
+    private RedisCache redisCache;
+
+    @Value("${tilemap.storage.path:/data/tilemap}")
+    private String defaultStoragePath;
+
+    @Value("${tilemap.base-url:http://localhost:8080/tilemap}")
+    private String defaultBaseUrl;
+
+    private static final String TILEMAP_CACHE_KEY = "ld:tilemap:config:";
+
+    @Override
+    public LdTilemapConfig selectTilemapConfigByMapName(String mapName) {
+        String cacheKey = TILEMAP_CACHE_KEY + mapName;
+
+//        LdTilemapConfig cached = redisCache.getCacheObject(cacheKey);
+//        if (cached != null) {
+//            return cached;
+//        }
+
+        LdTilemapConfig config = tilemapConfigMapper.selectByMapName(mapName);
+
+//        if (config != null) {
+//            redisCache.setCacheObject(cacheKey, config);
+//        }
+
+        return config;
+    }
+
+    @Override
+    public String getTilePath(String mapName, int zoom, int x, int y) {
+        LdTilemapConfig config = selectTilemapConfigByMapName(mapName);
+        if (config == null) {
+            return null;
+        }
+
+        String storagePath = config.getTileStoragePath();
+        if (storagePath == null || storagePath.isEmpty()) {
+            storagePath = defaultStoragePath + "/" + mapName;
+        }
+
+        // 瓦片路径格式: /path/mapName/zoom/x/y.png
+        String format = config.getFormat() != null ? config.getFormat() : "png";
+        return String.format("%s/%s/%d/%d/%d.%s", storagePath, mapName, zoom, x, y, format);
+    }
+
+    @Override
+    public String getTileUrl(String mapName, int zoom, int x, int y) {
+        LdTilemapConfig config = selectTilemapConfigByMapName(mapName);
+        if (config == null) {
+            return null;
+        }
+
+        String baseUrl = config.getTileBaseUrl();
+        if (baseUrl == null || baseUrl.isEmpty()) {
+            baseUrl = defaultBaseUrl + "/" + mapName;
+        }
+
+        String format = config.getFormat() != null ? config.getFormat() : "png";
+        return String.format("%s/%d/%d/%d.%s", baseUrl, zoom, x, y, format);
+    }
+}

+ 118 - 0
ruoyi-system/src/main/resources/mapper/robot/LdMapProjectMapper.xml

@@ -0,0 +1,118 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.ruoyi.robot.mapper.LdMapProjectMapper">
+
+    <resultMap type="com.ruoyi.robot.domain.LdMapProject" id="LdMapProjectResult">
+        <result property="id" column="id"/>
+        <result property="mapName" column="map_name"/>
+        <result property="displayName" column="display_name"/>
+        <result property="status" column="status"/>
+        <result property="thumbnailPath" column="thumbnail_path"/>
+        <result property="thumbnailUrl" column="thumbnail_url"/>
+        <result property="description" column="description"/>
+        <result property="fileSize" column="file_size"/>
+        <result property="isCurrent" column="is_current"/>
+        <result property="deviceId" column="device_id"/>
+        <result property="source" column="source"/>
+        <result property="createBy" column="create_by"/>
+        <result property="createTime" column="create_time"/>
+        <result property="updateBy" column="update_by"/>
+        <result property="updateTime" column="update_time"/>
+        <result property="remark" column="remark"/>
+    </resultMap>
+
+    <resultMap type="com.ruoyi.robot.domain.vo.LdMapListVo" id="LdMapListVoResult">
+        <result property="mapName" column="map_name"/>
+        <result property="status" column="status"/>
+    </resultMap>
+
+    <sql id="selectLdMapProjectVo">
+        select id, map_name, display_name, status, thumbnail_path, thumbnail_url,
+               description, file_size, is_current, device_id, source,
+               create_by, create_time, update_by, update_time, remark
+        from ld_map_project
+    </sql>
+    <insert id="insertLdMapProject" parameterType="com.ruoyi.robot.domain.LdMapProject" useGeneratedKeys="true" keyProperty="id">
+        insert into ld_map_project
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="mapName != null and mapName != ''">map_name,</if>
+            <if test="displayName != null">display_name,</if>
+            <if test="status != null and status != ''">status,</if>
+            <if test="thumbnailPath != null">thumbnail_path,</if>
+            <if test="thumbnailUrl != null">thumbnail_url,</if>
+            <if test="description != null">description,</if>
+            <if test="fileSize != null">file_size,</if>
+            <if test="isCurrent != null">is_current,</if>
+            <if test="deviceId != null">device_id,</if>
+            <if test="source != null">source,</if>
+            <if test="createBy != null">create_by,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="updateBy != null">update_by,</if>
+            <if test="updateTime != null">update_time,</if>
+            <if test="remark != null">remark,</if>
+        </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="mapName != null and mapName != ''">#{mapName},</if>
+            <if test="displayName != null">#{displayName},</if>
+            <if test="status != null and status != ''">#{status},</if>
+            <if test="thumbnailPath != null">#{thumbnailPath},</if>
+            <if test="thumbnailUrl != null">#{thumbnailUrl},</if>
+            <if test="description != null">#{description},</if>
+            <if test="fileSize != null">#{fileSize},</if>
+            <if test="isCurrent != null">#{isCurrent},</if>
+            <if test="deviceId != null">#{deviceId},</if>
+            <if test="source != null">#{source},</if>
+            <if test="createBy != null">#{createBy},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="updateBy != null">#{updateBy},</if>
+            <if test="updateTime != null">#{updateTime},</if>
+            <if test="remark != null">#{remark},</if>
+        </trim>
+    </insert>
+
+    <update id="updateLdMapProject" parameterType="com.ruoyi.robot.domain.LdMapProject">
+        update ld_map_project
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="mapName != null and mapName != ''">map_name = #{mapName},</if>
+            <if test="displayName != null">display_name = #{displayName},</if>
+            <if test="status != null and status != ''">status = #{status},</if>
+            <if test="thumbnailPath != null">thumbnail_path = #{thumbnailPath},</if>
+            <if test="thumbnailUrl != null">thumbnail_url = #{thumbnailUrl},</if>
+            <if test="description != null">description = #{description},</if>
+            <if test="fileSize != null">file_size = #{fileSize},</if>
+            <if test="isCurrent != null">is_current = #{isCurrent},</if>
+            <if test="deviceId != null">device_id = #{deviceId},</if>
+            <if test="source != null">source = #{source},</if>
+            <if test="createBy != null">create_by = #{createBy},</if>
+            <if test="createTime != null">create_time = #{createTime},</if>
+            <if test="updateBy != null">update_by = #{updateBy},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+            <if test="remark != null">remark = #{remark},</if>
+        </trim>
+        where map_name = #{mapName}
+    </update>
+
+    <select id="selectMapListVo" resultMap="LdMapListVoResult">
+        select map_name, status
+        from ld_map_project
+        where status != 'deleted'
+        order by is_current desc, update_time desc
+    </select>
+
+    <select id="selectCurrentMapByDeviceId" resultMap="LdMapProjectResult">
+        <include refid="selectLdMapProjectVo"/>
+        where is_current = 1
+        <if test="deviceId != null and deviceId != ''">
+            and device_id = #{deviceId}
+        </if>
+        limit 1
+    </select>
+
+    <select id="selectByMapName" resultMap="LdMapProjectResult">
+        <include refid="selectLdMapProjectVo"/>
+        where map_name = #{mapName}
+    </select>
+
+</mapper>

+ 49 - 0
ruoyi-system/src/main/resources/mapper/robot/LdRoadmapMapper.xml

@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.ruoyi.robot.mapper.LdRoadmapMapper">
+
+    <resultMap type="com.ruoyi.robot.domain.LdRoadmap" id="LdRoadmapResult">
+        <result property="id" column="id"/>
+        <result property="mapName" column="map_name"/>
+        <result property="geojsonData" column="geojson_data"/>
+        <result property="version" column="version"/>
+        <result property="pointCount" column="point_count"/>
+        <result property="lineCount" column="line_count"/>
+        <result property="isValid" column="is_valid"/>
+        <result property="createBy" column="create_by"/>
+        <result property="createTime" column="create_time"/>
+        <result property="updateBy" column="update_by"/>
+        <result property="updateTime" column="update_time"/>
+    </resultMap>
+
+    <sql id="selectLdRoadmapVo">
+        select id, map_name, geojson_data, version, point_count, line_count, is_valid,
+               create_by, create_time, update_by, update_time
+        from ld_roadmap
+    </sql>
+    <insert id="insert"></insert>
+    <update id="updateById" parameterType="com.ruoyi.robot.domain.LdRoadmap">
+        update ld_roadmap
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="mapName != null and mapName != ''">map_name = #{mapName},</if>
+            <if test="geojsonData != null">geojson_data = #{geojsonData},</if>
+            <if test="version != null">version = #{version},</if>
+            <if test="pointCount != null">point_count = #{pointCount},</if>
+            <if test="lineCount != null">line_count = #{lineCount},</if>
+            <if test="isValid != null">is_valid = #{isValid},</if>
+            <if test="createBy != null">create_by = #{createBy},</if>
+            <if test="createTime != null">create_time = #{createTime},</if>
+            <if test="updateBy != null">update_by = #{updateBy},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <select id="selectByMapName" resultMap="LdRoadmapResult">
+        <include refid="selectLdRoadmapVo"/>
+        where map_name = #{mapName} and is_valid = 1
+    </select>
+
+</mapper>

+ 138 - 0
ruoyi-system/src/main/resources/mapper/robot/LdTaskExecutionLogMapper.xml

@@ -0,0 +1,138 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.ruoyi.robot.mapper.LdTaskExecutionLogMapper">
+
+    <resultMap type="com.ruoyi.robot.domain.LdTaskExecutionLog" id="LdTaskExecutionLogResult">
+        <result property="id" column="id"/>
+        <result property="taskId" column="task_id"/>
+        <result property="taskName" column="task_name"/>
+        <result property="mapName" column="map_name"/>
+        <result property="executeTime" column="execute_time"/>
+        <result property="endTime" column="end_time"/>
+        <result property="status" column="status"/>
+        <result property="startWaypoint" column="start_waypoint"/>
+        <result property="endWaypoint" column="end_waypoint"/>
+        <result property="completedWaypoints" column="completed_waypoints"/>
+        <result property="totalWaypoints" column="total_waypoints"/>
+        <result property="errorCode" column="error_code"/>
+        <result property="errorMessage" column="error_message"/>
+        <result property="executeResult" column="execute_result"/>
+        <result property="deviceId" column="device_id"/>
+        <result property="createTime" column="create_time"/>
+    </resultMap>
+
+    <sql id="selectLdTaskExecutionLogVo">
+        select id, task_id, task_name, map_name, execute_time, end_time, status,
+               start_waypoint, end_waypoint, completed_waypoints, total_waypoints,
+               error_code, error_message, execute_result, device_id, create_time
+        from ld_task_execution_log
+    </sql>
+
+    <select id="selectLdTaskExecutionLogList" parameterType="com.ruoyi.robot.domain.LdTaskExecutionLog" resultMap="LdTaskExecutionLogResult">
+        <include refid="selectLdTaskExecutionLogVo"/>
+        <where>
+            <if test="taskId != null">
+                AND task_id = #{taskId}
+            </if>
+            <if test="taskName != null and taskName != ''">
+                AND task_name like concat('%', #{taskName}, '%')
+            </if>
+            <if test="mapName != null and mapName != ''">
+                AND map_name = #{mapName}
+            </if>
+            <if test="status != null and status != ''">
+                AND status = #{status}
+            </if>
+            <if test="deviceId != null and deviceId != ''">
+                AND device_id = #{deviceId}
+            </if>
+        </where>
+        order by execute_time desc
+    </select>
+
+    <select id="selectLdTaskExecutionLogById" parameterType="Long" resultMap="LdTaskExecutionLogResult">
+        <include refid="selectLdTaskExecutionLogVo"/>
+        where id = #{id}
+    </select>
+
+    <select id="selectLdTaskExecutionLogByTaskId" parameterType="Long" resultMap="LdTaskExecutionLogResult">
+        <include refid="selectLdTaskExecutionLogVo"/>
+        where task_id = #{taskId}
+        order by execute_time desc
+    </select>
+
+    <select id="selectTodayExecutionLogsByDeviceId" resultMap="LdTaskExecutionLogResult">
+        <include refid="selectLdTaskExecutionLogVo"/>
+        where device_id = #{deviceId}
+          and date(execute_time) = #{executeDate}
+        order by execute_time desc
+    </select>
+
+    <insert id="insertLdTaskExecutionLog" parameterType="com.ruoyi.robot.domain.LdTaskExecutionLog" useGeneratedKeys="true" keyProperty="id">
+        insert into ld_task_execution_log
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="taskId != null">task_id,</if>
+            <if test="taskName != null and taskName != ''">task_name,</if>
+            <if test="mapName != null and mapName != ''">map_name,</if>
+            <if test="executeTime != null">execute_time,</if>
+            <if test="endTime != null">end_time,</if>
+            <if test="status != null and status != ''">status,</if>
+            <if test="startWaypoint != null">start_waypoint,</if>
+            <if test="endWaypoint != null">end_waypoint,</if>
+            <if test="completedWaypoints != null">completed_waypoints,</if>
+            <if test="totalWaypoints != null">total_waypoints,</if>
+            <if test="errorCode != null">error_code,</if>
+            <if test="errorMessage != null">error_message,</if>
+            <if test="executeResult != null">execute_result,</if>
+            <if test="deviceId != null">device_id,</if>
+        </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="taskId != null">#{taskId},</if>
+            <if test="taskName != null and taskName != ''">#{taskName},</if>
+            <if test="mapName != null and mapName != ''">#{mapName},</if>
+            <if test="executeTime != null">#{executeTime},</if>
+            <if test="endTime != null">#{endTime},</if>
+            <if test="status != null and status != ''">#{status},</if>
+            <if test="startWaypoint != null">#{startWaypoint},</if>
+            <if test="endWaypoint != null">#{endWaypoint},</if>
+            <if test="completedWaypoints != null">#{completedWaypoints},</if>
+            <if test="totalWaypoints != null">#{totalWaypoints},</if>
+            <if test="errorCode != null">#{errorCode},</if>
+            <if test="errorMessage != null">#{errorMessage},</if>
+            <if test="executeResult != null">#{executeResult},</if>
+            <if test="deviceId != null">#{deviceId},</if>
+        </trim>
+    </insert>
+
+    <update id="updateLdTaskExecutionLog" parameterType="com.ruoyi.robot.domain.LdTaskExecutionLog">
+        update ld_task_execution_log
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="endTime != null">end_time = #{endTime},</if>
+            <if test="status != null and status != ''">status = #{status},</if>
+            <if test="endWaypoint != null">end_waypoint = #{endWaypoint},</if>
+            <if test="completedWaypoints != null">completed_waypoints = #{completedWaypoints},</if>
+            <if test="errorCode != null">error_code = #{errorCode},</if>
+            <if test="errorMessage != null">error_message = #{errorMessage},</if>
+            <if test="executeResult != null">execute_result = #{executeResult},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteLdTaskExecutionLogById" parameterType="Long">
+        delete from ld_task_execution_log where id = #{id}
+    </delete>
+
+    <delete id="deleteLdTaskExecutionLogByIds" parameterType="Long">
+        delete from ld_task_execution_log where id in
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+
+    <delete id="deleteLdTaskExecutionLogByTaskId" parameterType="Long">
+        delete from ld_task_execution_log where task_id = #{taskId}
+    </delete>
+
+</mapper>

+ 184 - 0
ruoyi-system/src/main/resources/mapper/robot/LdTaskMapper.xml

@@ -0,0 +1,184 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.ruoyi.robot.mapper.LdTaskMapper">
+
+    <resultMap type="com.ruoyi.robot.domain.LdTask" id="LdTaskResult">
+        <result property="id" column="id"/>
+        <result property="mapName" column="map_name"/>
+        <result property="taskName" column="task_name"/>
+        <result property="taskDesc" column="task_desc"/>
+        <result property="cronExpression" column="cron_expression"/>
+        <result property="executeTime" column="execute_time"/>
+        <result property="executeWeekdays" column="execute_weekdays"/>
+        <result property="repeatCount" column="repeat_count"/>
+        <result property="coordType" column="coord_type"/>
+        <result property="waypointIds" column="waypoint_ids"/>
+        <result property="waypointCoords" column="waypoint_coords"/>
+        <result property="waypointTypes" column="waypoint_types"/>
+        <result property="waypointActions" column="waypoint_actions"/>
+        <result property="status" column="status"/>
+        <result property="lastExecuteTime" column="last_execute_time"/>
+        <result property="lastExecuteStatus" column="last_execute_status"/>
+        <result property="nextExecuteTime" column="next_execute_time"/>
+        <result property="totalExecuteCount" column="total_execute_count"/>
+        <result property="deviceId" column="device_id"/>
+        <result property="createBy" column="create_by"/>
+        <result property="createTime" column="create_time"/>
+        <result property="updateBy" column="update_by"/>
+        <result property="updateTime" column="update_time"/>
+        <result property="remark" column="remark"/>
+    </resultMap>
+
+    <sql id="selectLdTaskVo">
+        select id, map_name, task_name, task_desc, cron_expression, execute_time, execute_weekdays, 
+               repeat_count, coord_type, waypoint_ids, waypoint_coords, waypoint_types, waypoint_actions,
+               status, last_execute_time, last_execute_status, next_execute_time, total_execute_count,
+               device_id, create_by, create_time, update_by, update_time, remark
+        from ld_task
+    </sql>
+
+    <select id="selectLdTaskList" parameterType="com.ruoyi.robot.domain.LdTask" resultMap="LdTaskResult">
+        <include refid="selectLdTaskVo"/>
+        <where>
+            <if test="mapName != null and mapName != ''">
+                AND map_name = #{mapName}
+            </if>
+            <if test="taskName != null and taskName != ''">
+                AND task_name like concat('%', #{taskName}, '%')
+            </if>
+            <if test="status != null and status != ''">
+                AND status = #{status}
+            </if>
+            <if test="deviceId != null and deviceId != ''">
+                AND device_id = #{deviceId}
+            </if>
+        </where>
+        order by create_time desc
+    </select>
+
+    <select id="selectLdTaskById" parameterType="Long" resultMap="LdTaskResult">
+        <include refid="selectLdTaskVo"/>
+        where id = #{id}
+    </select>
+
+    <select id="selectLdTaskListByMapName" parameterType="String" resultMap="LdTaskResult">
+        <include refid="selectLdTaskVo"/>
+        where map_name = #{mapName}
+        order by create_time desc
+    </select>
+
+    <select id="selectLdTaskByMapAndName" resultMap="LdTaskResult">
+        <include refid="selectLdTaskVo"/>
+        where map_name = #{mapName} and task_name = #{taskName}
+    </select>
+
+    <select id="selectPendingScheduledTasks" resultMap="LdTaskResult">
+        <include refid="selectLdTaskVo"/>
+        where cron_expression is not null 
+          and cron_expression != ''
+          and status in ('idle', 'paused')
+        order by create_time asc
+    </select>
+
+    <insert id="insertLdTask" parameterType="com.ruoyi.robot.domain.LdTask" useGeneratedKeys="true" keyProperty="id">
+        insert into ld_task
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="mapName != null and mapName != ''">map_name,</if>
+            <if test="taskName != null and taskName != ''">task_name,</if>
+            <if test="taskDesc != null">task_desc,</if>
+            <if test="cronExpression != null">cron_expression,</if>
+            <if test="executeTime != null">execute_time,</if>
+            <if test="executeWeekdays != null">execute_weekdays,</if>
+            <if test="repeatCount != null">repeat_count,</if>
+            <if test="coordType != null">coord_type,</if>
+            <if test="waypointIds != null">waypoint_ids,</if>
+            <if test="waypointCoords != null">waypoint_coords,</if>
+            <if test="waypointTypes != null">waypoint_types,</if>
+            <if test="waypointActions != null">waypoint_actions,</if>
+            <if test="status != null">status,</if>
+            <if test="deviceId != null">device_id,</if>
+            <if test="createBy != null and createBy != ''">create_by,</if>
+            <if test="remark != null">remark,</if>
+        </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="mapName != null and mapName != ''">#{mapName},</if>
+            <if test="taskName != null and taskName != ''">#{taskName},</if>
+            <if test="taskDesc != null">#{taskDesc},</if>
+            <if test="cronExpression != null">#{cronExpression},</if>
+            <if test="executeTime != null">#{executeTime},</if>
+            <if test="executeWeekdays != null">#{executeWeekdays},</if>
+            <if test="repeatCount != null">#{repeatCount},</if>
+            <if test="coordType != null">#{coordType},</if>
+            <if test="waypointIds != null">#{waypointIds},</if>
+            <if test="waypointCoords != null">#{waypointCoords},</if>
+            <if test="waypointTypes != null">#{waypointTypes},</if>
+            <if test="waypointActions != null">#{waypointActions},</if>
+            <if test="status != null">#{status},</if>
+            <if test="deviceId != null">#{deviceId},</if>
+            <if test="createBy != null and createBy != ''">#{createBy},</if>
+            <if test="remark != null">#{remark},</if>
+        </trim>
+    </insert>
+
+    <update id="updateLdTask" parameterType="com.ruoyi.robot.domain.LdTask">
+        update ld_task
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="taskDesc != null">task_desc = #{taskDesc},</if>
+            <if test="cronExpression != null">cron_expression = #{cronExpression},</if>
+            <if test="executeTime != null">execute_time = #{executeTime},</if>
+            <if test="executeWeekdays != null">execute_weekdays = #{executeWeekdays},</if>
+            <if test="repeatCount != null">repeat_count = #{repeatCount},</if>
+            <if test="coordType != null">coord_type = #{coordType},</if>
+            <if test="waypointIds != null">waypoint_ids = #{waypointIds},</if>
+            <if test="waypointCoords != null">waypoint_coords = #{waypointCoords},</if>
+            <if test="waypointTypes != null">waypoint_types = #{waypointTypes},</if>
+            <if test="waypointActions != null">waypoint_actions = #{waypointActions},</if>
+            <if test="status != null">status = #{status},</if>
+            <if test="lastExecuteTime != null">last_execute_time = #{lastExecuteTime},</if>
+            <if test="lastExecuteStatus != null">last_execute_status = #{lastExecuteStatus},</if>
+            <if test="nextExecuteTime != null">next_execute_time = #{nextExecuteTime},</if>
+            <if test="totalExecuteCount != null">total_execute_count = #{totalExecuteCount},</if>
+            <if test="deviceId != null">device_id = #{deviceId},</if>
+            <if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
+            <if test="remark != null">remark = #{remark},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <update id="updateTaskStatus">
+        update ld_task set status = #{status}, update_time = sysdate()
+        where id = #{id}
+    </update>
+
+    <delete id="deleteLdTaskById" parameterType="Long">
+        delete from ld_task where id = #{id}
+    </delete>
+
+    <delete id="deleteLdTaskByIds" parameterType="Long">
+        delete from ld_task where id in
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+
+    <delete id="deleteLdTaskByMapName" parameterType="String">
+        delete from ld_task where map_name = #{mapName}
+    </delete>
+
+    <!-- 根据设备ID查询正在运行的任务(状态为running或paused) -->
+    <select id="selectRunningTasksByDeviceId" parameterType="String" resultMap="LdTaskResult">
+        <include refid="selectLdTaskVo"/>
+        where device_id = #{deviceId}
+          and status in ('running', 'paused')
+        order by update_time desc
+    </select>
+
+    <!-- 根据设备ID批量更新任务状态 -->
+    <update id="updateTaskStatusByDeviceId">
+        update ld_task set status = #{status}, update_time = sysdate()
+        where device_id = #{deviceId} and status in ('running', 'paused')
+    </update>
+
+</mapper>

+ 45 - 0
ruoyi-system/src/main/resources/mapper/robot/LdTilemapConfigMapper.xml

@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.ruoyi.robot.mapper.LdTilemapConfigMapper">
+
+    <resultMap type="com.ruoyi.robot.domain.LdTilemapConfig" id="LdTilemapConfigResult">
+        <result property="id" column="id"/>
+        <result property="mapName" column="map_name"/>
+        <result property="layerCnt" column="layer_cnt"/>
+        <result property="minZoom" column="min_zoom"/>
+        <result property="maxZoom" column="max_zoom"/>
+        <result property="tileSize" column="tile_size"/>
+        <result property="projectionType" column="projection_type"/>
+        <result property="extentLeft" column="extent_left"/>
+        <result property="extentBottom" column="extent_bottom"/>
+        <result property="extentRight" column="extent_right"/>
+        <result property="extentTop" column="extent_top"/>
+        <result property="tileStoragePath" column="tile_storage_path"/>
+        <result property="tileBaseUrl" column="tile_base_url"/>
+        <result property="originX" column="origin_x"/>
+        <result property="originY" column="origin_y"/>
+        <result property="resolution" column="resolution"/>
+        <result property="format" column="format"/>
+        <result property="configJson" column="config_json"/>
+        <result property="createBy" column="create_by"/>
+        <result property="createTime" column="create_time"/>
+        <result property="updateBy" column="update_by"/>
+        <result property="updateTime" column="update_time"/>
+    </resultMap>
+
+    <sql id="selectLdTilemapConfigVo">
+        select id, map_name, layer_cnt, min_zoom, max_zoom, tile_size, projection_type,
+               extent_left, extent_bottom, extent_right, extent_top,
+               tile_storage_path, tile_base_url, origin_x, origin_y, resolution, format,
+               config_json, create_by, create_time, update_by, update_time
+        from ld_tilemap_config
+    </sql>
+
+    <select id="selectByMapName" resultMap="LdTilemapConfigResult">
+        <include refid="selectLdTilemapConfigVo"/>
+        where map_name = #{mapName}
+    </select>
+
+</mapper>