From 3f43e40ed1a1e9f66b3ad2862da9dbc4b710c712 Mon Sep 17 00:00:00 2001
From: nishiwei <13126973358@163.com>
Date: Fri, 23 Oct 2026 14:09:47 +0800
Subject: [PATCH] 智云3.0机场1适配内容提交

---
 ard-admin/src/main/java/com/ard/web/controller/sample/dock1/media/Dock1MediaController.java                 |   80 
 ard-sample/src/main/java/com/ard/sample/dock1/manage/model/receiver/Dock1DroneAuthorityInfoReceiver.java    |   35 
 ard-route/src/main/java/com/ard/route/util/InitializationUtils.java                                         |   17 
 ard-route/src/main/java/com/ard/route/util/RouteFileUtils.java                                              |    6 
 ard-sample/src/main/java/com/ard/sample/dock1/manage/service/ICommandService.java                           |   26 
 ard-sample/src/main/java/com/ard/sample/dock1/wayline/service/impl/Dock1WaylineFileServiceImpl.java         |  289 ++
 ard-admin/src/main/resources/application-daqing.yml                                                         |   16 
 ard-admin/src/main/java/com/ard/web/controller/sample/dock1/Dock1TopologyController.java                    |   45 
 ard-sample/src/main/java/com/ard/sample/component/websocket/handler/DockWebSocketHandler.java               |   80 
 ard-sample/src/main/java/com/ard/sample/dock1/manage/service/ILiveStreamService.java                        |   73 
 ard-sample/src/main/java/com/ard/sample/component/mqtt/handler/StateLiveStatusHandler.java                  |   10 
 ard-sample/src/main/java/com/ard/sample/dock1/manage/service/IWaylineTrajectoryService.java                 |   28 
 ard-sample/src/main/java/com/ard/sample/dock1/wayline/service/impl/Dock1FlightTaskServiceImpl.java          |  288 ++
 ard-sample/src/main/java/com/ard/sample/dock1/manage/service/impl/Dock1WaylineTrajectoryServiceImpl.java    |  274 ++
 ard-sample/src/main/java/com/ard/sample/manage/service/impl/DockModelResolver.java                          |   41 
 ard-sample/src/main/java/com/ard/sample/dock1/manage/service/impl/Dock1GatewayOSDServiceImpl.java           |   70 
 ard-admin/src/main/java/com/ard/web/controller/sample/dock1/Dock1CommandController.java                     |   38 
 ard-sample/src/main/java/com/ard/sample/dock1/manage/service/impl/Dock1LiveStreamServiceImpl.java           |  544 +++++
 ard-sample/src/main/java/com/ard/sample/dock1/wayline/model/dto/Dock1WaylineTaskCreateDTO.java              |   79 
 ard-route/src/main/java/com/ard/route/util/UavRouteReqUtils.java                                            |    7 
 ard-route/src/main/java/com/ard/route/util/ToolUtils.java                                                   |   17 
 ard-admin/src/main/java/com/ard/web/controller/sample/dock1/media/Dock1FileController.java                  |   95 
 ard-sample/src/main/java/com/ard/sample/manage/model/enums/DockModelEnum.java                               |   64 
 ard-sample/src/main/java/com/ard/sample/manage/service/impl/DockOsdDispatcher.java                          |   60 
 ard-sample/src/main/java/com/ard/sample/dock1/manage/service/impl/Dock1DockOSDServiceImpl.java              |  125 +
 sql/dock1_dictionary.sql                                                                                    |   58 
 ard-admin/src/main/java/com/ard/web/controller/sample/dock1/Dock1DevicePayloadController.java               |   39 
 ard-admin/src/main/java/com/ard/web/controller/sample/dock1/Dock1DeviceController.java                      |  279 ++
 ard-admin/src/main/java/com/ard/web/controller/sample/dock1/Dock1LiveStreamController.java                  |  163 +
 ard-admin/src/main/java/com/ard/web/controller/sample/dock1/wayline/Dock1WaylineJobController.java          |  116 +
 ard-admin/pom.xml                                                                                           |   17 
 ard-sample/src/main/java/com/ard/sample/dock1/manage/service/impl/Dock1ControlServiceImpl.java              |  246 ++
 ard-sample/src/main/java/com/ard/sample/manage/service/impl/DockOnlineServiceImpl.java                      |  108 
 ard-sample/src/main/java/com/ard/sample/media/service/impl/FileServiceImpl.java                             |   26 
 ard-sample/src/main/java/com/ard/sample/manage/service/impl/DeviceServiceImpl.java                          |   46 
 ard-sample/src/main/java/com/ard/sample/component/mqtt/config/MqttMessageChannel.java                       |    8 
 ard-admin/src/main/java/com/ard/web/controller/sample/dock1/wayline/Dock1WaylineTrajectoryController.java   |   46 
 ard-admin/src/main/java/com/ard/web/controller/sample/dock1/wayline/Dock1WaylineFileController.java         |  170 +
 ard-sample/src/main/java/com/ard/sample/dock1/manage/model/receiver/Dock1DroneAuthorityPayloadReceiver.java |   33 
 ard-route/src/main/java/com/ard/route/domain/djuav/kml/KmlPayloadInfo.java                                  |    6 
 ard-sample/src/main/java/com/ard/sample/dock1/manage/model/receiver/Dock1OsdDockReceiver.java               |  306 ++
 ard-sample/src/main/java/com/ard/sample/dock1/wayline/service/IWaylineFileService.java                      |   80 
 ard-admin/src/main/java/com/ard/web/controller/sample/dock1/Dock1DeviceFirmwareController.java              |  111 +
 ard-sample/src/main/java/com/ard/sample/dock1/manage/model/receiver/Dock1OsdSubDeviceReceiver.java          |   88 
 ard-admin/src/main/java/com/ard/web/controller/sample/dock1/Dock1DockController.java                        |   43 
 ard-route/src/main/java/com/ard/route/service/impl/ArdRouteInfoServiceImpl.java                             |   20 
 ard-sample/src/main/java/com/ard/sample/component/mqtt/model/ChannelName.java                               |    5 
 ard-sample/src/main/java/com/ard/sample/dock1/manage/service/IControlService.java                           |   35 
 ard-sample/src/main/java/com/ard/sample/dock1/wayline/service/IFlightTaskService.java                       |   22 
 ard-sample/src/main/java/com/ard/sample/dock1/wayline/service/IWaylineJobService.java                       |  129 +
 ard-sample/src/main/java/com/ard/sample/dock1/wayline/service/impl/Dock1WaylineJobServiceImpl.java          | 1160 ++++++++++
 ard-admin/src/main/java/com/ard/web/controller/sample/dock1/Dock1DeviceHmsController.java                   |   82 
 ard-sample/src/main/java/com/ard/sample/dock1/manage/service/impl/Dock1FlightSetupExceptionHandler.java     |   68 
 ard-sample/src/main/java/com/ard/sample/dock1/manage/service/impl/Dock1CommandServiceImpl.java              |  126 +
 ard-sample/src/main/java/com/ard/sample/storage/service/impl/StorageServiceImpl.java                        |   13 
 ard-admin/src/main/resources/application-daqing-druid.yml                                                   |    8 
 ard-sample/src/main/java/com/ard/sample/dock1/manage/service/impl/Dock1DeviceOSDServiceImpl.java            |  142 +
 sql/dock1_sys_dict.sql                                                                                      |   34 
 ard-sample/src/main/java/com/ard/sample/component/mqtt/model/EventsMethodEnum.java                          |    5 
 ard-admin/src/main/java/com/ard/web/controller/sample/dock1/Dock1DeviceLogsController.java                  |  124 +
 ard-admin/src/main/java/com/ard/web/controller/sample/media/FileController.java                             |    6 
 ard-sample/src/main/java/com/ard/sample/component/websocket/handler/LiveStreamHandler.java                  |   71 
 62 files changed, 6,345 insertions(+), 101 deletions(-)

diff --git a/ard-admin/pom.xml b/ard-admin/pom.xml
index 6e065ff..7ba3ef5 100644
--- a/ard-admin/pom.xml
+++ b/ard-admin/pom.xml
@@ -125,9 +125,20 @@
                     </execution>
                 </executions>
             </plugin>
-            <plugin>   
-                <groupId>org.apache.maven.plugins</groupId>   
-                <artifactId>maven-war-plugin</artifactId>   
+            <!-- 资源过滤使用 @ 作为分隔符,替换 application.yml 中的 @spring.profiles.active@ -->
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-resources-plugin</artifactId>
+                <configuration>
+                    <delimiters>
+                        <delimiter>@</delimiter>
+                    </delimiters>
+                    <useDefaultDelimiters>false</useDefaultDelimiters>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-war-plugin</artifactId>
                 <version>3.1.0</version>   
                 <configuration>
                     <failOnMissingWebXml>false</failOnMissingWebXml>
diff --git a/ard-admin/src/main/java/com/ard/web/controller/sample/dock1/Dock1CommandController.java b/ard-admin/src/main/java/com/ard/web/controller/sample/dock1/Dock1CommandController.java
new file mode 100644
index 0000000..868e2b4
--- /dev/null
+++ b/ard-admin/src/main/java/com/ard/web/controller/sample/dock1/Dock1CommandController.java
@@ -0,0 +1,38 @@
+/*
+ * 【机场1】从 dock2 原类 com.ard.web.controller.sample.CommandController 复制/独立适配(DJI Dock 1 / M30)。
+ * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。
+ */
+package com.ard.web.controller.sample.dock1;
+
+import com.ard.sample.common.model.ResponseResult;
+import com.ard.sample.manage.model.param.CommandParam;
+import com.ard.sample.dock1.manage.service.ICommandService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+import com.ard.web.controller.sample.CommandController;
+
+/**
+ * @author sean
+ * @version 1.2
+ * @date 2022/7/29
+ */
+@Api(tags = "指令控制")
+@RestController
+@Slf4j
+@RequestMapping("/dock1/devices")
+public class Dock1CommandController {
+
+    @Autowired
+    private ICommandService commandService;
+
+    @ApiOperation("负载控制-拍照")
+    @PostMapping("/{sn}/command/cameraPhotoTake")
+    public ResponseResult cameraPhotoTake(@PathVariable String sn,
+                                          @RequestBody CommandParam param) {
+        return commandService.cameraPhotoTake(sn, param);
+    }
+
+}
diff --git a/ard-admin/src/main/java/com/ard/web/controller/sample/dock1/Dock1DeviceController.java b/ard-admin/src/main/java/com/ard/web/controller/sample/dock1/Dock1DeviceController.java
new file mode 100644
index 0000000..f27f1dc
--- /dev/null
+++ b/ard-admin/src/main/java/com/ard/web/controller/sample/dock1/Dock1DeviceController.java
@@ -0,0 +1,279 @@
+/*
+ * 【机场1】从 dock2 原类 com.ard.web.controller.sample.DeviceController 复制/独立适配(DJI Dock 1 / M30)。
+ * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。
+ */
+package com.ard.web.controller.sample.dock1;
+
+import com.ard.common.core.controller.BaseController;
+import com.ard.common.core.page.TableDataInfo;
+import com.ard.device.camera.domain.ArdCamera;
+import com.ard.sample.common.error.CommonErrorEnum;
+import com.ard.sample.common.model.PaginationData;
+import com.ard.sample.common.model.ResponseResult;
+import com.ard.sample.component.mqtt.model.ChannelName;
+import com.ard.sample.component.mqtt.model.CommonTopicReceiver;
+import com.ard.sample.component.mqtt.model.CommonTopicResponse;
+import com.ard.sample.manage.model.dto.DeviceDTO;
+import com.ard.sample.manage.model.dto.DeviceFirmwareUpgradeDTO;
+import com.ard.sample.manage.model.entity.DeviceEntity;
+import com.ard.sample.manage.model.enums.DeviceFirmwareStatusEnum;
+import com.ard.sample.manage.model.enums.DeviceSetPropertyEnum;
+import com.ard.sample.manage.model.receiver.StatusGatewayReceiver;
+import com.ard.sample.manage.service.IDeviceService;
+import com.baomidou.mybatisplus.core.conditions.Wrapper;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.fasterxml.jackson.databind.JsonNode;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.time.ZoneId;
+import java.util.List;
+import java.util.Optional;
+
+import static com.ard.common.utils.PageUtils.startPage;
+import com.ard.web.controller.sample.DeviceController;
+
+/**
+ * @author sean.zhou
+ * @version 0.1
+ * @date 2021/11/15
+ */
+@Api(tags = "设备管理")
+@RestController
+@Slf4j
+@RequestMapping("/dock1/device")
+public class Dock1DeviceController extends BaseController {
+
+    @Autowired
+    private IDeviceService deviceService;
+
+    /**
+     * 处理上线的消息
+     *
+     * @param receiver The drone information is not empty.
+     */
+    // 机场1:不复制原 ServiceActivator(inputChannel = ChannelName.INBOUND_STATUS_ONLINE),消费职责由共享层原类承担
+    public void deviceOnline(CommonTopicReceiver<StatusGatewayReceiver> receiver) {
+        boolean online = deviceService.deviceOnline(receiver.getData());
+        if (online) {
+            // Notify pilot that the drone is online successfully.
+            deviceService.publishStatusReply(receiver.getData().getSn(),
+                    CommonTopicResponse.builder()
+                            .tid(receiver.getTid())
+                            .bid(receiver.getBid())
+                            .timestamp(System.currentTimeMillis())
+                            .method(receiver.getMethod())
+                            .build());
+        }
+    }
+
+    /**
+     * 处理离线的消息
+     *
+     * @param receiver The drone information is empty.
+     */
+    // 机场1:不复制原 ServiceActivator(inputChannel = ChannelName.INBOUND_STATUS_OFFLINE),消费职责由共享层原类承担
+    public void deviceOffline(CommonTopicReceiver<StatusGatewayReceiver> receiver) {
+
+        boolean offline = deviceService.deviceOffline(receiver.getData());
+        if (offline) {
+            // Notify pilot that the device is offline successfully.
+            deviceService.publishStatusReply(receiver.getData().getSn(),
+                    CommonTopicResponse.builder()
+                            .tid(receiver.getTid())
+                            .bid(receiver.getBid())
+                            .timestamp(System.currentTimeMillis())
+                            .method(receiver.getMethod())
+                            .build());
+
+        }
+    }
+
+    /**
+     * 获取所有在线设备列表
+     *
+     * @return
+     */
+    @ApiOperation("设备列表")
+    @PreAuthorize("@ss.hasPermi('device:dock:list')")
+    @GetMapping("/list")
+    public ResponseResult<List<DeviceDTO>> getDevices(@RequestParam(required = false) String nickname) {
+//        List<DeviceDTO> devicesList = deviceService.getDevicesTopoForWeb();
+        List<DeviceDTO> devicesList = deviceService.getDockList(nickname);
+        return ResponseResult.success(devicesList);
+    }
+
+//    /**
+//     * 获取按nickname筛选在线设备列表
+//     *
+//     * @return
+//     */
+//    @ApiOperation("设备列表-按nickname筛选")
+//    @GetMapping("/cameralist")
+//    public ResponseResult<List<DeviceDTO>> getCameraDevices(@RequestParam(required = false) String nickname) {
+//        List<DeviceDTO> devicesList = deviceService.getDockCameraList(nickname);
+//        return ResponseResult.success(devicesList);
+//    }
+
+    /**
+     * 绑定设备
+     *
+     * @param device
+     * @param deviceSn
+     * @return
+     */
+    @ApiOperation("设备绑定")
+    @PostMapping("/{device_sn}/binding")
+    public ResponseResult bindDevice(@RequestBody DeviceDTO device, @PathVariable("device_sn") String deviceSn) {
+        device.setDeviceSn(deviceSn);
+        boolean isUpd = deviceService.bindDevice(device);
+        return isUpd ? ResponseResult.success() : ResponseResult.error();
+    }
+
+    /**
+     * Obtain device information according to device sn.
+     *
+     * @param deviceSn
+     * @return
+     */
+    @ApiOperation("设备信息")
+    @GetMapping("/{device_sn}")
+    public ResponseResult getDevice(@PathVariable("device_sn") String deviceSn) {
+        Optional<DeviceDTO> deviceOpt = deviceService.getDeviceBySn(deviceSn);
+        return deviceOpt.isEmpty() ? ResponseResult.error("device not found.") :
+                ResponseResult.success(deviceOpt.get());
+    }
+
+    /**
+     * Get the binding devices list
+     * 获取绑定设备列表
+     *
+     * @param page
+     * @param pageSize
+     * @return
+     */
+    @ApiOperation("绑定设备列表")
+    @GetMapping("/list/bound")
+    @PreAuthorize("@ss.hasPermi('device:dock:list')")
+    public ResponseResult<PaginationData<DeviceDTO>> getBoundDevicesWithDomain(Integer domain,
+                                                                               @RequestParam(defaultValue = "1") Long page,
+                                                                               @RequestParam(value = "page_size",
+                                                                                       defaultValue = "50") Long pageSize,
+                                                                               @RequestParam(required = false) Long deptId) {
+        PaginationData<DeviceDTO> devices = deviceService.getBoundDevicesWithDomain(page, pageSize, domain, deptId);
+
+        return ResponseResult.success(devices);
+    }
+
+    /**
+     * Removing the binding state of the device.
+     *
+     * @param deviceSn
+     * @return
+     */
+    @ApiOperation("设备解绑")
+    @DeleteMapping("/{device_sn}/unbinding")
+    public ResponseResult unbindingDevice(@PathVariable("device_sn") String deviceSn) {
+        deviceService.unbindDevice(deviceSn);
+        return ResponseResult.success();
+    }
+
+    /**
+     * Update device information.
+     *
+     * @param device
+     * @param deviceSn
+     * @return
+     */
+    @ApiOperation("设备更新")
+    @PutMapping("/{device_sn}")
+    public ResponseResult updateDevice(@RequestBody DeviceDTO device,
+                                       @PathVariable("device_sn") String deviceSn) {
+        device.setDeviceSn(deviceSn);
+        boolean isUpd = deviceService.updateDevice(device);
+        return isUpd ? ResponseResult.success() : ResponseResult.error();
+    }
+
+    /**
+     * Delivers offline firmware upgrade tasks.
+     *
+     * @param upgradeDTOS
+     * @return
+     */
+    @ApiOperation("固件升级")
+    @PostMapping("/ota")
+    public ResponseResult createOtaJob(@RequestBody List<DeviceFirmwareUpgradeDTO> upgradeDTOS) {
+        return deviceService.createDeviceOtaJob(upgradeDTOS);
+    }
+
+    /**
+     * Set the property parameters of the drone.
+     *
+     * @param dockSn
+     * @param param
+     * @return
+     */
+    @ApiOperation("属性设置")
+    @PutMapping("{device_sn}/property")
+    public ResponseResult devicePropertySet(@PathVariable("device_sn") String dockSn,
+                                            @RequestBody JsonNode param) {
+        if (param.size() != 1) {
+            //return ResponseResult.error(CommonErrorEnum.ILLEGAL_ARGUMENT);
+            return ResponseResult.error(999,"参数不合法");
+        }
+        String property = param.fieldNames().next();
+        Optional<DeviceSetPropertyEnum> propertyEnumOpt = DeviceSetPropertyEnum.find(property);
+        if (propertyEnumOpt.isEmpty()) {
+            //return ResponseResult.error(CommonErrorEnum.ILLEGAL_ARGUMENT);
+            return ResponseResult.error(999,"参数无效");
+        }
+        return deviceService.devicePropertySet(dockSn, propertyEnumOpt.get(), param);
+//        return ResponseResult.success();
+    }
+
+    //.get(property)
+
+//    /**
+//     * 查询机场设备列表
+//     */
+//    @PreAuthorize("@ss.hasPermi('device:camera:list')")
+//    @GetMapping("/test/list")
+//    @ApiOperation("查询机场设备列表")
+//    public TableDataInfo list(DeviceDTO device) {
+//        startPage();
+//        DeviceEntity deviceEntity = deviceDTO2Entity(device);
+//        QueryWrapper<DeviceEntity> queryWrapper=new QueryWrapper<>(deviceEntity);
+//        List<DeviceEntity> list = deviceService.selectList(queryWrapper);
+//        return getDataTable(list);
+//    }
+
+    private DeviceEntity deviceDTO2Entity(DeviceDTO dto) {
+        DeviceEntity.DeviceEntityBuilder builder = DeviceEntity.builder();
+        if (dto == null) {
+            return builder.build();
+        }
+
+        return builder.deviceSn(dto.getDeviceSn())
+                .userId(dto.getUserId())
+                .nickname(dto.getNickname())
+                .boundStatus(dto.getBoundStatus())
+                .loginTime(dto.getLoginTime() != null ?
+                        dto.getLoginTime().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli() : null)
+                .boundTime(dto.getBoundTime() != null ?
+                        dto.getBoundTime().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli() : null)
+                .childSn(dto.getChildDeviceSn())
+                .domain(dto.getDomain())
+                .firmwareVersion(dto.getFirmwareVersion())
+                .workType(dto.getWorkType())
+                .webrtcUrl(dto.getWebrtcUrl())
+                .cameraWebrtcUrl(dto.getCameraWebrtcUrl())
+                .cameraState(dto.getCameraState())
+                .compatibleStatus(dto.getFirmwareStatus() == null ? null :
+                        DeviceFirmwareStatusEnum.CONSISTENT_UPGRADE != DeviceFirmwareStatusEnum.find(dto.getFirmwareStatus()))
+                .build();
+    }
+}
\ No newline at end of file
diff --git a/ard-admin/src/main/java/com/ard/web/controller/sample/dock1/Dock1DeviceFirmwareController.java b/ard-admin/src/main/java/com/ard/web/controller/sample/dock1/Dock1DeviceFirmwareController.java
new file mode 100644
index 0000000..046a292
--- /dev/null
+++ b/ard-admin/src/main/java/com/ard/web/controller/sample/dock1/Dock1DeviceFirmwareController.java
@@ -0,0 +1,111 @@
+/*
+ * 【机场1】从 dock2 原类 com.ard.web.controller.sample.DeviceFirmwareController 复制/独立适配(DJI Dock 1 / M30)。
+ * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。
+ */
+package com.ard.web.controller.sample.dock1;
+
+import com.ard.common.core.domain.model.LoginUser;
+import com.ard.common.utils.SecurityUtils;
+import com.ard.sample.common.model.PaginationData;
+import com.ard.sample.common.model.ResponseResult;
+import com.ard.sample.manage.model.dto.DeviceFirmwareDTO;
+import com.ard.sample.manage.model.dto.DeviceFirmwareNoteDTO;
+import com.ard.sample.manage.model.dto.FirmwareFileProperties;
+import com.ard.sample.manage.model.param.DeviceFirmwareQueryParam;
+import com.ard.sample.manage.model.param.DeviceFirmwareUpdateParam;
+import com.ard.sample.manage.model.param.DeviceFirmwareUploadParam;
+import com.ard.sample.manage.service.IDeviceFirmwareService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.validation.Valid;
+import javax.validation.constraints.NotNull;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import com.ard.web.controller.sample.DeviceFirmwareController;
+
+/**
+ * @author sean
+ * @version 1.2
+ * @date 2022/8/16
+ */
+@RestController
+@RequestMapping("/dock1/workspaces")
+@Validated
+public class Dock1DeviceFirmwareController {
+
+    @Autowired
+    private IDeviceFirmwareService service;
+
+    /**
+     * Get the latest firmware version information for this device model.
+     * @param deviceNames
+     * @return
+     */
+    @GetMapping("/firmware-release-notes/latest")
+    public ResponseResult<List<DeviceFirmwareNoteDTO>> getLatestFirmwareNote(@RequestParam("device_name") List<String> deviceNames) {
+
+        List<DeviceFirmwareNoteDTO> releaseNotes = deviceNames.stream()
+                .map(deviceName -> service.getLatestFirmwareReleaseNote(deviceName))
+                .filter(Optional::isPresent)
+                .map(Optional::get)
+                .collect(Collectors.toList());
+
+        return ResponseResult.success(releaseNotes);
+    }
+
+    /**
+     * Query firmware information based on parameters.
+     * @param workspaceId
+     * @param param
+     * @return
+     */
+    @GetMapping("/firmwares")
+    public ResponseResult<PaginationData<DeviceFirmwareDTO>> getAllFirmwarePagination(@Valid DeviceFirmwareQueryParam param) {
+
+        PaginationData<DeviceFirmwareDTO> data = service.getAllFirmwarePagination(param);
+        return ResponseResult.success(data);
+    }
+
+    /**
+     * Import firmware file for device upgrades.
+     * @param file
+     * @param param
+     * @return
+     */
+    @PostMapping("/firmwares/file/upload")
+    public ResponseResult importFirmwareFile(@NotNull(message = "No file received.") MultipartFile file,
+                                             @Valid DeviceFirmwareUploadParam param) {
+
+        if (!file.getOriginalFilename().endsWith(FirmwareFileProperties.FIRMWARE_FILE_SUFFIX)) {
+            return ResponseResult.error("The file format is incorrect.");
+        }
+        LoginUser loginUser = SecurityUtils.getLoginUser();
+        String creator = loginUser.getUsername();
+
+        service.importFirmwareFile(creator, param, file);
+        return ResponseResult.success();
+    }
+
+    /**
+     * Change the firmware availability status.
+     * @param workspaceId
+     * @param firmwareId
+     * @param param
+     * @return
+     */
+    @PutMapping("/{workspace_id}/firmwares/{firmware_id}")
+    public ResponseResult changeFirmwareStatus(@PathVariable("workspace_id") String workspaceId,
+                                             @PathVariable("firmware_id") String firmwareId,
+                                             @Valid @RequestBody DeviceFirmwareUpdateParam param) {
+
+        service.updateFirmwareInfo(DeviceFirmwareDTO.builder()
+                .firmwareId(firmwareId).firmwareStatus(param.getStatus()).build());
+        return ResponseResult.success();
+    }
+
+
+}
diff --git a/ard-admin/src/main/java/com/ard/web/controller/sample/dock1/Dock1DeviceHmsController.java b/ard-admin/src/main/java/com/ard/web/controller/sample/dock1/Dock1DeviceHmsController.java
new file mode 100644
index 0000000..5c47330
--- /dev/null
+++ b/ard-admin/src/main/java/com/ard/web/controller/sample/dock1/Dock1DeviceHmsController.java
@@ -0,0 +1,82 @@
+/*
+ * 【机场1】从 dock2 原类 com.ard.web.controller.sample.DeviceHmsController 复制/独立适配(DJI Dock 1 / M30)。
+ * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。
+ */
+package com.ard.web.controller.sample.dock1;
+
+import com.ard.sample.common.model.PaginationData;
+import com.ard.sample.common.model.ResponseResult;
+import com.ard.sample.manage.model.dto.DeviceHmsDTO;
+import com.ard.sample.manage.model.param.DeviceHmsQueryParam;
+import com.ard.sample.manage.service.IDeviceHmsService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.v3.oas.annotations.Operation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import com.ard.web.controller.sample.DeviceHmsController;
+
+/**
+ * @author sean
+ * @version 1.1
+ * @date 2022/7/7
+ */
+@Api(tags = "设备HMS管理")
+@RestController
+@Slf4j
+@RequestMapping("/dock1/devices")
+public class Dock1DeviceHmsController {
+
+    @Autowired
+    private IDeviceHmsService deviceHmsService;
+
+    /**
+     * 查询设备hms信息的页面。/{workspace_id},@PathVariable("workspace_id") String workspaceId
+     * @param param
+     * @return
+     */
+    @Operation(summary = "获取设备HMS信息", description = "参数传空则忽略该条件,返回所有符合其他条件的数据")
+    @ApiOperation("获取设备HMS信息")
+    @GetMapping("/devices/hms")
+    public ResponseResult<PaginationData<DeviceHmsDTO>> getHmsInformation(DeviceHmsQueryParam param) {
+        if (param == null) {
+            param = new DeviceHmsQueryParam();
+        }
+        PaginationData<DeviceHmsDTO> devices = deviceHmsService.getDeviceHmsByParam(param);
+
+        return ResponseResult.success(devices);
+    }
+
+    /**
+     * Update unread hms messages to read status.
+     * @param deviceSn
+     * @return
+     */
+    @ApiOperation("更新HMS消息为已读状态")
+    @PutMapping("/devices/hms/{device_sn}")
+    public ResponseResult updateReadHmsByDeviceSn(@PathVariable("device_sn") String deviceSn) {
+        deviceHmsService.updateUnreadHms(deviceSn);
+        return ResponseResult.success();
+    }
+
+    /**
+     * Get hms messages for a single device.
+     * @param deviceSn
+     * @return
+     */
+    @ApiOperation("获取设备未读HMS消息")
+    @GetMapping("/devices/hms/{device_sn}")
+    public ResponseResult<List<DeviceHmsDTO>> getUnreadHmsByDeviceSn(@PathVariable("device_sn") String deviceSn) {
+        PaginationData<DeviceHmsDTO> paginationData = deviceHmsService.getDeviceHmsByParam(
+                DeviceHmsQueryParam.builder()
+                        .deviceSn(new HashSet<>(Set.of(deviceSn)))
+                        .updateTime(0L)
+                        .build());
+        return ResponseResult.success(paginationData.getList());
+    }
+}
diff --git a/ard-admin/src/main/java/com/ard/web/controller/sample/dock1/Dock1DeviceLogsController.java b/ard-admin/src/main/java/com/ard/web/controller/sample/dock1/Dock1DeviceLogsController.java
new file mode 100644
index 0000000..3306a39
--- /dev/null
+++ b/ard-admin/src/main/java/com/ard/web/controller/sample/dock1/Dock1DeviceLogsController.java
@@ -0,0 +1,124 @@
+/*
+ * 【机场1】从 dock2 原类 com.ard.web.controller.sample.DeviceLogsController 复制/独立适配(DJI Dock 1 / M30)。
+ * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。
+ */
+package com.ard.web.controller.sample.dock1;
+
+import com.ard.common.core.domain.model.LoginUser;
+import com.ard.common.utils.SecurityUtils;
+import com.ard.sample.common.model.PaginationData;
+import com.ard.sample.common.model.ResponseResult;
+import com.ard.sample.manage.model.dto.DeviceLogsDTO;
+import com.ard.sample.manage.model.param.DeviceLogsCreateParam;
+import com.ard.sample.manage.model.param.DeviceLogsQueryParam;
+import com.ard.sample.manage.model.param.LogsFileUpdateParam;
+import com.ard.sample.manage.service.IDeviceLogsService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.net.URL;
+import java.util.List;
+import com.ard.web.controller.sample.DeviceLogsController;
+
+/**
+ * @author sean
+ * @version 1.2
+ * @date 2022/9/7
+ */
+@RestController
+@Slf4j
+@RequestMapping("/dock1/workspaces")
+public class Dock1DeviceLogsController {
+
+    @Autowired
+    private IDeviceLogsService deviceLogsService;
+
+    /**
+     * Obtain the device upload log list by paging according to the query parameters.
+     * @param workspaceId
+     * @param deviceSn
+     * @param param
+     * @return
+     */
+    @GetMapping("/{workspace_id}/devices/{device_sn}/logs-uploaded")
+    public ResponseResult getUploadedLogs(DeviceLogsQueryParam param, @PathVariable("workspace_id") String workspaceId,
+                                          @PathVariable("device_sn") String deviceSn) {
+        PaginationData<DeviceLogsDTO> data = deviceLogsService.getUploadedLogs(deviceSn, param);
+        return ResponseResult.success(data);
+    }
+
+    /**
+     * Get a list of log files that can be uploaded in real time.
+     * @param workspaceId
+     * @param deviceSn
+     * @param domainList
+     * @return
+     */
+    @GetMapping("/{workspace_id}/devices/{device_sn}/logs")
+    public ResponseResult getLogsBySn(@PathVariable("workspace_id") String workspaceId,
+                                      @PathVariable("device_sn") String deviceSn,
+                                      @RequestParam("domain_list") List<String> domainList) {
+        return deviceLogsService.getRealTimeLogs(deviceSn, domainList);
+    }
+
+    /**
+     * Initiate a log upload request to the gateway.
+     * @return
+     */
+    @PostMapping("/{workspace_id}/devices/{device_sn}/logs")
+    public ResponseResult uploadLogs(@PathVariable("workspace_id") String workspaceId,
+                                     @PathVariable("device_sn") String deviceSn,
+                                     HttpServletRequest request, @RequestBody DeviceLogsCreateParam param) {
+
+        LoginUser loginUser = SecurityUtils.getLoginUser();
+        return deviceLogsService.pushFileUpload(loginUser.getUsername(), deviceSn, param);
+    }
+
+    /**
+     * Cancel logs file upload.
+     * @return
+     */
+    @DeleteMapping("/{workspace_id}/devices/{device_sn}/logs")
+    public ResponseResult cancelUploadedLogs(@PathVariable("workspace_id") String workspaceId,
+                                             @PathVariable("device_sn") String deviceSn,
+                                             @RequestBody LogsFileUpdateParam param) {
+
+        return deviceLogsService.pushUpdateFile(deviceSn, param);
+    }
+
+    /**
+     * Delete upload history.
+     * @return
+     */
+    @DeleteMapping("/{workspace_id}/devices/{device_sn}/logs/{logs_id}")
+    public ResponseResult deleteUploadedLogs(@PathVariable("workspace_id") String workspaceId,
+                                             @PathVariable("device_sn") String deviceSn,
+                                             @PathVariable("logs_id") String logsId) {
+        deviceLogsService.deleteLogs(deviceSn, logsId);
+        return ResponseResult.success();
+    }
+    /**
+     * Query the download address of the file according to the wayline file id,
+     * and redirect to this address directly for download.
+     * @param workspaceId
+     * @param fileId
+     * @param logsId
+     * @param response
+     */
+    @GetMapping("/{workspace_id}/logs/{logs_id}/url/{file_id}")
+    public ResponseResult getFileUrl(@PathVariable(name = "workspace_id") String workspaceId,
+                            @PathVariable(name = "file_id") String fileId,
+                           @PathVariable(name = "logs_id") String logsId, HttpServletResponse response) {
+
+        try {
+            URL url = deviceLogsService.getLogsFileUrl(logsId, fileId);
+            return ResponseResult.success(url.toString());
+        } catch (Exception e) {
+            log.error("Failed to get the logs file download address.");
+            e.printStackTrace();
+        }
+        return ResponseResult.error("Failed to get the logs file download address.");
+    }
+}
diff --git a/ard-admin/src/main/java/com/ard/web/controller/sample/dock1/Dock1DevicePayloadController.java b/ard-admin/src/main/java/com/ard/web/controller/sample/dock1/Dock1DevicePayloadController.java
new file mode 100644
index 0000000..0f6a278
--- /dev/null
+++ b/ard-admin/src/main/java/com/ard/web/controller/sample/dock1/Dock1DevicePayloadController.java
@@ -0,0 +1,39 @@
+/*
+ * 【机场1】从 dock2 原类 com.ard.web.controller.sample.DevicePayloadController 复制/独立适配(DJI Dock 1 / M30)。
+ * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。
+ */
+package com.ard.web.controller.sample.dock1;
+
+import com.ard.sample.component.mqtt.model.ChannelName;
+import com.ard.sample.manage.model.receiver.DeviceBasicReceiver;
+import com.ard.sample.manage.service.IDevicePayloadService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.messaging.MessageHeaders;
+import org.springframework.web.bind.annotation.RestController;
+import com.ard.web.controller.sample.DevicePayloadController;
+
+/**
+ * @author sean.zhou
+ * @date 2021/11/19
+ * @version 0.1
+ */
+@RestController
+public class Dock1DevicePayloadController {
+
+    @Autowired
+    private IDevicePayloadService devicePayloadService;
+
+    /**
+     * Handles messages in the state topic about basic drone data.
+     *
+     * Note: Only the data of the drone payload is handled here. You can handle other data from the drone
+     * according to your business needs.
+     * @param deviceBasic   basic drone data
+     * @param headers
+     */
+    // 机场1:不复制原 ServiceActivator(inputChannel = ChannelName.INBOUND_STATE_BASIC),消费职责由共享层原类承担
+    public void stateBasic(DeviceBasicReceiver deviceBasic, MessageHeaders headers) {
+        //  由于收到的负载数据的payloadSn为null 而且同时收到2个 暂时无法处理 先注释掉 以后再说
+       // devicePayloadService.saveDeviceBasicPayload(deviceBasic.getPayloads(), headers.getTimestamp());
+    }
+}
\ No newline at end of file
diff --git a/ard-admin/src/main/java/com/ard/web/controller/sample/dock1/Dock1DockController.java b/ard-admin/src/main/java/com/ard/web/controller/sample/dock1/Dock1DockController.java
new file mode 100644
index 0000000..9d33b09
--- /dev/null
+++ b/ard-admin/src/main/java/com/ard/web/controller/sample/dock1/Dock1DockController.java
@@ -0,0 +1,43 @@
+/*
+ * 【机场1】从 dock2 原类 com.ard.web.controller.sample.DockController 复制/独立适配(DJI Dock 1 / M30)。
+ * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。
+ */
+package com.ard.web.controller.sample.dock1;
+
+import com.ard.common.core.domain.model.LoginUser;
+import com.ard.common.utils.SecurityUtils;
+import com.ard.sample.common.model.ResponseResult;
+import com.ard.sample.manage.model.param.RemoteDebugParam;
+import com.ard.sample.dock1.manage.service.IControlService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+import com.ard.web.controller.sample.DockController;
+
+/**
+ * @author sean
+ * @version 1.2
+ * @date 2022/7/29
+ */
+@Api(tags = "机场控制")
+@RestController
+@Slf4j
+@RequestMapping("/dock1/devices")
+public class Dock1DockController {
+
+    @Autowired
+    private IControlService controlService;
+
+    @ApiOperation("创建控制任务")
+    @PostMapping("/{sn}/jobs/{service_identifier}")
+    public ResponseResult createControlJob(@PathVariable String sn,
+                                           @PathVariable("service_identifier") String serviceIdentifier,
+                                           @RequestBody(required = false) RemoteDebugParam param) {
+        // 获取谁在调接口,后续可能要websocket发给前端在多路直播中显示
+        //LoginUser loginUser = SecurityUtils.getLoginUser();
+        return controlService.controlDock(sn, serviceIdentifier, param);
+    }
+
+}
diff --git a/ard-admin/src/main/java/com/ard/web/controller/sample/dock1/Dock1LiveStreamController.java b/ard-admin/src/main/java/com/ard/web/controller/sample/dock1/Dock1LiveStreamController.java
new file mode 100644
index 0000000..26afd8b
--- /dev/null
+++ b/ard-admin/src/main/java/com/ard/web/controller/sample/dock1/Dock1LiveStreamController.java
@@ -0,0 +1,163 @@
+/*
+ * 【机场1】从 dock2 原类 com.ard.web.controller.sample.LiveStreamController 复制/独立适配(DJI Dock 1 / M30)。
+ * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。
+ */
+package com.ard.web.controller.sample.dock1;
+
+import com.ard.common.core.domain.entity.SysUser;
+import com.ard.common.utils.SecurityUtils;
+import com.ard.sample.common.model.ResponseResult;
+import com.ard.sample.component.mqtt.model.ChannelName;
+import com.ard.sample.component.websocket.handler.LiveStreamHandler;
+import com.ard.sample.manage.model.dto.CapacityDeviceDTO;
+import com.ard.sample.manage.model.dto.LiveTypeDTO;
+import com.ard.sample.manage.model.param.RemoteDebugParam;
+import com.ard.sample.manage.model.receiver.LiveCapacityReceiver;
+import com.ard.sample.manage.model.receiver.LiveStatusTwoReceiver;
+import com.ard.sample.manage.service.IDeviceService;
+import com.ard.sample.dock1.manage.service.ILiveStreamService;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.messaging.MessageHeaders;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.List;
+import com.ard.web.controller.sample.LiveStreamController;
+
+/**
+ * @author sean.zhou
+ * @version 0.1
+ * @date 2021/11/19
+ */
+
+@Api(tags = "直播功能")
+@RestController
+@Slf4j
+@RequestMapping("/dock1/live")
+public class Dock1LiveStreamController {
+
+    @Autowired
+    private ILiveStreamService liveStreamService;
+
+    @Autowired
+    private LiveStreamHandler _liveStreamHandler;
+
+    @Autowired
+    private ObjectMapper mapper;
+
+    /**
+     * Analyze the live streaming capabilities of drones.
+     * This data is necessary if drones are required for live streaming.
+     *
+     * @param liveCapacity the capacity of drone and dock
+     */
+    // 机场1:不复制原 ServiceActivator(inputChannel = ChannelName.INBOUND_STATE_CAPACITY),消费职责由共享层原类承担
+    public void stateCapacity(LiveCapacityReceiver liveCapacity, MessageHeaders headers) {
+        liveStreamService.saveLiveCapacity(liveCapacity, headers.getTimestamp());
+    }
+
+
+    /**
+     * 处理摄像头返回状态
+     * */
+    // 机场1:不复制原 ServiceActivator(inputChannel = ChannelName.INBOUND_STATE_STATUS),消费职责由共享层原类承担
+    public void LiveStateStatus(LiveStatusTwoReceiver liveReceiver, MessageHeaders headers) {
+//        log.info("摄像头返回状态通道INBOUND_STATE_STATUS:{}", liveReceiver);
+    }
+
+    /**
+     * Get live capability data of all drones in the current user's workspace from the database.
+     *
+     * @param request
+     * @return live capability
+     */
+    @ApiOperation(value = "获取直播能力数据", notes = "获取当前用户工作空间下所有无人机的直播能力数据")
+//    @PreAuthorize("@ss.hasPermi('dock:live:capacity')")
+    @GetMapping("/capacity")
+    public ResponseResult<List<CapacityDeviceDTO>> getLiveCapacity(HttpServletRequest request) {
+        // Get information about the current user.
+        List<CapacityDeviceDTO> liveCapacity = liveStreamService.getLiveCapacity();
+
+        return ResponseResult.success(liveCapacity);
+    }
+
+    @ApiOperation(value = "获取sn设备的摄像头模式", notes = "获取sn设备的直播能力数据中摄像头的videotype数组")
+    @GetMapping("/videotype")
+    public ResponseResult<List<String>> getVideoType(String sn){
+        return liveStreamService.getLiveLens(sn);
+    }
+
+    @ApiOperation("获取video_id")
+    @PostMapping("/videoid/{sn}")
+    public ResponseResult<LiveTypeDTO> getVideoId(@PathVariable String sn) {
+        LiveTypeDTO liveTypeDTO = new LiveTypeDTO();
+        try{
+            liveTypeDTO = _liveStreamHandler.getLiveParam(sn);
+
+        }
+        catch (Exception e){
+            // videoId = e.getMessage();
+            e.printStackTrace();
+        }
+        return ResponseResult.success(liveTypeDTO);
+    }
+
+    /**
+     * Live streaming according to the parameters passed in from the web side.
+     *
+     * @param liveParam Live streaming parameters.
+     * @return
+     */
+    @ApiOperation(value = "开始直播", notes = "根据前端传入的参数开始直播")
+//    @PreAuthorize("@ss.hasPermi('dock:live:start')")
+    @PostMapping("/streams/start")
+    public ResponseResult liveStart(@RequestBody LiveTypeDTO liveParam) {
+        return liveStreamService.liveStart(liveParam);
+    }
+
+    /**
+     * Stop live streaming according to the parameters passed in from the web side.
+     *
+     * @param liveParam Live streaming parameters.
+     * @return
+     */
+    @ApiOperation(value = "停止直播", notes = "根据前端传入的参数停止直播")
+//    @PreAuthorize("@ss.hasPermi('dock:live:stop')")
+    @PostMapping("/streams/stop")
+    public ResponseResult liveStop(@RequestBody LiveTypeDTO liveParam) {
+        return liveStreamService.liveStop(liveParam.getVideoId());
+    }
+
+    /**
+     * Set the quality of the live streaming according to the parameters passed in from the web side.
+     *
+     * @param liveParam Live streaming parameters.
+     * @return
+     */
+    @ApiOperation(value = "设置直播质量", notes = "根据前端传入的参数设置直播质量")
+//    @PreAuthorize("@ss.hasPermi('dock:live:quality')")
+    @PostMapping("/quality/switch")
+    public ResponseResult liveSetQuality(@RequestBody LiveTypeDTO liveParam) {
+        return liveStreamService.liveSetQuality(liveParam);
+    }
+
+    @ApiOperation(value = "切换镜头", notes = "根据前端传入的参数切换镜头")
+//    @PreAuthorize("@ss.hasPermi('dock:live:lens')")
+    @PostMapping("/lens/switch")
+    public ResponseResult liveLensChange(@RequestBody LiveTypeDTO liveParam) {
+        return liveStreamService.liveLensChange(liveParam);
+    }
+
+    @ApiOperation(value = "机场直播相机切换", notes = "根据前端传入的参数切换相机")
+//    @PreAuthorize("@ss.hasPermi('dock:live:camera')")
+    @PostMapping("/camera/switch")
+    public ResponseResult liveCameraChange(@RequestBody LiveTypeDTO liveParam) {
+        return liveStreamService.liveCameraChange(liveParam);
+    }
+
+}
\ No newline at end of file
diff --git a/ard-admin/src/main/java/com/ard/web/controller/sample/dock1/Dock1TopologyController.java b/ard-admin/src/main/java/com/ard/web/controller/sample/dock1/Dock1TopologyController.java
new file mode 100644
index 0000000..7321356
--- /dev/null
+++ b/ard-admin/src/main/java/com/ard/web/controller/sample/dock1/Dock1TopologyController.java
@@ -0,0 +1,45 @@
+/*
+ * 【机场1】从 dock2 原类 com.ard.web.controller.sample.TopologyController 复制/独立适配(DJI Dock 1 / M30)。
+ * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。
+ */
+package com.ard.web.controller.sample.dock1;
+
+import com.ard.sample.common.model.ResponseResult;
+import com.ard.sample.manage.model.dto.TopologyDTO;
+import com.ard.sample.manage.service.ITopologyService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import com.ard.web.controller.sample.TopologyController;
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/12/8
+ */
+@RestController
+@RequestMapping("/dock1/workspaces")
+public class Dock1TopologyController {
+
+    @Autowired
+    private ITopologyService topologyService;
+
+    /**
+     * Get the topology list of all devices in the current user workspace for pilot display.
+     * @param workspaceId
+     * @return
+     */
+    @GetMapping("/{workspace_id}/devices/topologies")
+    public ResponseResult<Map<String, List<TopologyDTO>>> getDevicesTopologiesForPilot(
+            @PathVariable(name = "workspace_id") String workspaceId) {
+        List<TopologyDTO> topologyList = topologyService.getDeviceTopology(workspaceId);
+        return ResponseResult.success(new ConcurrentHashMap<>(Map.of("list", topologyList)));
+    }
+
+}
diff --git a/ard-admin/src/main/java/com/ard/web/controller/sample/dock1/media/Dock1FileController.java b/ard-admin/src/main/java/com/ard/web/controller/sample/dock1/media/Dock1FileController.java
new file mode 100644
index 0000000..f4fbcb3
--- /dev/null
+++ b/ard-admin/src/main/java/com/ard/web/controller/sample/dock1/media/Dock1FileController.java
@@ -0,0 +1,95 @@
+/*
+ * 【机场1】从 dock2 原类 com.ard.web.controller.sample.media.FileController 复制/独立适配(DJI Dock 1 / M30)。
+ * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。
+ */
+package com.ard.web.controller.sample.dock1.media;
+
+import com.ard.sample.common.model.PaginationData;
+import com.ard.sample.common.model.ResponseResult;
+import com.ard.sample.media.model.MediaFileDTO;
+import com.ard.sample.media.service.IFileService;
+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.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.net.URL;
+import com.ard.web.controller.sample.media.FileController;
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/12/9
+ */
+@Api(tags = "媒体文件管理")
+@RestController
+@RequestMapping("/dock1/media/files")
+public class Dock1FileController {
+
+    @Autowired
+    private IFileService fileService;
+
+    /**
+     * Get information about all the media files in this workspace based on the workspace id.
+     * @return
+     */
+    @ApiOperation("获取所有媒体文件列表")
+    @GetMapping("/files")
+    public ResponseResult<PaginationData<MediaFileDTO>> getFilesList(@RequestParam(defaultValue = "1") Long page,
+                               @RequestParam(name = "page_size", defaultValue = "10") Long pageSize,
+                               @ApiParam(value = "任务ID,筛选指定任务下的文件", required = false)
+                               @RequestParam(name = "job_id", required = false) String jobId,
+                               @ApiParam(value = "开始时间", required = false)
+                               @RequestParam(name = "start_time", required = false) Long startTime,
+                               @ApiParam(value = "结束时间", required = false)
+                               @RequestParam(name = "end_time", required = false) Long endTime) {
+        PaginationData<MediaFileDTO> filesList = fileService.getMediaFilesPagination(page, pageSize, jobId, startTime, endTime);
+        return ResponseResult.success(filesList);
+    }
+
+    /**
+     * Query the download address of the file according to the media file id,
+     * and redirect to this address directly for download.
+     * @param fileId
+     * @param response
+     */
+    @ApiOperation("根据id获取媒体文件地址")
+    @GetMapping("/file/{file_id}/url")
+    public void getFileUrl(@PathVariable(name = "file_id") String fileId, HttpServletResponse response) {
+
+        try {
+            URL url = fileService.getObjectUrl(fileId);
+            response.sendRedirect(url.toString());
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * 根据任务id获取全部媒体库文件列表
+     *
+     * @param page     页码,默认值为1,从1开始计数
+     * @param pageSize 每页条数,默认值为10,参数名映射为page_size(前端传参用)
+     * @param jobId    任务ID,必填参数,用于筛选指定任务下的媒体文件
+     * @param isType   文件类型筛选标识,非必填(若需必填可加required = true):
+     *                 1 - 筛选图片文件(按 .jpeg/.jpg 扩展名,兼容各机型镜头前缀 W/Z/T/V);
+     *                 0 - 筛选视频文件(按 .mp4 扩展名)
+     * @return 分页的媒体文件列表数据
+     * */
+    @ApiOperation("获取任务中媒体文件列表")
+    @GetMapping("/jobfiles")
+    public ResponseResult<PaginationData<MediaFileDTO>> getJobFiles(@ApiParam(value = "页码,默认1", defaultValue = "1")
+                                                                    @RequestParam(defaultValue = "1") Long page,
+                                                                    @ApiParam(value = "每页条数,默认10", defaultValue = "10")
+                                                                    @RequestParam(name = "page_size", defaultValue = "10") Long pageSize,
+                                                                    @ApiParam(value = "任务ID(必填),筛选指定任务下的文件", required = true)
+                                                                    @RequestParam(name = "job_id") String jobId,
+                                                                    @ApiParam(value = "文件类型筛选:1=图片文件,0=视频文件")
+                                                                    @RequestParam(name = "isType") String isType) {
+        PaginationData<MediaFileDTO> filesList = fileService.getJobFiles(page, pageSize, jobId, isType);
+        return ResponseResult.success(filesList);
+    }
+}
diff --git a/ard-admin/src/main/java/com/ard/web/controller/sample/dock1/media/Dock1MediaController.java b/ard-admin/src/main/java/com/ard/web/controller/sample/dock1/media/Dock1MediaController.java
new file mode 100644
index 0000000..6301b00
--- /dev/null
+++ b/ard-admin/src/main/java/com/ard/web/controller/sample/dock1/media/Dock1MediaController.java
@@ -0,0 +1,80 @@
+/*
+ * 【机场1】从 dock2 原类 com.ard.web.controller.sample.media.MediaController 复制/独立适配(DJI Dock 1 / M30)。
+ * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。
+ */
+package com.ard.web.controller.sample.dock1.media;
+
+import com.ard.sample.common.model.ResponseResult;
+import com.ard.sample.component.mqtt.model.MapKeyConst;
+import com.ard.sample.media.model.FileUploadDTO;
+import com.ard.sample.media.service.IMediaService;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import com.ard.web.controller.sample.media.MediaController;
+
+/**
+ * @author sean/workspaces
+ * @version 0.2
+ * @date 2021/12/7
+ */
+@Api(tags = "媒体库管理")
+@Slf4j
+@RestController
+@RequestMapping("/dock1/media")
+public class Dock1MediaController {
+
+    @Autowired
+    private IMediaService mediaService;
+
+    /**
+     * Check if the file has been uploaded by the fingerprint.
+     * @param file
+     * @return
+     */
+    @ApiOperation("检查文件是否已上传")
+    @PostMapping("/fast-upload")
+    public ResponseResult fastUpload( @RequestBody FileUploadDTO file) {
+
+        boolean isExist = mediaService.fastUpload(file.getFingerprint());
+
+        return isExist ? ResponseResult.success() : ResponseResult.error(file.getFingerprint() + "don't exist.");
+    }
+
+    /**
+     * When the file is uploaded to the storage server by pilot,
+     * the basic information of the file is reported through this interface.
+     * @param file
+     * @return
+     */
+    @ApiOperation("上报文件基本信息")
+    @PostMapping("/upload-callback")
+    public ResponseResult<String> uploadCallback(@RequestBody FileUploadDTO file) {
+        mediaService.saveMediaFile(file);
+        return ResponseResult.success(file.getObjectKey());
+
+    }
+
+    /**
+     * 根据工作区 ID 和微小指纹的集合查询此工作区中已存在的文件。
+     * @param tinyFingerprints  There is only one tiny_fingerprint parameter in the body.
+     *                          But it is not recommended to use Map to receive the parameter.
+     * @return
+     */
+    @ApiOperation("查询已存在的文件")
+    @PostMapping("/files/tiny-fingerprints")
+    public ResponseResult<Map<String, List<String>>> uploadCallback(
+                               @RequestBody Map<String, List<String>> tinyFingerprints){
+
+        List<String> existingList = mediaService.getExistTinyFingerprints(tinyFingerprints.get(MapKeyConst.TINY_FINGERPRINTS));
+        return ResponseResult.success(new ConcurrentHashMap<>(Map.of(MapKeyConst.TINY_FINGERPRINTS, existingList)));
+    }
+
+}
\ No newline at end of file
diff --git a/ard-admin/src/main/java/com/ard/web/controller/sample/dock1/wayline/Dock1WaylineFileController.java b/ard-admin/src/main/java/com/ard/web/controller/sample/dock1/wayline/Dock1WaylineFileController.java
new file mode 100644
index 0000000..1fdf8cc
--- /dev/null
+++ b/ard-admin/src/main/java/com/ard/web/controller/sample/dock1/wayline/Dock1WaylineFileController.java
@@ -0,0 +1,170 @@
+/*
+ * 【机场1】从 dock2 原类 com.ard.web.controller.sample.wayline.WaylineFileController 复制/独立适配(DJI Dock 1 / M30)。
+ * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。
+ */
+package com.ard.web.controller.sample.dock1.wayline;
+
+import com.ard.common.constant.HttpStatus;
+import com.ard.common.core.controller.BaseController;
+import com.ard.common.core.domain.model.LoginUser;
+import com.ard.common.core.page.TableDataInfo;
+import com.ard.common.utils.SecurityUtils;
+import com.ard.sample.common.model.PaginationData;
+import com.ard.sample.common.model.ResponseResult;
+import com.ard.sample.wayline.model.dto.WaylineFileDTO;
+import com.ard.sample.wayline.model.dto.WaylineFileUploadDTO;
+import com.ard.sample.wayline.model.param.WaylineQueryParam;
+import com.ard.sample.dock1.wayline.service.IWaylineFileService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.net.URL;
+import java.sql.SQLException;
+import java.util.List;
+import java.util.Objects;
+import com.ard.web.controller.sample.wayline.WaylineFileController;
+
+/**
+ * @author sean
+ * @version 0.3
+ * @date 2021/12/22
+ */
+@Api(tags="航线管理")
+@RestController
+@RequestMapping("/dock1/wayline")
+public class Dock1WaylineFileController extends BaseController {
+
+    @Autowired
+    private IWaylineFileService waylineFileService;
+
+    /**
+     * Query the basic data of the wayline file according to the query conditions.
+     * The query condition field in pilot is fixed.
+     * @return
+     */
+    @ApiOperation("查询航线列表")
+    @GetMapping("/list")
+    public TableDataInfo getWaylinesPagination(WaylineQueryParam param) {
+
+        PaginationData<WaylineFileDTO> data = waylineFileService.getWaylinesByParam(param);
+
+        TableDataInfo rspData = new TableDataInfo();
+        rspData.setCode(HttpStatus.SUCCESS);
+        rspData.setMsg("查询成功");
+        rspData.setRows(data.getList());
+        rspData.setTotal(data.getPagination().getTotal());
+        return rspData;
+    }
+
+    /**
+     * Query the download address of the file according to the wayline file id,
+     * and redirect to this address directly for download.
+     * @param waylineId
+     * @param response
+     */
+    @ApiOperation("下载航线文件")
+    @GetMapping("/download")
+    public void getFileUrl(String waylineId, HttpServletResponse response) {
+        try {
+            URL url = waylineFileService.getObjectUrl(waylineId);
+            response.sendRedirect(url.toString());
+
+        } catch (IOException | SQLException e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * When the wayline file is uploaded to the storage server by pilot,
+     * the basic information of the file is reported through this interface.
+     * @param uploadFile
+     * @return
+     */
+    @ApiOperation("上传航线")
+    @PostMapping("/upload")
+    public ResponseResult uploadCallBack(@RequestBody WaylineFileUploadDTO uploadFile) {
+
+        LoginUser loginUser = SecurityUtils.getLoginUser();
+        WaylineFileDTO metadata = uploadFile.getMetadata();
+        metadata.setUsername(loginUser.getUsername());
+        metadata.setObjectKey(uploadFile.getObjectKey());
+        metadata.setName(uploadFile.getName());
+
+        int id = waylineFileService.saveWaylineFile(metadata);
+
+        return id <= 0 ? ResponseResult.error() : ResponseResult.success();
+    }
+
+    /**
+     * Favorite the wayline file according to the wayline file id.
+     * @param ids   wayline file id
+     * @return
+     */
+    @ApiOperation("收藏航线")
+    @PostMapping("/favorites")
+    public ResponseResult markFavorite(@RequestParam(name = "id") List<String> ids) {
+        boolean isMark = waylineFileService.markFavorite(ids, true);
+
+        return isMark ? ResponseResult.success() : ResponseResult.error();
+    }
+
+    /**
+     * Delete the favorites of this wayline file based on the wayline file id.
+     * @param ids wayline file id
+     * @return
+     */
+    @ApiOperation("取消收藏航线")
+    @DeleteMapping("/favorites")
+    public ResponseResult unmarkFavorite(@RequestParam(name = "id") List<String> ids) {
+        boolean isMark = waylineFileService.markFavorite(ids, false);
+
+        return isMark ? ResponseResult.success() : ResponseResult.error();
+    }
+
+    /**
+     * Checking whether the name already exists according to the wayline name must ensure the uniqueness of the wayline name.
+     * This interface will be called when uploading waylines and must be available.
+     * @param names
+     * @return
+     */
+    @ApiOperation("检查航线名称是否重复")
+    @GetMapping("/checkDuplicateNames")
+    public ResponseResult checkDuplicateNames(@RequestParam(name = "name") List<String> names) {
+        List<String> existNamesList = waylineFileService.getDuplicateNames(names);
+        return ResponseResult.success(existNamesList);
+    }
+
+    /**
+     * Delete the wayline file in the workspace according to the wayline id.
+     * @param waylineId
+     * @return
+     */
+    @ApiOperation("删除航线")
+    @DeleteMapping("/delete/{wayline_id}")
+    public ResponseResult deleteWayline(@PathVariable(name = "wayline_id") String waylineId) {
+        boolean isDel = waylineFileService.deleteByWaylineId(waylineId);
+        return isDel ? ResponseResult.success() : ResponseResult.error("Failed to delete wayline.");
+    }
+
+    /**
+     * Import kmz wayline files.
+     * @param file
+     * @return
+     */
+    @ApiOperation("导入航线")
+    @PostMapping("/importKmzFile")
+    public ResponseResult importKmzFile(@RequestPart("file") MultipartFile file) {
+        if (Objects.isNull(file)) {
+            return ResponseResult.error("No file received.");
+        }
+        LoginUser loginUser = SecurityUtils.getLoginUser();
+        String creator = loginUser.getUsername();
+        waylineFileService.importKmzFile(file, creator);
+        return ResponseResult.success();
+    }
+}
diff --git a/ard-admin/src/main/java/com/ard/web/controller/sample/dock1/wayline/Dock1WaylineJobController.java b/ard-admin/src/main/java/com/ard/web/controller/sample/dock1/wayline/Dock1WaylineJobController.java
new file mode 100644
index 0000000..273a0bc
--- /dev/null
+++ b/ard-admin/src/main/java/com/ard/web/controller/sample/dock1/wayline/Dock1WaylineJobController.java
@@ -0,0 +1,116 @@
+/*
+ * 【机场1】从 dock2 原类 com.ard.web.controller.sample.wayline.WaylineJobController 复制/独立适配(DJI Dock 1 / M30)。
+ * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。
+ */
+package com.ard.web.controller.sample.dock1.wayline;
+
+import com.ard.common.core.domain.model.LoginUser;
+import com.ard.common.utils.SecurityUtils;
+import com.ard.sample.common.model.PaginationData;
+import com.ard.sample.common.model.ResponseResult;
+import com.ard.sample.wayline.model.dto.WaylineJobDTO;
+import com.ard.sample.wayline.model.param.CreateJobParam;
+import com.ard.sample.dock1.wayline.service.IWaylineJobService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+import javax.validation.Valid;
+import java.sql.SQLException;
+import java.util.Optional;
+import java.util.Set;
+import com.ard.web.controller.sample.wayline.WaylineJobController;
+
+/**
+ * @author sean
+ * @version 1.1
+ * @date 2022/6/1
+ */
+@Api(tags = "航线任务")
+@RequestMapping("/dock1/wayline")
+@RestController
+public class Dock1WaylineJobController {
+
+    @Autowired
+    private IWaylineJobService waylineJobService;
+
+    /**
+     * Create a wayline task for the Dock.
+     * @param param
+     * @return
+     * @throws SQLException
+     */
+    @ApiOperation("创建航线任务")
+    @PostMapping("/flight-tasks")
+    public ResponseResult createJob(@Valid @RequestBody CreateJobParam param) throws SQLException {
+        LoginUser loginUser = SecurityUtils.getLoginUser();
+        return waylineJobService.publishFlightTask(param, loginUser);
+    }
+
+    @ApiOperation("获取航线任务")
+    @GetMapping("/flight-id")
+    public ResponseResult<WaylineJobDTO> getJob(@RequestParam(name = "job_id") String jobId) {
+        try {
+            if(jobId == null){
+                return ResponseResult.error(98,"任务ID不能为空");
+            }
+            Optional<WaylineJobDTO> waylineJobDTO = waylineJobService.getJobByJobId(jobId);
+            if(waylineJobDTO.isEmpty()) {
+                return ResponseResult.error(99,"任务不存在");
+            }
+            return ResponseResult.success(waylineJobDTO.get());
+        } catch (Exception e) {
+            return ResponseResult.error(e.getMessage());
+        }
+    }
+
+    /**
+     * Paginate through all jobs.
+     * @param page
+     * @param pageSize
+     * @return
+     */
+    @ApiOperation("航线任务列表")
+    @GetMapping("/jobs")
+    public ResponseResult<PaginationData<WaylineJobDTO>> getJobs(@RequestParam(defaultValue = "1") Long page,
+                     @RequestParam(name = "page_size", defaultValue = "10") Long pageSize,
+                                                                 @RequestParam(required = false) Integer status,
+                                                                 @RequestParam(required = false) Long startTime,
+                                                                 @RequestParam(required = false) Long endTime,
+                                                                 @RequestParam(required = false) Integer taskType,
+                                                                 @RequestParam(required = false) String name,
+                                                                 @RequestParam(required = false) Boolean isComplete) {
+        try {
+//        PaginationData<WaylineJobDTO> data = waylineJobService.getJobsByWorkspaceId(page, pageSize);defaultValue
+            PaginationData<WaylineJobDTO> data = waylineJobService.getJobsList(page, pageSize, status, startTime, endTime, taskType, name, isComplete);
+            return ResponseResult.success(data);
+        }
+        catch (Exception e) {
+            return ResponseResult.error(e.getMessage());
+        }
+    }
+
+    /**
+     * Send the command to cancel the jobs.
+     * @param jobIds
+     * @return
+     * @throws SQLException
+     */
+    @ApiOperation("取消航线任务")
+    @DeleteMapping("/jobs")
+    public ResponseResult publishCancelJob(@RequestParam(name = "job_id") Set<String> jobIds){
+        return waylineJobService.cancelFlightTask(jobIds);
+    }
+
+    /**
+     * Set the media files for this job to upload immediately.
+     * @param jobId
+     * @return
+     */
+    @ApiOperation("设置航线任务上传最高优先级")
+    @PostMapping("/{workspace_id}/jobs/{job_id}/media-highest")
+    public ResponseResult uploadMediaHighestPriority(@PathVariable(name = "job_id") String jobId) {
+        waylineJobService.uploadMediaHighestPriority(jobId);
+        return ResponseResult.success();
+    }
+}
diff --git a/ard-admin/src/main/java/com/ard/web/controller/sample/dock1/wayline/Dock1WaylineTrajectoryController.java b/ard-admin/src/main/java/com/ard/web/controller/sample/dock1/wayline/Dock1WaylineTrajectoryController.java
new file mode 100644
index 0000000..d13465a
--- /dev/null
+++ b/ard-admin/src/main/java/com/ard/web/controller/sample/dock1/wayline/Dock1WaylineTrajectoryController.java
@@ -0,0 +1,46 @@
+/*
+ * 【机场1】从 dock2 原类 com.ard.web.controller.sample.wayline.WaylineTrajectoryController 复制/独立适配(DJI Dock 1 / M30)。
+ * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。
+ */
+package com.ard.web.controller.sample.dock1.wayline;
+
+import com.ard.sample.common.model.ResponseResult;
+import com.ard.sample.manage.model.dto.WaylineTrajectoryDTO;
+// 机场1:与 OSD 轨迹写入共用同一个轨迹服务实例,保证 job_id 能查到完整轨迹。
+import com.ard.sample.manage.service.IWaylineTrajectoryService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.Optional;
+import com.ard.web.controller.sample.wayline.WaylineTrajectoryController;
+
+@Api(tags = "航线历史任务轨迹")
+@RequestMapping("/dock1/waylineTrajectory")
+@RestController
+public class Dock1WaylineTrajectoryController {
+    @Autowired
+    private IWaylineTrajectoryService _waylineTrajectoryService;
+
+    @ApiOperation("获取航线任务轨迹")
+    @GetMapping("/flight-id")
+    public ResponseResult<WaylineTrajectoryDTO> getJob(@RequestParam(name = "job_id") String jobId) {
+        try {
+            if(jobId == null){
+                return ResponseResult.error(98,"任务ID不能为空");
+            }
+            WaylineTrajectoryDTO waylineJobDTO = _waylineTrajectoryService.getWayLineTrajectorybyJobID(jobId);
+            if(waylineJobDTO==null) {
+                return ResponseResult.error(99,"任务不存在");
+            }
+            return ResponseResult.success(waylineJobDTO);
+        } catch (Exception e) {
+            return ResponseResult.error(97, "获取航线轨迹发生异常:"+e.getMessage());
+        }
+    }
+}
+
diff --git a/ard-admin/src/main/java/com/ard/web/controller/sample/media/FileController.java b/ard-admin/src/main/java/com/ard/web/controller/sample/media/FileController.java
index 9ef6638..21896de 100644
--- a/ard-admin/src/main/java/com/ard/web/controller/sample/media/FileController.java
+++ b/ard-admin/src/main/java/com/ard/web/controller/sample/media/FileController.java
@@ -70,8 +70,8 @@
      * @param pageSize 每页条数,默认值为10,参数名映射为page_size(前端传参用)
      * @param jobId    任务ID,必填参数,用于筛选指定任务下的媒体文件
      * @param isType   文件类型筛选标识,非必填(若需必填可加required = true):
-     *                 1 - 筛选文件名包含V.jpeg的文件;
-     *                 0 - 筛选文件名包含V.mp4的文件
+     *                 1 - 筛选图片文件(按 .jpeg/.jpg 扩展名,兼容各机型镜头前缀 W/Z/T/V);
+     *                 0 - 筛选视频文件(按 .mp4 扩展名)
      * @return 分页的媒体文件列表数据
      * */
     @ApiOperation("获取任务中媒体文件列表")
@@ -82,7 +82,7 @@
                                                                     @RequestParam(name = "page_size", defaultValue = "10") Long pageSize,
                                                                     @ApiParam(value = "任务ID(必填),筛选指定任务下的文件", required = true)
                                                                     @RequestParam(name = "job_id") String jobId,
-                                                                    @ApiParam(value = "文件类型筛选:1=V.jpeg文件,0=V.mp4文件")
+                                                                    @ApiParam(value = "文件类型筛选:1=图片文件,0=视频文件")
                                                                     @RequestParam(name = "isType") String isType) {
         PaginationData<MediaFileDTO> filesList = fileService.getJobFiles(page, pageSize, jobId, isType);
         return ResponseResult.success(filesList);
diff --git a/ard-admin/src/main/resources/application-daqing-druid.yml b/ard-admin/src/main/resources/application-daqing-druid.yml
index d62e8c2..a6a92dc 100644
--- a/ard-admin/src/main/resources/application-daqing-druid.yml
+++ b/ard-admin/src/main/resources/application-daqing-druid.yml
@@ -6,9 +6,13 @@
         druid:
             # 主库数据源
             master:
-                url: jdbc:mysql://192.168.10.139:3306/ard-uav?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+#                url: jdbc:mysql://10.67.52.22:3306/ard-uav?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+#                username: root
+#                password: Ard12345*
+                #四厂数据源
+                url: jdbc:mysql://10.67.52.22:3306/ard-uav-inspection?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                 username: root
-                password: Ard12345*
+                password: Uh09100802*
             # 从库数据源
             slave:
                 # 从数据源开关/默认关闭
diff --git a/ard-admin/src/main/resources/application-daqing.yml b/ard-admin/src/main/resources/application-daqing.yml
index 40791c4..1a5b093 100644
--- a/ard-admin/src/main/resources/application-daqing.yml
+++ b/ard-admin/src/main/resources/application-daqing.yml
@@ -49,15 +49,15 @@
     id: 143640
     key: da5babf38a5d05b0f46a7d82910da1b
     license: khjd7FzUTzJgaArNv97XdA2nKQtYfESOvBHLqjC6MpvPqxCfuQ82nOlTgzoiLp7UoZujARopN2A1ctPFaJ8oHiHQrn3IRV4wqN3hdSM40xXj02YVvTWQBT54WS8Ks+E8Zatp3gk2T0+7cM5a5a3mWzxLMv7CNqw2z9pFBoOBdpY=
-    host: 192.168.10.139
+    host: 10.67.52.22
     port: 123
 
 #流媒体
 mediamtx:
-  apiHost: 192.168.10.139
-  playHost: 192.168.10.139
+  apiHost: 10.67.52.22
+  playHost: 10.67.52.22
   https: true
-  pushHost: 192.168.10.139
+  pushHost: 10.67.52.22
   rtmpPort: 1935
   rtspPort: 8554
   webrtcPort: 8889
@@ -71,7 +71,7 @@
 # mqtt配置
 mqtt:
   protocol: tcp
-  host: 192.168.10.139
+  host: 10.67.52.22
   port: 1883
   username: admin
   password: public
@@ -82,7 +82,7 @@
 oss:
   enable: true
   provider: minio
-  endpoint: https://192.168.10.139:9001
+  endpoint: https://10.67.52.22:9001
   access-key: admin
   secret-key: ard12345
   bucket: cloud-bucket
@@ -92,9 +92,9 @@
 
 # 机场单独用的oss配置,要http地址
 dock:
-  endpoint: http://192.168.10.139:9011
+  endpoint: http://10.67.52.22:9011
   bucket: dockfile
 
 # 点云文件夹路径配置
 pointCloud:
-  url : https://192.168.10.139:3001/tileset/
\ No newline at end of file
+  url : https://10.67.52.22:3001/tileset/
\ No newline at end of file
diff --git a/ard-route/src/main/java/com/ard/route/domain/djuav/kml/KmlPayloadInfo.java b/ard-route/src/main/java/com/ard/route/domain/djuav/kml/KmlPayloadInfo.java
index f6e074d..ecdf9ca 100644
--- a/ard-route/src/main/java/com/ard/route/domain/djuav/kml/KmlPayloadInfo.java
+++ b/ard-route/src/main/java/com/ard/route/domain/djuav/kml/KmlPayloadInfo.java
@@ -12,6 +12,12 @@
     @XStreamAlias("wpml:payloadEnumValue")
     private String payloadEnumValue;
 
+    /**
+     * 负载子类型。机场1(M30/M30T)生成 KMZ 时需要该字段,其余机型保持原输出。
+     */
+    @XStreamAlias("wpml:payloadSubEnumValue")
+    private String payloadSubEnumValue;
+
     @XStreamAlias("wpml:payloadPositionIndex")
     private String payloadPositionIndex;
 
diff --git a/ard-route/src/main/java/com/ard/route/service/impl/ArdRouteInfoServiceImpl.java b/ard-route/src/main/java/com/ard/route/service/impl/ArdRouteInfoServiceImpl.java
index 67677b2..eddf3c2 100644
--- a/ard-route/src/main/java/com/ard/route/service/impl/ArdRouteInfoServiceImpl.java
+++ b/ard-route/src/main/java/com/ard/route/service/impl/ArdRouteInfoServiceImpl.java
@@ -44,6 +44,7 @@
 import org.springframework.stereotype.Service;
 import org.springframework.web.multipart.MultipartFile;
 import javax.annotation.Resource;
+import static com.ard.route.enums.kml.DroneEnumValueEnums.M30_M30T;
 import static com.ard.route.enums.kml.DroneEnumValueEnums.M350_RTK;
 import static com.ard.route.util.InitializationUtils.divided23;
 import static com.ard.route.util.ToolUtils.*;
@@ -174,8 +175,14 @@
         }else {
             ardRouteInfo.getUavRouteReq().setHeightMode("relativeToStartPoint");
         }
-        ardRouteInfo.getUavRouteReq().setDroneType(subStringDroneType(ardRouteInfo.getAircraft()));
-        KmzResultVo kmzResult = routeService.buildKmz(ardRouteInfo.getUavRouteReq(),ardRouteInfo.getName());
+        UavRouteReq uavRouteReq = ardRouteInfo.getUavRouteReq();
+        uavRouteReq.setDroneType(subStringDroneType(ardRouteInfo.getAircraft()));
+        // 机场1:M30/M30T 需保留 aircraft 中的子类型(0-67-0 / 0-67-1),
+        // 否则 M30T 会按 M30 生成 KMZ(payload 52 而非 53)
+        if (M30_M30T.getValue().equals(uavRouteReq.getDroneType())) {
+            uavRouteReq.setSubDroneType(subStringSubDroneType(ardRouteInfo.getAircraft()));
+        }
+        KmzResultVo kmzResult = routeService.buildKmz(uavRouteReq,ardRouteInfo.getName());
         zoomProcessing(ardRouteInfo);
         ardRouteInfo.setPath(kmzResult.getPath());
         ardRouteInfo.setReadOnly("0");
@@ -271,9 +278,14 @@
             // 先删除航线
             delOssService(route);
             // 再生成航线
-            ardRouteInfo.getUavRouteReq().setDroneType(subStringDroneType(ardRouteInfo.getAircraft()));
+            UavRouteReq uavRouteReq = ardRouteInfo.getUavRouteReq();
+            uavRouteReq.setDroneType(subStringDroneType(ardRouteInfo.getAircraft()));
+            // 机场1:M30T(0-67-1) 需同步子类型,避免编辑后按 M30 重新生成 KMZ
+            if (M30_M30T.getValue().equals(uavRouteReq.getDroneType())) {
+                uavRouteReq.setSubDroneType(subStringSubDroneType(ardRouteInfo.getAircraft()));
+            }
             zoomProcessing(ardRouteInfo);
-            kmzResult = routeService.buildKmz(ardRouteInfo.getUavRouteReq(), ardRouteInfo.getName());
+            kmzResult = routeService.buildKmz(uavRouteReq, ardRouteInfo.getName());
             if ("更新航线kmz文件失败".equals(kmzResult.getData())) {
                 return 0;
             }
diff --git a/ard-route/src/main/java/com/ard/route/util/InitializationUtils.java b/ard-route/src/main/java/com/ard/route/util/InitializationUtils.java
index 17fc9b1..56734ea 100644
--- a/ard-route/src/main/java/com/ard/route/util/InitializationUtils.java
+++ b/ard-route/src/main/java/com/ard/route/util/InitializationUtils.java
@@ -4,9 +4,12 @@
 import com.ard.route.domain.sixhe.ArdRouteActions;
 import com.ard.route.domain.sixhe.ArdRouteChange;
 import com.ard.route.domain.sixhe.ArdRoutePoints;
+import com.ard.route.enums.kml.DroneEnumValueEnums;
+import com.ard.route.enums.kml.PayloadTypeEnums;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Objects;
 
 import static com.ard.route.enums.kml.DroneEnumValueEnums.M350_RTK;
 import static com.ard.route.util.ToolUtils.getDoublePoint;
@@ -38,8 +41,18 @@
                             uavRouteReq.getRoutePointList().get(0).getHeight());
         }
         //uavRouteReq.setDroneType(89);
-        uavRouteReq.setSubDroneType(0);
-        uavRouteReq.setPayloadType(43);
+        // 机场1:M30/M30T 使用机身自带双光/三光相机,payloadEnumValue 分别为 52/53,
+        // 不能沿用 H20T(43)。其余机型保持原有逻辑,机场2/3 的航线生成结果不变。
+        if (Objects.equals(DroneEnumValueEnums.M30_M30T.getValue(), uavRouteReq.getDroneType())) {
+            if (uavRouteReq.getSubDroneType() == null) {
+                uavRouteReq.setSubDroneType(0);
+            }
+            uavRouteReq.setPayloadType(Objects.equals(1, uavRouteReq.getSubDroneType())
+                    ? PayloadTypeEnums.M30T.getValue() : PayloadTypeEnums.M30.getValue());
+        } else {
+            uavRouteReq.setSubDroneType(0);
+            uavRouteReq.setPayloadType(43);
+        }
         uavRouteReq.setPayloadPosition(0);
         uavRouteReq.setImageFormat("wide");
         uavRouteReq.setFinishAction("goHome");
diff --git a/ard-route/src/main/java/com/ard/route/util/RouteFileUtils.java b/ard-route/src/main/java/com/ard/route/util/RouteFileUtils.java
index b79dc6a..a364264 100644
--- a/ard-route/src/main/java/com/ard/route/util/RouteFileUtils.java
+++ b/ard-route/src/main/java/com/ard/route/util/RouteFileUtils.java
@@ -167,6 +167,12 @@
     public static KmlPayloadInfo buildKmlPayloadInfo(Integer payloadType, Integer payloadPosition) {
         KmlPayloadInfo kmlPayloadInfo = new KmlPayloadInfo();
         kmlPayloadInfo.setPayloadEnumValue(String.valueOf(payloadType));
+        // 机场1:M30/M30T 的 WPML 需要 payloadSubEnumValue,否则机场侧校验可能不通过;
+        // 其余机型保持原有输出,不影响机场2/3。
+        if (Objects.equals(PayloadTypeEnums.M30.getValue(), payloadType)
+                || Objects.equals(PayloadTypeEnums.M30T.getValue(), payloadType)) {
+            kmlPayloadInfo.setPayloadSubEnumValue("0");
+        }
         kmlPayloadInfo.setPayloadPositionIndex(String.valueOf(payloadPosition));
         return kmlPayloadInfo;
     }
diff --git a/ard-route/src/main/java/com/ard/route/util/ToolUtils.java b/ard-route/src/main/java/com/ard/route/util/ToolUtils.java
index dcab57b..ef51939 100644
--- a/ard-route/src/main/java/com/ard/route/util/ToolUtils.java
+++ b/ard-route/src/main/java/com/ard/route/util/ToolUtils.java
@@ -2,10 +2,12 @@
 
 import com.ard.inspection.domain.ArdPointCloud;
 import com.ard.route.domain.djuav.UavRouteReq;
+import com.ard.route.enums.kml.DroneEnumValueEnums;
 import java.io.*;
 import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
 import java.util.List;
+import java.util.Objects;
 
 /**
  * 工具类
@@ -139,6 +141,15 @@
     }
 
     /**
+     * 截取机型子类型
+     * 参数: "0-67-1" 返回 1;格式不足三段时返回 0
+     */
+    public static Integer subStringSubDroneType(String str) {
+        String[] parts = str.split("-");
+        return parts.length > 2 ? Integer.valueOf(parts[2]) : 0;
+    }
+
+    /**
      * 截取字符串
      * 获取经纬度高度
      */
@@ -173,6 +184,12 @@
      * 获取无人机类型方法
      */
     public static String droneTypeData(UavRouteReq uavRouteReq) {
+        // 机场1:M30(67-0) / M30T(67-1) 需要保留 subType,避免航线记录的 aircraft 都变成 0-67-0。
+        // 其它机型保持原有返回值,机场2/3 不受影响。
+        if (Objects.equals(DroneEnumValueEnums.M30_M30T.getValue(), uavRouteReq.getDroneType())) {
+            return "0-" + uavRouteReq.getDroneType() + "-"
+                    + (uavRouteReq.getSubDroneType() == null ? 0 : uavRouteReq.getSubDroneType());
+        }
         return "0-" + uavRouteReq.getDroneType() + "-0";
     }
 
diff --git a/ard-route/src/main/java/com/ard/route/util/UavRouteReqUtils.java b/ard-route/src/main/java/com/ard/route/util/UavRouteReqUtils.java
index b9d620e..55ef274 100644
--- a/ard-route/src/main/java/com/ard/route/util/UavRouteReqUtils.java
+++ b/ard-route/src/main/java/com/ard/route/util/UavRouteReqUtils.java
@@ -51,7 +51,12 @@
         if (vo.getKmlInfo().getDocument().getFolder().getTemplateType() != null){
             uavRouteReq.setTemplateType(vo.getKmlInfo().getDocument().getFolder().getTemplateType());
         }
-        uavRouteReq.setDroneType(Integer.valueOf(vo.getKmlInfo().getDocument().getKmlMissionConfig().getDroneInfo().getDroneEnumValue()));
+        KmlDroneInfo droneInfo = vo.getKmlInfo().getDocument().getKmlMissionConfig().getDroneInfo();
+        uavRouteReq.setDroneType(Integer.valueOf(droneInfo.getDroneEnumValue()));
+        // 机场1:M30/M30T 依赖 droneSubEnumValue 区分(0-67-0 / 0-67-1),导入 KMZ 时需一并保留
+        if (droneInfo.getDroneSubEnumValue() != null && !droneInfo.getDroneSubEnumValue().isEmpty()) {
+            uavRouteReq.setSubDroneType(Integer.valueOf(droneInfo.getDroneSubEnumValue()));
+        }
         uavRouteReq.setImageFormat(vo.getKmlInfo().getDocument().getFolder().getPayloadParam().getImageFormat());
         uavRouteReq.setGlobalHeight(Double.valueOf(vo.getKmlInfo().getDocument().getFolder().getGlobalHeight()));
         uavRouteReq.setAutoFlightSpeed(Double.valueOf(vo.getKmlInfo().getDocument().getFolder().getAutoFlightSpeed()));
diff --git a/ard-sample/src/main/java/com/ard/sample/component/mqtt/config/MqttMessageChannel.java b/ard-sample/src/main/java/com/ard/sample/component/mqtt/config/MqttMessageChannel.java
index ce6fc87..b94fbaf 100644
--- a/ard-sample/src/main/java/com/ard/sample/component/mqtt/config/MqttMessageChannel.java
+++ b/ard-sample/src/main/java/com/ard/sample/component/mqtt/config/MqttMessageChannel.java
@@ -115,6 +115,14 @@
         return new DirectChannel();
     }
 
+    /**
+     * 机场1:航线准备异常通知通道。
+     */
+    @Bean(name = ChannelName.INBOUND_EVENTS_FLIGHT_SETUP_EXCEPTION_NOTIFY)
+    public MessageChannel eventsFlightSetupExceptionNotifyChannel() {
+        return new DirectChannel();
+    }
+
     @Bean(name = ChannelName.INBOUND_EVENTS_FILE_UPLOAD_CALLBACK)
     public MessageChannel eventsFileUploadCallbackChannel() {
         return new DirectChannel();
diff --git a/ard-sample/src/main/java/com/ard/sample/component/mqtt/handler/StateLiveStatusHandler.java b/ard-sample/src/main/java/com/ard/sample/component/mqtt/handler/StateLiveStatusHandler.java
index 7bc77ef..1b22057 100644
--- a/ard-sample/src/main/java/com/ard/sample/component/mqtt/handler/StateLiveStatusHandler.java
+++ b/ard-sample/src/main/java/com/ard/sample/component/mqtt/handler/StateLiveStatusHandler.java
@@ -17,6 +17,7 @@
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
 
 import java.util.List;
 import java.util.Map;
@@ -43,7 +44,9 @@
                 List<LiveStatusTwoReceiver> list = mapper.convertValue(dataNode.get(StateDataEnum.LIVESTATUS.getDesc()),
                         new TypeReference<List<LiveStatusTwoReceiver>>() {});
                 for (LiveStatusTwoReceiver receiver : list) {
-                    if(receiver.getVideoId()!=null && receiver.getVideoId().contains(childSn)){
+                    // 机场1(Dock1)未连接飞行器时 childDeviceSn 为空,跳过匹配,避免 String.contains(null) 抛 NPE
+                    if (StringUtils.hasText(childSn) && receiver.getVideoId() != null
+                            && receiver.getVideoId().contains(childSn)) {
                         stateReceiver.setData(receiver);
                         continue;
                     }
@@ -56,7 +59,10 @@
                 catch (Exception e){
                     log.error("接收state直播设备状态反馈转换异常:{}",e);
                 }
-                _deviceService.setChildRedisCamera(childSn, liveStatus);
+                // 无子设备 SN 时不写缓存,避免产生 COVER_STATE_PREFIX + null 的非法 Redis key
+                if (StringUtils.hasText(childSn)) {
+                    _deviceService.setChildRedisCamera(childSn, liveStatus);
+                }
             }
 
             return stateReceiver;
diff --git a/ard-sample/src/main/java/com/ard/sample/component/mqtt/model/ChannelName.java b/ard-sample/src/main/java/com/ard/sample/component/mqtt/model/ChannelName.java
index 2ac0fc6..8cca3fa 100644
--- a/ard-sample/src/main/java/com/ard/sample/component/mqtt/model/ChannelName.java
+++ b/ard-sample/src/main/java/com/ard/sample/component/mqtt/model/ChannelName.java
@@ -55,6 +55,11 @@
 
     public static final String INBOUND_EVENTS_FLIGHT_TASK_PROGRESS = "inboundEventsFlightTaskProgress";
 
+    /**
+     * 机场1:航线准备异常通知(flight_setup_exception_notify),Dock1/M30 新增事件。
+     */
+    public static final String INBOUND_EVENTS_FLIGHT_SETUP_EXCEPTION_NOTIFY = "inboundEventsFlightSetupExceptionNotify";
+
     public static final String INBOUND_EVENTS_FILE_UPLOAD_CALLBACK = "inboundEventsFileUploadCallback";
 
     public static final String INBOUND_REQUESTS_AIRPORT_BIND_STATUS = "inboundRequestsAirportBindStatus";
diff --git a/ard-sample/src/main/java/com/ard/sample/component/mqtt/model/EventsMethodEnum.java b/ard-sample/src/main/java/com/ard/sample/component/mqtt/model/EventsMethodEnum.java
index be4130c..b24f1a0 100644
--- a/ard-sample/src/main/java/com/ard/sample/component/mqtt/model/EventsMethodEnum.java
+++ b/ard-sample/src/main/java/com/ard/sample/component/mqtt/model/EventsMethodEnum.java
@@ -11,6 +11,11 @@
 
     FLIGHT_TASK_PROGRESS("flighttask_progress", ChannelName.INBOUND_EVENTS_FLIGHT_TASK_PROGRESS),
 
+    /**
+     * 机场1:航线准备异常通知(Dock1/M30 新增事件,共享层原枚举缺少该方法)。
+     */
+    FLIGHT_SETUP_EXCEPTION_NOTIFY("flight_setup_exception_notify", ChannelName.INBOUND_EVENTS_FLIGHT_SETUP_EXCEPTION_NOTIFY),
+
     FILE_UPLOAD_CALLBACK("file_upload_callback", ChannelName.INBOUND_EVENTS_FILE_UPLOAD_CALLBACK),
 
     HMS("hms", ChannelName.INBOUND_EVENTS_HMS),
diff --git a/ard-sample/src/main/java/com/ard/sample/component/websocket/handler/DockWebSocketHandler.java b/ard-sample/src/main/java/com/ard/sample/component/websocket/handler/DockWebSocketHandler.java
index 2d39797..6ad0a20 100644
--- a/ard-sample/src/main/java/com/ard/sample/component/websocket/handler/DockWebSocketHandler.java
+++ b/ard-sample/src/main/java/com/ard/sample/component/websocket/handler/DockWebSocketHandler.java
@@ -18,6 +18,7 @@
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
 
 /**
@@ -38,6 +39,12 @@
     private static final long HEARTBEAT_TIMEOUT = 90;
     /** 心跳检测任务间隔(秒):定时扫描超时连接 */
     private static final long HEARTBEAT_CHECK_INTERVAL = 10;
+    /**
+     * 停流延迟(秒):页面刷新或网络抖动会先断开旧连接再建立新连接,
+     * 断开时立即 live_stop 会让新页面需要重新起流,视频出现空档甚至丢失。
+     * 延迟期间重新订阅则取消停止。
+     */
+    private static final long LIVE_STOP_DELAY_SECONDS = 30;
 
     // ====================== 核心存储(线程安全) ======================
     /** 用户会话映射:userId -> WebSocketSession */
@@ -48,12 +55,21 @@
     private final Map<WebSocketSession, Object> sessionLocks = new ConcurrentHashMap<>();
     /** 用户最后心跳时间:userId -> 最后心跳时间戳(毫秒) */
     private final Map<String, Long> userLastHeartbeatTime = new ConcurrentHashMap<>();
+    /** 待执行的延迟停流任务:设备SN -> 定时任务 */
+    private final Map<String, ScheduledFuture<?>> pendingLiveStops = new ConcurrentHashMap<>();
 
     // ====================== 线程池 ======================
     /** 心跳检测线程池(单线程守护线程) */
     private final ScheduledExecutorService heartbeatCheckExecutor = Executors.newSingleThreadScheduledExecutor(runnable -> {
         Thread thread = new Thread(runnable, "websocket-heartbeat-check-thread");
         thread.setDaemon(true); // 守护线程,不阻塞JVM退出
+        return thread;
+    });
+
+    /** 延迟停流线程池(单线程守护线程),停流可能走 HTTP,独立线程避免阻塞心跳检测 */
+    private final ScheduledExecutorService liveStopExecutor = Executors.newSingleThreadScheduledExecutor(runnable -> {
+        Thread thread = new Thread(runnable, "websocket-live-stop-thread");
+        thread.setDaemon(true);
         return thread;
     });
 
@@ -188,6 +204,10 @@
         if (isSubscribe) {
             // 订阅操作:不存在则创建,存在则添加用户
             subscriptions.computeIfAbsent(subscriptionKey, k -> ConcurrentHashMap.newKeySet()).add(userId);
+            // 页面刷新会先断连再重连,新订阅到达时取消上一次断连触发的延迟停流
+            if (MessageType.LIVESTREAM.getType().equals(type)) {
+                cancelPendingLiveStop(sn);
+            }
 //            log.info("用户[{}]订阅设备[{}]的[{}]类型消息", userId, sn, type);
 
 //            // 直播类型订阅:触发开启直播
@@ -202,12 +222,12 @@
                 subscriberIds.remove(userId);
                 log.info("用户[{}]取消订阅设备[{}]的[{}]类型消息", userId, sn, type);
 
-                // 订阅组为空时移除,并停止直播(直播类型)
+                // 订阅组为空时移除,并延迟停止直播(直播类型)。
+                // 不立即停流:页面刷新会先断连再重连,立即停流会导致视频长时间黑屏。
                 if (subscriberIds.isEmpty()) {
                     subscriptions.remove(subscriptionKey);
                     if (MessageType.LIVESTREAM.getType().equals(type)) {
-                        _liveStreamService.StopLiveStreaming(sn);
-                        log.info("设备[{}]直播流已停止(无订阅用户)", sn);
+                        scheduleStopLiveStream(sn);
                     }
                 }
             }
@@ -283,10 +303,13 @@
 
             // 线程安全发送消息
             try {
-                if(MessageType.LIVESTREAM.getType().equals(type)) {
+                // 指定了接收用户时只推给该用户;uId 为空(如后台定时任务起流)时推给全部订阅者
+                if (MessageType.LIVESTREAM.getType().equals(type) && uId != null && !uId.isEmpty()) {
                     WebSocketSession liveSession = userSessions.get(uId);
-                    sendMessage(liveSession, content);
-                    log.info("单独向用户[{}]推送设备[{}]的[{}]类型消息:{}", uId, sn, type, content);
+                    if (liveSession != null && liveSession.isOpen()) {
+                        sendMessage(liveSession, content);
+                        log.info("单独向用户[{}]推送设备[{}]的[{}]类型消息:{}", uId, sn, type, content);
+                    }
                     return;
                 }
                 sendMessage(session, content);
@@ -367,17 +390,52 @@
             if (subscriberIds.remove(userId)) {
                 log.debug("用户[{}]已移除订阅:{}", userId, entry.getKey());
 
-                // 订阅组为空时移除,并停止直播(直播类型)
+                // 订阅组为空时移除,并延迟停止直播(直播类型)。
+                // 页面刷新/短暂断网会重建连接,延迟停流可避免视频空档;期间重新订阅会取消停止。
                 if (subscriberIds.isEmpty()) {
                     String subscriptionKey = entry.getKey();
                     if (subscriptionKey.contains(MessageType.LIVESTREAM.getType())) {
                         String sn = subscriptionKey.split(":")[0];
-                        _liveStreamService.StopLiveStreaming(sn);
-                        log.info("设备[{}]无订阅用户,已停止直播流", sn);
+                        scheduleStopLiveStream(sn);
                     }
                     iterator.remove(); // 安全移除空订阅组
                 }
             }
+        }
+    }
+
+    /**
+     * 延迟停止直播流。
+     * 页面刷新/网络抖动会先断开旧连接再建立新连接,断开时立即停流会让新页面重新起流,
+     * 视频出现空档甚至丢失;这里延迟 LIVE_STOP_DELAY_SECONDS 秒再停,期间有新订阅则取消。
+     */
+    private void scheduleStopLiveStream(String sn) {
+        String subscriptionKey = getSubscriptionKey(sn, MessageType.LIVESTREAM.getType());
+        pendingLiveStops.compute(sn, (key, oldFuture) -> {
+            if (oldFuture != null && !oldFuture.isDone()) {
+                oldFuture.cancel(false);
+            }
+            return liveStopExecutor.schedule(() -> {
+                pendingLiveStops.remove(sn);
+                Set<String> subscriberIds = subscriptions.get(subscriptionKey);
+                // 延迟期间又有用户订阅(如页面刷新重连),不停止直播
+                if (subscriberIds != null && !subscriberIds.isEmpty()) {
+                    log.info("设备[{}]延迟停流期间重新订阅,继续直播", sn);
+                    return;
+                }
+                _liveStreamService.StopLiveStreaming(sn);
+                log.info("设备[{}]无订阅用户(已延迟{}秒),停止直播流", sn, LIVE_STOP_DELAY_SECONDS);
+            }, LIVE_STOP_DELAY_SECONDS, TimeUnit.SECONDS);
+        });
+    }
+
+    /**
+     * 取消待执行的延迟停流任务(设备重新被订阅时调用)。
+     */
+    private void cancelPendingLiveStop(String sn) {
+        ScheduledFuture<?> future = pendingLiveStops.remove(sn);
+        if (future != null && !future.isDone()) {
+            future.cancel(false);
         }
     }
 
@@ -402,6 +460,10 @@
                 log.error("WebSocket心跳检测线程池关闭被中断", e);
             }
         }
+        // 关闭延迟停流线程池
+        if (liveStopExecutor != null && !liveStopExecutor.isShutdown()) {
+            liveStopExecutor.shutdownNow();
+        }
         log.info("WebSocket处理器资源已销毁");
     }
 }
\ No newline at end of file
diff --git a/ard-sample/src/main/java/com/ard/sample/component/websocket/handler/LiveStreamHandler.java b/ard-sample/src/main/java/com/ard/sample/component/websocket/handler/LiveStreamHandler.java
index 0cfc482..d8ea6e6 100644
--- a/ard-sample/src/main/java/com/ard/sample/component/websocket/handler/LiveStreamHandler.java
+++ b/ard-sample/src/main/java/com/ard/sample/component/websocket/handler/LiveStreamHandler.java
@@ -79,23 +79,21 @@
 
     public void StopLiveStreaming(String sn) {
         ResponseResult rlive = null;
-//        String resultLive = "";
         try {
             // 获取直播参数
             LiveTypeDTO param = getLiveParam(sn);
             // 停止直播逻辑
             rlive = _liveStreamService.liveStop(param.getVideoId());
             log.info("SN: {}-停止直播!返回值:{}", sn, rlive);
-//            rlive.setGateway(sn);
-//            rlive.setType(MessageType.LIVESTREAM.getType());
-//            resultLive = _objectMapper.writeValueAsString(rlive);
-            RedisOpsUtils.del(RedisConst.WEBSOCKET_VIDEO_ADDRESS_PREFIX + sn);
-            log.info("SN: {}-停止直播,并删除redis直播信息!", sn);
         } catch (Exception e) {
-            log.info("SN: {}-停止直播时发生异常,原因:{}", sn,e.getMessage());
-            e.printStackTrace();
+            log.info("SN: {}-停止直播时发生异常,原因:{}", sn, e.getMessage(), e);
+        } finally {
+            // 无论停流是否成功都清理缓存地址:
+            // 设备离线等原因导致停流异常时,若不清 Redis,后续订阅会直接拿到失效地址,
+            // 页面持续黑屏,只有等设备离线清理缓存后才能恢复
+            RedisOpsUtils.del(RedisConst.WEBSOCKET_VIDEO_ADDRESS_PREFIX + sn);
+            log.info("SN: {}-已删除redis直播信息!", sn);
         }
-        // 停止直播不返回数据,直接删除redis中的直播地址标记即可;并且方法在websocket连接断开后执行。
     }
 
     public String getResult(String sn, String msg) {
@@ -140,32 +138,43 @@
                 continue;
             }
             List<CapacityCameraDTO> camerasList = device.getCamerasList();
+            if (camerasList == null || camerasList.isEmpty()) {
+                continue;
+            }
             for (CapacityCameraDTO camera : camerasList) {
-                // 机场3中飞行器,没有name,需要其他条件确定主摄像头参数
-                for(CapacityVideoDTO video : camera.getVideosList())
-                {
-                    if(video.getSwitchVideoTypes().size() == 1 && camera.getName() == null)
-                    {
-                        continue;
+                List<CapacityVideoDTO> videosList = camera.getVideosList();
+                if (videosList == null || videosList.isEmpty()) {
+                    continue;
+                }
+                // 机场1:M30 / M30T 相机索引为 52-x-x / 53-x-x,live_capacity 可能同时上报
+                // normal-0、zoom-0、ir-0 多条码流。默认优先选 normal 码流(可见光),
+                // 避免原来“循环取最后一条”时默认落到红外。其他机型保持原行为(取最后一条)。
+                boolean m30SeriesCamera = camera.getIndex() != null
+                        && (camera.getIndex().startsWith("52-") || camera.getIndex().startsWith("53-"));
+                CapacityVideoDTO selectedVideo = null;
+                for (CapacityVideoDTO video : videosList) {
+                    selectedVideo = video;
+                    if (m30SeriesCamera && video.getIndex() != null
+                            && video.getIndex().startsWith("normal")) {
+                        break;
                     }
                 }
-//                camera.getVideosList().get(0).getSwitchVideoTypes().add("main");
-//                if(camera.getVideosList().size() == 1 && camera.getName() == null) {
-//                    continue;
-//                }
-//                if (camera.getName() == null || camera.getName().isEmpty()) {
-//                    continue;
-//                }
-                String videoid = device.getSn() + "/" + camera.getIndex();
-                List<CapacityVideoDTO> videosList = camera.getVideosList();
-                for (CapacityVideoDTO video : videosList) {
-                    videoid = videoid + "/" + video.getIndex();
-                    param.setUrlType(1);
-                    param.setUrl("");
-                    param.setVideoId(videoid);
-                    param.setVideoQuality(1);
-                    param.setVideoType(video.getSwitchVideoTypes().get(0));
+                if (selectedVideo == null) {
+                    continue;
                 }
+                List<String> switchVideoTypes = selectedVideo.getSwitchVideoTypes();
+                if (switchVideoTypes == null || switchVideoTypes.isEmpty()) {
+                    // 没有镜头类型信息时跳过该相机,避免 get(0) 越界
+                    continue;
+                }
+                // 机场1:video_id 格式固定为 {sn}/{camera_index}/{video_index}。
+                // 按选中的码流独立构造,单码流设备结果与原来完全一致。
+                String videoid = device.getSn() + "/" + camera.getIndex() + "/" + selectedVideo.getIndex();
+                param.setUrlType(1);
+                param.setUrl("");
+                param.setVideoId(videoid);
+                param.setVideoQuality(1);
+                param.setVideoType(switchVideoTypes.get(0));
             }
         }
         return param;
diff --git a/ard-sample/src/main/java/com/ard/sample/dock1/manage/model/receiver/Dock1DroneAuthorityInfoReceiver.java b/ard-sample/src/main/java/com/ard/sample/dock1/manage/model/receiver/Dock1DroneAuthorityInfoReceiver.java
new file mode 100644
index 0000000..f1c14b4
--- /dev/null
+++ b/ard-sample/src/main/java/com/ard/sample/dock1/manage/model/receiver/Dock1DroneAuthorityInfoReceiver.java
@@ -0,0 +1,35 @@
+/*
+ * 【机场1】dock1 独有 OSD 子结构(DJI Dock 1 / M30)。
+ * 官方定义见上云 API Dock1 分册 properties:drone_authority_info(飞行器控制权状态更新)。
+ */
+package com.ard.sample.dock1.manage.model.receiver;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 【机场1】飞行器控制权状态更新	struct(dock2 无此结构)
+ */
+@Data
+public class Dock1DroneAuthorityInfoReceiver {
+
+    /**
+     * 飞行控制权	enum_string	{"":"未获取","A":"A控","B":"B控"}
+     * */
+    @JsonProperty("control_source")
+    private String controlSource;
+
+    /**
+     * 飞行控制权是否锁定(负载控制权不支持锁定)	bool
+     * */
+    @JsonProperty("locked")
+    private Boolean locked;
+
+    /**
+     * 负载	array	{"size": -, "item_type": struct}
+     * */
+    @JsonProperty("payloads")
+    private List<Dock1DroneAuthorityPayloadReceiver> payloads;
+}
diff --git a/ard-sample/src/main/java/com/ard/sample/dock1/manage/model/receiver/Dock1DroneAuthorityPayloadReceiver.java b/ard-sample/src/main/java/com/ard/sample/dock1/manage/model/receiver/Dock1DroneAuthorityPayloadReceiver.java
new file mode 100644
index 0000000..655e4fc
--- /dev/null
+++ b/ard-sample/src/main/java/com/ard/sample/dock1/manage/model/receiver/Dock1DroneAuthorityPayloadReceiver.java
@@ -0,0 +1,33 @@
+/*
+ * 【机场1】dock1 独有 OSD 子结构(DJI Dock 1 / M30)。
+ * 官方定义见上云 API Dock1 分册 properties:drone_authority_info.payloads 数组元素。
+ */
+package com.ard.sample.dock1.manage.model.receiver;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+/**
+ * 【机场1】飞行器控制权 - 负载控制权	struct(drone_authority_info.payloads 元素)
+ */
+@Data
+public class Dock1DroneAuthorityPayloadReceiver {
+
+    /**
+     * 负载控制权	enum_string	{"":"未获取","A":"A控","B":"B控"}
+     * */
+    @JsonProperty("control_source")
+    private String controlSource;
+
+    /**
+     * 负载编号	text	格式 {type-subtype-gimbalindex}
+     * */
+    @JsonProperty("payload_index")
+    private String payloadIndex;
+
+    /**
+     * 负载序列号(SN)	text
+     * */
+    @JsonProperty("sn")
+    private String sn;
+}
diff --git a/ard-sample/src/main/java/com/ard/sample/dock1/manage/model/receiver/Dock1OsdDockReceiver.java b/ard-sample/src/main/java/com/ard/sample/dock1/manage/model/receiver/Dock1OsdDockReceiver.java
new file mode 100644
index 0000000..5c567e7
--- /dev/null
+++ b/ard-sample/src/main/java/com/ard/sample/dock1/manage/model/receiver/Dock1OsdDockReceiver.java
@@ -0,0 +1,306 @@
+/*
+ * 【机场1】从 dock2 原类 com.ard.sample.manage.model.receiver.OsdDockReceiver 复制/独立适配(DJI Dock 1 / M30)。
+ * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。
+ */
+package com.ard.sample.dock1.manage.model.receiver;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import com.ard.sample.manage.model.receiver.AlternateLandPointReceiver;
+import com.ard.sample.manage.model.receiver.BackupBatteryReceiver;
+import com.ard.sample.manage.model.receiver.DeviceMaintainStatusReceiver;
+import com.ard.sample.manage.model.receiver.DockMediaFileDetailReceiver;
+import com.ard.sample.manage.model.receiver.DockSdrReceiver;
+import com.ard.sample.manage.model.receiver.DockSubDeviceReceiver;
+import com.ard.sample.manage.model.receiver.DockWirelessLinkReceiver;
+import com.ard.sample.manage.model.receiver.DroneBatteryMaintenanceInfoReceiver;
+import com.ard.sample.manage.model.receiver.DroneChargeStateReceiver;
+import com.ard.sample.manage.model.receiver.NetworkStateReceiver;
+import com.ard.sample.manage.model.receiver.OsdDockReceiver;
+import com.ard.sample.manage.model.receiver.PositionStateReceiver;
+import com.ard.sample.manage.model.receiver.StorageReceiver;
+
+/**
+ * @author sean
+ * @version 1.3
+ * @date 2022/11/3
+ */
+@Data
+public class Dock1OsdDockReceiver {
+
+    /**
+     * 网络状态	struct
+     * */
+    @JsonProperty("network_state")
+    private NetworkStateReceiver networkState;
+
+    /**
+     * 飞行器是否在舱	enum_int	{"0":"舱外","1":"舱内"}
+     * */
+    @JsonProperty("drone_in_dock")
+    private Integer droneInDock;
+
+    /**
+     * 飞行器充电状态	struct
+     * */
+    @JsonProperty("drone_charge_state")
+    private DroneChargeStateReceiver droneChargeState;
+
+    /**
+     * 降雨量	enum_int
+     * "0":"无雨"
+     * "1":"小雨"
+     * "2":"中雨"
+     * "3":"大雨"
+     * */
+    @JsonProperty("rainfall")
+    private Integer rainfall;
+
+    /**
+     * 风速	float	{"unit_name":"米每秒 / m/s"}
+     * */
+    @JsonProperty("wind_speed")
+    private Float windSpeed;
+
+    /**
+     * 环境温度	float	{"unit_name":"摄氏度 / °C"}
+     * */
+    @JsonProperty("environment_temperature")
+    private Float environmentTemperature;
+
+    /**
+     * 舱内温度	float	{"unit_name":"摄氏度 / °C"}
+     * */
+    @JsonProperty("temperature")
+    private Float temperature;
+
+    /**
+     * 舱内湿度	float	{"unit_name":"相对湿度 / %RH"}
+     * */
+    @JsonProperty("humidity")
+    private Integer humidity;
+
+    /**
+     * 纬度	double	{"max":"90","min":"-90","step":"0.01"}
+     * */
+    @JsonProperty("latitude")
+    private Double latitude;
+
+    /**
+     * 经度	double	{"max":"180","min":"-180","step":"0.01"}
+     * */
+    @JsonProperty("longitude")
+    private Double longitude;
+
+    /**
+     * 椭球高度	double	{"unit_name":"米 / m"}
+     * */
+    @JsonProperty("height")
+    private Double height;
+
+    /**
+     * 备降点	struct
+     * */
+    @JsonProperty("alternate_land_point")
+    private AlternateLandPointReceiver alternateLandPoint;
+
+    /**
+     * 首次上电时间	int	{"unit_name":"毫秒 / ms"}
+     * */
+    @JsonProperty("first_power_on")
+    private Long firstPowerOn;
+
+    /**
+     * 搜星状态	struct
+     * */
+    @JsonProperty("position_state")
+    private PositionStateReceiver positionState;
+
+    /**
+     * 存储容量	struct
+     * */
+    @JsonProperty("storage")
+    private StorageReceiver storage;
+
+    /**
+     * 机场状态	enum_int
+     * "0":"空闲中",
+     * "1":"现场调试",
+     * "2":"远程调试",
+     * "3":"固件升级中",
+     * "4":"作业中",
+     * "5":"待标定"
+     * */
+    @JsonProperty("mode_code")
+    private Integer modeCode;
+
+    /**
+     * 舱盖状态	enum_int
+     * "0":"关闭",
+     * "1":"打开",
+     * "2":"半开",
+     * "3":"舱盖状态异常"
+     * */
+    @JsonProperty("cover_state")
+    private Integer coverState;
+
+    /**
+     *机场静音模式	enum_int	{"0":"非静音模式","1":"静音模式"}
+     * */
+    @JsonProperty("silent_mode")
+    private Integer silentMode;
+
+    /**
+     * 补光灯状态	enum_int	{"0":"关闭","1":"打开"}
+     * */
+    @JsonProperty("supplement_light_state")
+    private Integer supplementLightState;
+
+    /**
+     * 紧急停止按钮状态	enum_int	{"0":"关闭","1":"开启"}
+     * */
+    @JsonProperty("emergency_stop_state")
+    private Integer emergencyStopState;
+
+    /**
+     * 空调模式 上云文档中没有注释
+     * */
+    @JsonProperty("air_conditioner_mode")
+    private Integer airConditionerMode;
+
+    /**
+     * 电池运行模式 enum_int	{"1":"计划模式","2":"待命模式"}
+     * */
+    @JsonProperty("battery_store_mode")
+    private Integer batteryStoreMode;
+
+    /**
+     * 机场声光报警状态	enum_int	{"0":"关闭","1":"开启"}
+     * */
+    @JsonProperty("alarm_state")
+    private Integer alarmState;
+
+    /**
+     * 推杆状态	enum_int	{"0":"关闭","1":"打开","2":"半开","3":"推杆状态异常"}
+     * */
+    @JsonProperty("putter_state")
+    private Integer putterState;
+
+    /**
+     * 子设备状态	struct
+     * */
+    @JsonProperty("sub_device")
+    private DockSubDeviceReceiver subDevice;
+
+    /**
+     * 机场累计作业次数	int	{"unit_name":"次 / count"}
+     * */
+    @JsonProperty("job_number")
+    private Integer jobNumber;
+
+    /**
+     * 机场累计运行时长	int	{"unit_name":"秒 / s"}
+     * */
+    @JsonProperty("acc_time")
+    private Long accTime;
+
+    /**
+     * 机场激活时间(unix 时间戳)	int	{"unit_name":"秒 / s"}
+     * */
+    @JsonProperty("activation_time")
+    private Long activationTime;
+
+    /**
+     * 保养信息	struct
+     * */
+    @JsonProperty("maintain_status")
+    private DeviceMaintainStatusReceiver maintainStatus;
+
+    /**
+     * 市电电压	int	{"unit_name":"伏特 / V"}
+     * */
+    @JsonProperty("electric_supply_voltage")
+    private Integer electricSupplyVoltage;
+
+    /**
+     * 工作电压	int	{"unit_name":"毫伏 / mV"}
+     * */
+    @JsonProperty("working_voltage")
+    private Integer workingVoltage;
+
+    /**
+     * 工作电流	float	{"unit_name":"毫安 / mA"}
+     * */
+    @JsonProperty("working_current")
+    private Integer workingCurrent;
+
+    /**
+     * 机场备用电池信息	struct
+     * */
+    @JsonProperty("backup_battery")
+    private BackupBatteryReceiver backupBattery;
+
+    /**
+     * 飞行器电池保养信息	struct
+     * */
+    @JsonProperty("drone_battery_maintenance_info")
+    private DroneBatteryMaintenanceInfoReceiver droneBatteryMaintenanceInfo;
+
+    /**
+     * 机场任务状态	enum_int
+     * "0":"作业准备中",
+     * "1":"飞行作业中",
+     * "2":"作业后状态恢复",
+     * "3":"自定义飞行区更新中",
+     * "4":"地形障碍物更新中",
+     * "5":"任务空闲",
+     * "255":"飞行器异常",
+     * "256":"未知状态"
+     * */
+    @JsonProperty("flighttask_step_code")
+    private Integer flighttaskStepCode;
+
+    /**
+     * 飞行任务准备容量   上云文档中没有
+     * */
+    @JsonProperty("flighttask_prepare_capacity")
+    private Integer flighttaskPrepareCapacity;
+
+    /**
+     * 媒体文件上传细节	struct
+     * */
+    @JsonProperty("media_file_detail")
+    private DockMediaFileDetailReceiver mediaFileDetail;
+
+    /**
+     *
+     * */
+    @JsonProperty("sdr")
+    private DockSdrReceiver sdr;
+
+    /**
+     * 图传链路	struct
+     * */
+    @JsonProperty("wireless_link")
+    private DockWirelessLinkReceiver wirelessLink;
+
+    // 机场1:C2 以下 3 个字段为 dock1 独有(dock2 分册无此定义),官方文案取自上云 API Dock1 properties
+
+    /**
+     * DRC 链路的状态	enum_int	{"0":"未连接","1":"连接中","2":"已连接"}
+     * */
+    @JsonProperty("drc_state")
+    private Integer drcState;
+
+    /**
+     * 飞行器控制权状态更新	struct
+     * */
+    @JsonProperty("drone_authority_info")
+    private Dock1DroneAuthorityInfoReceiver droneAuthorityInfo;
+
+    /**
+     * 机场的航线解析库版本号	text
+     * */
+    @JsonProperty("wpmz_version")
+    private String wpmzVersion;
+}
diff --git a/ard-sample/src/main/java/com/ard/sample/dock1/manage/model/receiver/Dock1OsdSubDeviceReceiver.java b/ard-sample/src/main/java/com/ard/sample/dock1/manage/model/receiver/Dock1OsdSubDeviceReceiver.java
new file mode 100644
index 0000000..53f7b3d
--- /dev/null
+++ b/ard-sample/src/main/java/com/ard/sample/dock1/manage/model/receiver/Dock1OsdSubDeviceReceiver.java
@@ -0,0 +1,88 @@
+/*
+ * 【机场1】M30 / M30T 飞行器 OSD 接收器。
+ * 共享层 OsdSubDeviceReceiver 只覆盖机场2/3 机型字段,这里在其基础上补充 M30 专属字段;
+ * 不影响共享类与机场2/3 的解析逻辑。
+ */
+package com.ard.sample.dock1.manage.model.receiver;
+
+import com.ard.sample.manage.model.receiver.OsdSubDeviceReceiver;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.List;
+
+/**
+ * 机场1(M30 / M30T)飞行器 OSD 数据。
+ *
+ * @author ard
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
+public class Dock1OsdSubDeviceReceiver extends OsdSubDeviceReceiver {
+
+    /**
+     * 国家/地区代码。
+     */
+    @JsonProperty("country")
+    private String country;
+
+    /**
+     * 当前指挥官飞行模式。
+     * 注意:M30/M30T 不同固件可能上报枚举整数或布尔值,用 Object 兼容,避免整帧 OSD 解析失败。
+     */
+    @JsonProperty("current_commander_flight_mode")
+    private Object currentCommanderFlightMode;
+
+    /**
+     * 遥控器失联后是否退出航线。
+     * 注意:M30/M30T 不同固件可能上报枚举整数或布尔值,用 Object 兼容。
+     */
+    @JsonProperty("exit_wayline_when_rc_lost")
+    private Object exitWaylineWhenRcLost;
+
+    /**
+     * PSDK UI 资源。
+     */
+    @JsonProperty("psdk_ui_resource")
+    private List<Object> psdkUiResource;
+
+    /**
+     * PSDK 控件值。
+     */
+    @JsonProperty("psdk_widget_values")
+    private List<Object> psdkWidgetValues;
+
+    /**
+     * 遥控器失联动作。
+     * 注意:M30/M30T 不同固件可能上报枚举整数或布尔值,用 Object 兼容。
+     */
+    @JsonProperty("rc_lost_action")
+    private Object rcLostAction;
+
+    /**
+     * RID 状态。
+     * 注意:M30T 实测上报布尔值(false/true),部分固件上报整数,
+     * 原定义为 Integer 会导致整帧 OSD 反序列化失败(状态/遥测全部丢失),这里用 Object 兼容。
+     */
+    @JsonProperty("rid_state")
+    private Object ridState;
+
+    /**
+     * 返航高度。
+     */
+    @JsonProperty("rth_altitude")
+    private Integer rthAltitude;
+
+    /**
+     * 航线文件版本。
+     */
+    @JsonProperty("wpmz_version")
+    private String wpmzVersion;
+
+}
diff --git a/ard-sample/src/main/java/com/ard/sample/dock1/manage/service/ICommandService.java b/ard-sample/src/main/java/com/ard/sample/dock1/manage/service/ICommandService.java
new file mode 100644
index 0000000..c6f7f7a
--- /dev/null
+++ b/ard-sample/src/main/java/com/ard/sample/dock1/manage/service/ICommandService.java
@@ -0,0 +1,26 @@
+/*
+ * 【机场1】从 dock2 原类 com.ard.sample.manage.service.ICommandService 复制/独立适配(DJI Dock 1 / M30)。
+ * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。
+ */
+package com.ard.sample.dock1.manage.service;
+
+import com.ard.sample.common.model.ResponseResult;
+import com.ard.sample.manage.model.param.CommandParam;
+import org.springframework.messaging.MessageHeaders;
+
+/**
+ * @author sean
+ * @version 1.2
+ * @date 2022/7/29
+ */
+public interface ICommandService {
+
+    /**
+     * 负载控制-拍照
+     * @param sn
+     * @return
+     */
+    ResponseResult cameraPhotoTake(String sn, CommandParam param);
+
+
+}
diff --git a/ard-sample/src/main/java/com/ard/sample/dock1/manage/service/IControlService.java b/ard-sample/src/main/java/com/ard/sample/dock1/manage/service/IControlService.java
new file mode 100644
index 0000000..4bd6a43
--- /dev/null
+++ b/ard-sample/src/main/java/com/ard/sample/dock1/manage/service/IControlService.java
@@ -0,0 +1,35 @@
+/*
+ * 【机场1】从 dock2 原类 com.ard.sample.manage.service.IControlService 复制/独立适配(DJI Dock 1 / M30)。
+ * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。
+ */
+package com.ard.sample.dock1.manage.service;
+
+import com.ard.sample.common.model.ResponseResult;
+import com.ard.sample.component.mqtt.model.CommonTopicReceiver;
+import com.ard.sample.manage.model.param.RemoteDebugParam;
+import org.springframework.messaging.MessageHeaders;
+
+/**
+ * @author sean
+ * @version 1.2
+ * @date 2022/7/29
+ */
+public interface IControlService {
+
+    /**
+     * Remotely debug the dock via commands.
+     * @param sn
+     * @param serviceIdentifier
+     * @param param
+     * @return
+     */
+    ResponseResult controlDock(String sn, String serviceIdentifier, RemoteDebugParam param);
+
+    /**
+     * Handles multi-state command progress information.
+     * @param receiver
+     * @param headers
+     */
+    void handleControlProgress(CommonTopicReceiver receiver, MessageHeaders headers);
+
+}
diff --git a/ard-sample/src/main/java/com/ard/sample/dock1/manage/service/ILiveStreamService.java b/ard-sample/src/main/java/com/ard/sample/dock1/manage/service/ILiveStreamService.java
new file mode 100644
index 0000000..5e887b8
--- /dev/null
+++ b/ard-sample/src/main/java/com/ard/sample/dock1/manage/service/ILiveStreamService.java
@@ -0,0 +1,73 @@
+/*
+ * 【机场1】从 dock2 原类 com.ard.sample.manage.service.ILiveStreamService 复制/独立适配(DJI Dock 1 / M30)。
+ * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。
+ */
+package com.ard.sample.dock1.manage.service;
+
+import com.ard.sample.common.model.ResponseResult;
+import com.ard.sample.manage.model.dto.CapacityDeviceDTO;
+import com.ard.sample.manage.model.dto.LiveTypeDTO;
+import com.ard.sample.manage.model.receiver.LiveCapacityReceiver;
+
+import java.util.List;
+
+/**
+ * @author sean.zhou
+ * @date 2021/11/19
+ * @version 0.1
+ */
+public interface ILiveStreamService {
+
+    /**
+     * Get all the drone data that can be broadcast live in this workspace.
+     * @return
+     */
+    List<CapacityDeviceDTO> getLiveCapacity();
+
+    /**
+     * Save live capability data.
+     * @param liveCapacityReceiver
+     * @param timestamp
+     */
+    void saveLiveCapacity(LiveCapacityReceiver liveCapacityReceiver, Long timestamp);
+
+    /**
+     * Initiate a live streaming by publishing mqtt message.
+     * @param liveParam Parameters needed for on-demand.
+     * @return
+     */
+    ResponseResult liveStart(LiveTypeDTO liveParam);
+
+    /**
+     * Stop the live streaming by publishing mqtt message.
+     * @param videoId
+     * @return
+     */
+    ResponseResult liveStop(String videoId);
+
+    /**
+     * Readjust the clarity of the live streaming by publishing mqtt messages.
+     * @param liveParam
+     * @return
+     */
+    ResponseResult liveSetQuality(LiveTypeDTO liveParam);
+
+    /**
+     * Switches the lens of the device during the live streaming.
+     * @param liveParam
+     * @return
+     */
+    ResponseResult liveLensChange(LiveTypeDTO liveParam);
+
+    /**
+     * 获取直播能力中摄像头模式(广角、变焦、红外)
+     * */
+    ResponseResult<List<String>> getLiveLens(String sn);
+
+    /**
+     * 直播相机切换
+     * @param liveParam
+     * @return
+     */
+    ResponseResult liveCameraChange(LiveTypeDTO liveParam);
+}
diff --git a/ard-sample/src/main/java/com/ard/sample/dock1/manage/service/IWaylineTrajectoryService.java b/ard-sample/src/main/java/com/ard/sample/dock1/manage/service/IWaylineTrajectoryService.java
new file mode 100644
index 0000000..770c316
--- /dev/null
+++ b/ard-sample/src/main/java/com/ard/sample/dock1/manage/service/IWaylineTrajectoryService.java
@@ -0,0 +1,28 @@
+/*
+ * 【机场1】从 dock2 原类 com.ard.sample.manage.service.IWaylineTrajectoryService 复制/独立适配(DJI Dock 1 / M30)。
+ * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。
+ */
+package com.ard.sample.dock1.manage.service;
+
+import com.ard.sample.manage.model.dto.WaylineTrajectoryDTO;
+import com.ard.sample.manage.model.entity.WaylineTrajectoryEntity;
+
+import java.util.List;
+
+public interface IWaylineTrajectoryService {
+
+    /**
+     * 增加list数据,type为:0时,表示轨迹点列表;type为:1时,表示任务状态数据。
+     * @param sn 设备序列号
+     * @param type 类型
+     * @param dataJson json数据
+     * */
+    void setList(String sn, String type, String dataJson);
+
+    /**
+     * 返回航线轨迹完整的数据,拼接的轨迹点列表和任务状态数据。
+     * @param jobId 任务ID
+     * @return 航线轨迹完整的数据
+     * */
+    WaylineTrajectoryDTO getWayLineTrajectorybyJobID(String jobId);
+}
diff --git a/ard-sample/src/main/java/com/ard/sample/dock1/manage/service/impl/Dock1CommandServiceImpl.java b/ard-sample/src/main/java/com/ard/sample/dock1/manage/service/impl/Dock1CommandServiceImpl.java
new file mode 100644
index 0000000..d75d1a3
--- /dev/null
+++ b/ard-sample/src/main/java/com/ard/sample/dock1/manage/service/impl/Dock1CommandServiceImpl.java
@@ -0,0 +1,126 @@
+/*
+ * 【机场1】从 dock2 原类 com.ard.sample.manage.service.impl.CommandServiceImpl 复制/独立适配(DJI Dock 1 / M30)。
+ * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。
+ */
+package com.ard.sample.dock1.manage.service.impl;
+
+import com.ard.sample.common.error.CommonErrorEnum;
+import com.ard.sample.common.model.ResponseResult;
+import com.ard.sample.component.mqtt.model.*;
+import com.ard.sample.component.mqtt.service.IMessageSenderService;
+import com.ard.common.core.redis.RedisConst;
+import com.ard.common.core.redis.RedisOpsUtils;
+import com.ard.sample.manage.model.dto.DeviceDTO;
+import com.ard.sample.manage.model.param.CommandParam;
+import com.ard.sample.dock1.manage.service.ICommandService;
+import com.ard.sample.manage.service.IDeviceService;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.integration.mqtt.support.MqttHeaders;
+import org.springframework.messaging.MessageHeaders;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.UUID;
+import com.ard.sample.manage.service.impl.CommandServiceImpl;
+
+/**
+ * Description:
+ * Author: lsy
+ * Date: 2025年09月24日9:00
+ **/
+@Slf4j
+@Service
+public class Dock1CommandServiceImpl implements ICommandService {
+
+    @Autowired
+    private ObjectMapper mapper;
+    @Resource
+    private IDeviceService deviceService;
+    @Resource
+    private IMessageSenderService messageSenderService;
+    /**
+     * 负载控制-拍照
+     * @param sn
+     * @return
+     */
+    @Override
+    public ResponseResult cameraPhotoTake(String sn, CommandParam param) {
+
+        boolean isExist = deviceService.checkDeviceOnline(sn);
+        if (!isExist) {
+            return ResponseResult.error("The dock is offline.");
+        }
+        String topic = TopicConst.THING_MODEL_PRE + TopicConst.PRODUCT + sn + TopicConst.SERVICES_SUF;
+        String bid = UUID.randomUUID().toString();
+        ServiceReply serviceReplyOpt = messageSenderService.publishWithReply(
+                topic, CommonTopicResponse.builder()
+                        .tid(UUID.randomUUID().toString())
+                        .bid(bid)
+                        .method("camera_photo_take")
+                        .timestamp(System.currentTimeMillis())
+                        .data(param)
+                        .build());
+
+        ServiceReply<EventsOutputReceiver> serviceReply = mapper.convertValue(
+                serviceReplyOpt, new TypeReference<ServiceReply<EventsOutputReceiver>>() {});
+        if (ResponseResult.CODE_SUCCESS != serviceReply.getResult()) {
+            return ResponseResult.error(serviceReply.getResult(), CommonErrorEnum.getMsgByCode(serviceReply.getResult()));
+        }
+//        if (controlMethodEnum.getProgress()) {
+//            RedisOpsUtils.setWithExpire(serviceIdentifier + RedisConst.DELIMITER +  bid, sn,
+//                    RedisConst.DEVICE_ALIVE_SECOND * RedisConst.DEVICE_ALIVE_SECOND);
+//        }
+        return ResponseResult.success();
+    }
+
+    // 机场1:不复制原 ServiceActivator(inputChannel = ChannelName.INBOUND_EVENTS_CAMERA_PHOTO_TAKE_PROGRESS),消费职责由共享层原类承担
+    public void handleCameraPhotoTakeProgress(CommonTopicReceiver receiver, MessageHeaders headers) {
+        String key = receiver.getMethod() + RedisConst.DELIMITER + receiver.getBid();
+        if (RedisOpsUtils.getExpire(key) <= 0) {
+            return;
+        }
+        String sn = RedisOpsUtils.get(key).toString();
+
+        EventsReceiver<EventsOutputReceiver> eventsReceiver = mapper.convertValue(receiver.getData(),
+                new TypeReference<EventsReceiver<EventsOutputReceiver>>(){});
+        eventsReceiver.setBid(receiver.getBid());
+        eventsReceiver.setSn(sn);
+
+        log.info("SN: {}, {} ===> CameraPhotoTak progress: {}",
+                sn, receiver.getMethod(), eventsReceiver.getOutput().getProgress().toString());
+
+        if (eventsReceiver.getResult() != ResponseResult.CODE_SUCCESS) {
+            log.error("SN: {}, {} ===> Error code: {}", sn, receiver.getMethod(), eventsReceiver.getResult());
+        }
+
+        if (eventsReceiver.getOutput().getProgress().getPercent() == 100 ||
+                EventsResultStatusEnum.find(eventsReceiver.getOutput().getStatus()).getEnd()) {
+            RedisOpsUtils.del(key);
+        }
+
+        DeviceDTO device = (DeviceDTO) RedisOpsUtils.get(RedisConst.DEVICE_ONLINE_PREFIX + sn);
+//        webSocketMessageService.sendBatch(
+//                WebSocketManager.getValueWithWorkspaceAndUserType(
+//                        device.getWorkspaceId(), UserTypeEnum.WEB.getVal()),
+//                CustomWebSocketMessage.builder()
+//                        .data(eventsReceiver)
+//                        .timestamp(System.currentTimeMillis())
+//                        .bizCode(receiver.getMethod())
+//                        .build());
+
+        if (receiver.getNeedReply() != null && receiver.getNeedReply() == 1) {
+            String topic = headers.get(MqttHeaders.RECEIVED_TOPIC) + TopicConst._REPLY_SUF;
+            messageSenderService.publish(topic,
+                    CommonTopicResponse.builder()
+                            .tid(receiver.getTid())
+                            .bid(receiver.getBid())
+                            .method(receiver.getMethod())
+                            .timestamp(System.currentTimeMillis())
+                            .data(RequestsReply.success())
+                            .build());
+        }
+    }
+}
diff --git a/ard-sample/src/main/java/com/ard/sample/dock1/manage/service/impl/Dock1ControlServiceImpl.java b/ard-sample/src/main/java/com/ard/sample/dock1/manage/service/impl/Dock1ControlServiceImpl.java
new file mode 100644
index 0000000..fde35ab
--- /dev/null
+++ b/ard-sample/src/main/java/com/ard/sample/dock1/manage/service/impl/Dock1ControlServiceImpl.java
@@ -0,0 +1,246 @@
+/*
+ * 【机场1】从 dock2 原类 com.ard.sample.manage.service.impl.ControlServiceImpl 复制/独立适配(DJI Dock 1 / M30)。
+ * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。
+ */
+package com.ard.sample.dock1.manage.service.impl;
+
+import com.ard.sample.common.error.CommonErrorEnum;
+import com.ard.sample.common.model.ResponseResult;
+import com.ard.sample.component.mqtt.model.*;
+import com.ard.sample.component.mqtt.service.IMessageSenderService;
+import com.ard.common.core.redis.RedisConst;
+import com.ard.common.core.redis.RedisOpsUtils;
+import com.ard.sample.component.websocket.enums.MessageType;
+import com.ard.sample.component.websocket.handler.DockWebSocketHandler;
+import com.ard.sample.component.websocket.handler.LiveStreamHandler;
+import com.ard.sample.component.websocket.service.ISendMessageService;
+import com.ard.sample.manage.model.dto.BasicDeviceProperty;
+import com.ard.sample.manage.model.dto.DeviceDTO;
+import com.ard.sample.manage.model.enums.DeviceDomainEnum;
+import com.ard.sample.manage.model.enums.RemoteControlMethodEnum;
+import com.ard.sample.manage.model.param.RemoteDebugParam;
+import com.ard.sample.dock1.manage.service.IControlService;
+import com.ard.sample.manage.service.IDeviceService;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.integration.mqtt.support.MqttHeaders;
+import org.springframework.messaging.MessageHeaders;
+import org.springframework.stereotype.Service;
+import java.util.Objects;
+import java.util.UUID;
+import com.ard.sample.manage.service.impl.ControlServiceImpl;
+
+/**
+ * @author sean
+ * @version 1.2
+ * @date 2022/7/29
+ */
+@Service
+@Slf4j
+public class Dock1ControlServiceImpl implements IControlService {
+
+    /**
+     * 机场1:Dock1 设备类型。
+     */
+    private static final int DOCK1_DEVICE_TYPE = 1;
+
+    @Autowired
+    private IMessageSenderService messageSenderService;
+
+    @Autowired
+    private DockWebSocketHandler _dockWebSocketHandler;
+
+    @Autowired
+    private ISendMessageService webSocketMessageService;
+
+    @Autowired
+    private IDeviceService _deviceService;
+
+    @Autowired
+    private LiveStreamHandler _liveStreamHandler;
+
+//    @Autowired
+//    private IWebSocketManageService webSocketManageService;
+
+    @Autowired
+    private IDeviceService deviceService;
+
+    @Autowired
+    private ObjectMapper mapper;
+
+    @Override
+    public ResponseResult controlDock(String sn, String serviceIdentifier, RemoteDebugParam param) {
+        RemoteControlMethodEnum controlMethodEnum = RemoteControlMethodEnum.find(serviceIdentifier);
+        if (RemoteControlMethodEnum.UNKNOWN == controlMethodEnum) {
+            return ResponseResult.error("The " + serviceIdentifier + " method does not exist.");
+        }
+
+
+        Object data = "";
+        // Add parameter validation.
+        if (Objects.nonNull(controlMethodEnum.getClazz())) {
+            if (Objects.isNull(param)) {
+                return ResponseResult.error(CommonErrorEnum.ILLEGAL_ARGUMENT);
+            }
+            BasicDeviceProperty basicDeviceProperty = null;
+            if(serviceIdentifier.equals("sdr_workmode_switch")){
+                basicDeviceProperty= mapper.convertValue(param.getLink_workmode(), controlMethodEnum.getClazz());
+            }
+            else
+            {
+                basicDeviceProperty = mapper.convertValue(param.getAction(), controlMethodEnum.getClazz());
+            }
+            if (!basicDeviceProperty.valid()) {
+                return ResponseResult.error(CommonErrorEnum.ILLEGAL_ARGUMENT);
+            }
+            data = basicDeviceProperty;
+        }
+
+        boolean isExist = deviceService.checkDeviceOnline(sn);
+        if (!isExist) {
+            return ResponseResult.error(CommonErrorEnum.DEVICE_OFFLINE.getErrorCode(), CommonErrorEnum.DEVICE_OFFLINE.getErrorMsg());
+        }
+        DeviceDTO device = (DeviceDTO) RedisOpsUtils.get(RedisConst.DEVICE_ONLINE_PREFIX + sn);
+        // 机场1:/dock1 控制接口只允许操作 Dock1,避免机场2/3 的 SN 误走 dock1 链路。
+        if (device == null || !Objects.equals(DOCK1_DEVICE_TYPE, device.getType())
+                || !Objects.equals(DeviceDomainEnum.DOCK.getVal(), device.getDomain())) {
+            return ResponseResult.error(314013004, "该设备不属于机场1,请使用对应的机场接口操作!");
+        }
+        // 重启机场时清redis视频直播地址,机场重启不掉线。但是流停了。
+        if(RemoteControlMethodEnum.DEVICE_REBOOT == controlMethodEnum) {
+            // 重启机场时,读取舱盖状态,开舱盖不让重启,关舱盖重启
+            Integer cover_state = (Integer)RedisOpsUtils.get(RedisConst.COVER_STATE_OSD_PREFIX + device.getDeviceSn());
+            if(cover_state != null && cover_state != 0) {
+                return ResponseResult.error(111111,"请关闭舱盖再执行重启操作!");
+            }
+            RedisOpsUtils.del(RedisConst.WEBSOCKET_VIDEO_ADDRESS_PREFIX + sn);
+            RedisOpsUtils.del(RedisConst.COVER_STATE_PREFIX + sn);
+            RedisOpsUtils.del(RedisConst.WEBSOCKET_VIDEO_ADDRESS_PREFIX + device.getChildDeviceSn());
+        }
+        if(RemoteControlMethodEnum.DRONE_CLOSE == controlMethodEnum) {
+            // 飞行器关机时drone_close,关闭飞行器视频流
+            _liveStreamHandler.StopLiveStreaming(device.getChildDeviceSn());
+            RedisOpsUtils.del(RedisConst.WEBSOCKET_VIDEO_ADDRESS_PREFIX + device.getChildDeviceSn());
+            log.info("SN: {}-飞行器关机,停止直播并删除redis直播信息!", device.getChildDeviceSn());
+        }
+        String topic = TopicConst.THING_MODEL_PRE + TopicConst.PRODUCT + sn + TopicConst.SERVICES_SUF;
+        String bid = UUID.randomUUID().toString();
+        ServiceReply serviceReplyOpt = messageSenderService.publishWithReply(
+                topic, CommonTopicResponse.builder()
+                        .tid(UUID.randomUUID().toString())
+                        .bid(bid)
+                        .method(serviceIdentifier)
+                        .timestamp(System.currentTimeMillis())
+                        .data(data)
+                        .build());
+
+        ServiceReply<EventsOutputReceiver> serviceReply = mapper.convertValue(
+                serviceReplyOpt, new TypeReference<ServiceReply<EventsOutputReceiver>>() {});
+        if (ResponseResult.CODE_SUCCESS != serviceReply.getResult()) {
+            String errorMsg = CommonErrorEnum.getMsgByCode(serviceReply.getResult());
+            return ResponseResult.error(serviceReply.getResult(),errorMsg);
+//            return ResponseResult.error(serviceReply.getResult(),
+//                    Objects.nonNull(serviceReply.getOutput()) ? serviceReply.getOutput().getStatus() : "errorCode: "
+//                            + serviceReply.getResult());
+        }
+        if (controlMethodEnum.getProgress()) {
+            RedisOpsUtils.setWithExpire(serviceIdentifier + RedisConst.DELIMITER +  bid, sn,
+                    RedisConst.DEVICE_ALIVE_SECOND * RedisConst.DEVICE_ALIVE_SECOND);
+        }
+        // 执行操作成功后,更新摄像头内外镜头状态
+        // 机场1:Dock1 只有舱外单相机,camera_position 恒为 1,不存在舱内/舱外切换;
+        // 共享层在开盖时写 0 是 Dock2 双相机的语义,这里按 Dock1 修正。
+        if(RemoteControlMethodEnum.COVER_OPEN == controlMethodEnum) {
+            _deviceService.setRedisCameraPosition(sn,1);
+        }
+        if(RemoteControlMethodEnum.COVER_CLOSE == controlMethodEnum) {
+            _deviceService.setRedisCameraPosition(sn,1);
+        }
+
+        return ResponseResult.success();
+    }
+
+    @Override
+    // 机场1:不复制原 ServiceActivator(inputChannel = ChannelName.INBOUND_EVENTS_CONTROL_PROGRESS),消费职责由共享层原类承担
+    public void handleControlProgress(CommonTopicReceiver receiver, MessageHeaders headers) {
+        String key = receiver.getMethod() + RedisConst.DELIMITER + receiver.getBid();
+        if (RedisOpsUtils.getExpire(key) <= 0) {
+            return;
+        }
+        // 更新设备列表状态--关闭舱盖中...
+        String method = receiver.getMethod();
+        String deviceSn = receiver.getGateway();
+        if(method.equals("cover_close")){
+            _deviceService.updateWorkType(deviceSn, 2L);
+            RedisOpsUtils.set(RedisConst.OSD_STATUS_PREFIX + deviceSn, 2);
+        }
+        String sn = RedisOpsUtils.get(key).toString();
+        receiver.setType(MessageType.PROGRESS.getType());
+        EventsReceiver<EventsOutputReceiver> eventsReceiver = mapper.convertValue(receiver.getData(),
+                new TypeReference<EventsReceiver<EventsOutputReceiver>>(){});
+        eventsReceiver.setBid(receiver.getBid());
+        eventsReceiver.setGateway(sn);
+        eventsReceiver.setType(MessageType.PROGRESS.getType());
+        eventsReceiver.setMethod(receiver.getMethod());
+
+        log.info("SN: {}, {} ===> Control progress: {}",
+                sn, receiver.getMethod(), eventsReceiver.getOutput().getProgress().toString());
+
+        if (eventsReceiver.getResult() != ResponseResult.CODE_SUCCESS) {
+            log.error("SN: {}, {} ===> Error code: {}", sn, receiver.getMethod(), eventsReceiver.getResult());
+        }
+
+        if (eventsReceiver.getOutput().getProgress().getPercent() == 100 ||
+                EventsResultStatusEnum.find(eventsReceiver.getOutput().getStatus()).getEnd()) {
+            RedisOpsUtils.del(key);
+        }
+
+        DeviceDTO device = (DeviceDTO) RedisOpsUtils.get(RedisConst.DEVICE_ONLINE_PREFIX + sn);
+//        webSocketMessageService.sendBatch(
+//                WebSocketManager.getValueWithWorkspaceAndUserType(
+//                        device.getWorkspaceId(), UserTypeEnum.WEB.getVal()),
+//                CustomWebSocketMessage.builder()
+//                        .data(eventsReceiver)
+//                        .timestamp(System.currentTimeMillis())
+//                        .bizCode(receiver.getMethod())
+//                        .build());
+        // 更新设备列表状态--飞行器开机中...
+        if(method.equals("drone_open")){
+            _deviceService.updateWorkType(device.getChildDeviceSn(), 7L);
+            RedisOpsUtils.set(RedisConst.OSD_STATUS_PREFIX + device.getChildDeviceSn(), 7);
+        }
+        // 机场重启状态--ok后删除redis状态
+        if(method.equals("device_reboot")){
+            if(eventsReceiver.getOutput().getProgress().getPercent() == 100){
+                RedisOpsUtils.del(RedisConst.WEBSOCKET_VIDEO_ADDRESS_PREFIX + sn);
+                RedisOpsUtils.del(RedisConst.COVER_STATE_PREFIX + sn);
+                RedisOpsUtils.del(RedisConst.WEBSOCKET_VIDEO_ADDRESS_PREFIX + device.getChildDeviceSn());
+            }
+        }
+
+
+        String dockProgress ="";
+        try {
+            dockProgress = mapper.writeValueAsString(eventsReceiver);
+        }
+        catch (Exception e) {
+            e.printStackTrace();
+        }
+        // 设备控制进度消息websocket发给前端
+        _dockWebSocketHandler.sendMessageToSubscribers(sn, MessageType.PROGRESS.getType(), dockProgress, "");
+
+        if (receiver.getNeedReply() != null && receiver.getNeedReply() == 1) {
+            String topic = headers.get(MqttHeaders.RECEIVED_TOPIC) + TopicConst._REPLY_SUF;
+            messageSenderService.publish(topic,
+                    CommonTopicResponse.builder()
+                            .tid(receiver.getTid())
+                            .bid(receiver.getBid())
+                            .method(receiver.getMethod())
+                            .timestamp(System.currentTimeMillis())
+                            .data(RequestsReply.success())
+                            .build());
+        }
+    }
+}
diff --git a/ard-sample/src/main/java/com/ard/sample/dock1/manage/service/impl/Dock1DeviceOSDServiceImpl.java b/ard-sample/src/main/java/com/ard/sample/dock1/manage/service/impl/Dock1DeviceOSDServiceImpl.java
new file mode 100644
index 0000000..6b8d784
--- /dev/null
+++ b/ard-sample/src/main/java/com/ard/sample/dock1/manage/service/impl/Dock1DeviceOSDServiceImpl.java
@@ -0,0 +1,142 @@
+/*
+ * 【机场1】从 dock2 原类 com.ard.sample.manage.service.impl.DeviceOSDServiceImpl 复制/独立适配(DJI Dock 1 / M30)。
+ * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。
+ */
+package com.ard.sample.dock1.manage.service.impl;
+
+import com.ard.common.core.redis.RedisConst;
+import com.ard.common.core.redis.RedisOpsUtils;
+import com.ard.sample.component.mqtt.model.CommonTopicReceiver;
+
+import com.ard.sample.component.websocket.config.ConcurrentWebSocketSession;
+import com.ard.sample.component.websocket.enums.MessageType;
+import com.ard.sample.component.websocket.handler.DockWebSocketHandler;
+import com.ard.sample.component.websocket.model.BizCodeEnum;
+import com.ard.sample.component.websocket.model.CustomWebSocketMessage;
+import com.ard.sample.manage.model.dto.DeviceDTO;
+import com.ard.sample.manage.model.dto.TelemetryDTO;
+import com.ard.sample.manage.model.dto.TelemetryDeviceDTO;
+import com.ard.sample.manage.model.enums.DeviceDomainEnum;
+import com.ard.sample.manage.model.receiver.OsdSubDeviceReceiver;
+// 机场1:使用带 M30 专属字段的子类解析飞行器 OSD(仍兼容共享层类型)。
+import com.ard.sample.dock1.manage.model.receiver.Dock1OsdSubDeviceReceiver;
+import com.ard.sample.manage.service.IDeviceService;
+// 机场1:轨迹缓存必须与共享任务进度(FlightTaskServiceImpl 的 type="1" 写入)使用同一个服务实例,
+// 否则 type="0" 的轨迹点写进 dock1 独立缓存、而 type="1" 的任务状态写在共享缓存,两处永远无法配对。
+// 这里复用共享层 IWaylineTrajectoryService,不修改共享层实现。
+import com.ard.sample.manage.service.IWaylineTrajectoryService;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.stereotype.Service;
+
+import java.util.Collection;
+import com.ard.sample.manage.service.impl.AbstractTSAService;
+import com.ard.sample.manage.service.impl.DeviceOSDServiceImpl;
+
+/**
+ * @author sean
+ * @version 0.3
+ * @date 2022/2/21
+ */
+@Service
+@Slf4j
+public class Dock1DeviceOSDServiceImpl extends AbstractTSAService {
+
+    @Autowired
+    private IDeviceService _deviceService;
+
+    @Autowired
+    private IWaylineTrajectoryService _waylineTrajectoryService;
+
+    @Autowired
+    private DockWebSocketHandler _dockWebSocketHandler;
+
+    protected Dock1DeviceOSDServiceImpl(@Autowired @Qualifier("dock1DockOSDServiceImpl") AbstractTSAService tsaService) {
+        super(tsaService);
+    }
+
+    @Override
+    public void pushTelemetryData(Collection<ConcurrentWebSocketSession> sessions,
+                                  CustomWebSocketMessage<TelemetryDTO> message, Object osdData) {
+        if (osdData instanceof OsdSubDeviceReceiver) {
+            OsdSubDeviceReceiver data = (OsdSubDeviceReceiver) osdData;
+            TelemetryDTO telemetry = message.getData();
+            telemetry.setHost(TelemetryDeviceDTO.builder()
+                    .latitude(data.getLatitude())
+                    .longitude(data.getLongitude())
+                    .altitude(data.getElevation())
+                    .attitudeHead(data.getAttitudeHead())
+                    .elevation(data.getElevation())
+                    .horizontalSpeed(data.getHorizontalSpeed())
+                    .verticalSpeed(data.getVerticalSpeed())
+                    .build());
+
+            this.sendMessageService.sendBatch(sessions, message);
+        }
+    }
+    @Override
+    public void handleOSD(CommonTopicReceiver receiver, DeviceDTO device,
+                          Collection<ConcurrentWebSocketSession> webSessions,
+                          CustomWebSocketMessage<TelemetryDTO> wsMessage) {
+        if (DeviceDomainEnum.SUB_DEVICE.getVal() == device.getDomain()) {
+            wsMessage.setBizCode(BizCodeEnum.DEVICE_OSD.getCode());
+
+            // 机场1:M30/M30T 的 OSD 比共享类多 9 个字段,用 dock1 子类承接。
+            // 机场1:Dock1 的飞行器 OSD 中 payloads 本身就是数组,convertValue 已经完成解析;
+            // 共享层按 device.payloadsList(扁平 key)二次覆盖的逻辑不适用于 dock1,
+            // 且该列表当前恒为 null,会在每条 OSD 上触发 NPE 并打印误导性告警,故 dock1 链直接跳过。
+            // 容错:单个字段类型异常(如固件把 rid_state 上报成布尔)不应丢弃整帧 OSD,
+            // 解析失败时降级为共享接收器,保证状态、遥测、轨迹链路继续工作。
+            OsdSubDeviceReceiver data;
+            try {
+                data = mapper.convertValue(receiver.getData(), Dock1OsdSubDeviceReceiver.class);
+            } catch (IllegalArgumentException e) {
+                log.warn("机场1 飞行器 OSD 解析失败,降级为共享接收器:{}", e.getMessage());
+                data = mapper.convertValue(receiver.getData(), OsdSubDeviceReceiver.class);
+            }
+            //data.setChildDeviceSn(device.getDeviceSn());
+            String dockOsd = "";
+            receiver.setType(MessageType.STATUS.getType()+"-aircraft");
+//            receiver.setData(data);
+            try {
+                dockOsd=mapper.writeValueAsString(receiver);
+                JsonNode jsonNode = mapper.readTree(dockOsd);
+                //在JSON的data下插入childDeviceSn,飞行器的sn
+                if(jsonNode.get("data") instanceof ObjectNode){
+                    ObjectNode objectNode = (ObjectNode) jsonNode.get("data");
+                    objectNode.put("childDeviceSn", device.getDeviceSn());
+                }
+                if(jsonNode.has("data")){
+
+                    if(jsonNode.get("data").has("mode_code")) {
+                        Long code = jsonNode.get("data").get("mode_code").asLong();
+                        if ("0,1,2,3,4,5,8,9,10,11,13,14".contains(code.toString())) {
+                            _deviceService.updateWorkType(device.getDeviceSn(), code);
+                            RedisOpsUtils.set(RedisConst.OSD_STATUS_PREFIX + device.getDeviceSn(), code);
+                        }
+                    }
+                }
+                dockOsd=mapper.writeValueAsString(jsonNode);
+            } catch (NullPointerException | JsonProcessingException e) {
+                log.warn("Please remount the payload, or restart the drone. Otherwise the data of the payload will not be received.");
+            }
+
+            RedisOpsUtils.setWithExpire(RedisConst.OSD_PREFIX + device.getDeviceSn(), data, RedisConst.DEVICE_ALIVE_SECOND);
+            wsMessage.getData().setHost(data);
+
+            // 发送给websocket客户端
+            _dockWebSocketHandler.sendMessageToSubscribers(device.getDeviceSn(), MessageType.STATUS.getType(), dockOsd, "");
+
+            // 将飞行器的OSD数据dockOsd入库;device.getDeviceSn()为飞行器sn,这里要机场的sn,试试receiver.getGateway();
+            _waylineTrajectoryService.setList(receiver.getGateway(),"0", dockOsd);
+
+            sendMessageService.sendBatch(webSessions, wsMessage);
+            this.pushTelemetryData(data, device.getDeviceSn());
+        }
+        tsaService.handleOSD(receiver, device, webSessions, wsMessage);
+    }
+}
diff --git a/ard-sample/src/main/java/com/ard/sample/dock1/manage/service/impl/Dock1DockOSDServiceImpl.java b/ard-sample/src/main/java/com/ard/sample/dock1/manage/service/impl/Dock1DockOSDServiceImpl.java
new file mode 100644
index 0000000..4180808
--- /dev/null
+++ b/ard-sample/src/main/java/com/ard/sample/dock1/manage/service/impl/Dock1DockOSDServiceImpl.java
@@ -0,0 +1,125 @@
+/*
+ * 【机场1】从 dock2 原类 com.ard.sample.manage.service.impl.DockOSDServiceImpl 复制/独立适配(DJI Dock 1 / M30)。
+ * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。
+ */
+package com.ard.sample.dock1.manage.service.impl;
+
+import com.ard.common.core.redis.RedisConst;
+import com.ard.common.core.redis.RedisOpsUtils;
+import com.ard.sample.component.mqtt.model.CommonTopicReceiver;
+import com.ard.sample.component.websocket.config.ConcurrentWebSocketSession;
+import com.ard.sample.component.websocket.enums.MessageType;
+import com.ard.sample.component.websocket.handler.DockWebSocketHandler;
+import com.ard.sample.component.websocket.model.BizCodeEnum;
+import com.ard.sample.component.websocket.model.CustomWebSocketMessage;
+import com.ard.sample.manage.model.dto.DeviceDTO;
+import com.ard.sample.manage.model.dto.TelemetryDTO;
+import com.ard.sample.manage.model.enums.DeviceDomainEnum;
+import com.ard.sample.dock1.manage.model.receiver.Dock1OsdDockReceiver;
+import com.ard.sample.manage.service.IDeviceService;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.Collection;
+import com.ard.sample.manage.service.impl.AbstractTSAService;
+import com.ard.sample.manage.service.impl.DockOSDServiceImpl;
+
+/**
+ * @author sean
+ * @version 1.0
+ * @date 2022/5/11
+ */
+@Service
+public class Dock1DockOSDServiceImpl extends AbstractTSAService {
+
+    @Autowired
+    private DockWebSocketHandler _dockWebSocketHandler;
+
+    @Autowired
+    private IDeviceService _deviceService;
+
+    public Dock1DockOSDServiceImpl() {
+        super(null);
+    }
+
+
+    @Override
+    public void pushTelemetryData(Collection<ConcurrentWebSocketSession> sessions,
+                                  CustomWebSocketMessage<TelemetryDTO> message, Object Object) {
+
+    }
+
+    @Override
+    public void handleOSD(CommonTopicReceiver receiver, DeviceDTO device,
+                          Collection<ConcurrentWebSocketSession> webSessions,
+                          CustomWebSocketMessage<TelemetryDTO> wsMessage) {
+
+        if (DeviceDomainEnum.DOCK.getVal() == device.getDomain()) {
+            wsMessage.setBizCode(BizCodeEnum.DOCK_OSD.getCode());
+            Dock1OsdDockReceiver data = mapper.convertValue(receiver.getData(), Dock1OsdDockReceiver.class);
+//            receiver.setType(MessageType.STATUS.getType());
+            String dockOsd = "";
+            String dockOsdData = "";
+            try {
+                dockOsdData = mapper.writeValueAsString(receiver.getData());
+                if (dockOsdData.contains("flighttask_step_code")) {
+                    JsonNode jsonNode = mapper.readTree(dockOsdData);
+                    if (jsonNode.has("flighttask_step_code")) {
+                        String deviceSn = device.getDeviceSn();
+                        Long code = jsonNode.get("flighttask_step_code").asLong();
+                        if (code != null) {
+                            //只要状态 "0":"作业准备中","1":"飞行作业中","5":"任务空闲"
+                            if (code == 0 || code == 1 || code == 5) {
+                                _deviceService.updateWorkType(deviceSn, code);
+                                RedisOpsUtils.set(RedisConst.OSD_STATUS_PREFIX + deviceSn, code);
+                            }
+                        }
+                    }
+                    receiver.setType(MessageType.STATUS.getType() + "-flighttask");
+                } else if (dockOsdData.contains("network_state")) {
+                    JsonNode jsonNode = mapper.readTree(dockOsdData);
+                    // 机场1:Dock1 刚上线时 network_state 可能不带 sub_device(飞行器尚未入舱),
+                    // 原写法直接 jsonNode.get("sub_device").has(...) 会抛 NPE,导致整帧 OSD 丢失。
+                    JsonNode subDeviceNode = jsonNode.get("sub_device");
+                    JsonNode droneInDockNode = jsonNode.get("drone_in_dock");
+                    if (subDeviceNode != null && !subDeviceNode.isNull()
+                            && droneInDockNode != null && !droneInDockNode.isNull()
+                            && subDeviceNode.has("device_online_status")
+                            && subDeviceNode.has("device_sn")) {
+                        String childDeviceSn = subDeviceNode.get("device_sn").asText();
+                        Long inDock = droneInDockNode.asLong();
+                        Long deviceOnlineStatus = subDeviceNode.get("device_online_status").asLong();
+                        if (inDock.equals(1L) && deviceOnlineStatus.equals(0L)) {// 飞行器在舱内且关机状态为6
+                            _deviceService.updateWorkType(childDeviceSn, 6L);
+                            RedisOpsUtils.set(RedisConst.OSD_STATUS_PREFIX + device.getChildDeviceSn(), 6);
+                        }
+                    }
+                    if (jsonNode.has("cover_state")) {
+                        // 舱盖状态存redis
+                        RedisOpsUtils.set(RedisConst.COVER_STATE_OSD_PREFIX + device.getDeviceSn(), jsonNode.get("cover_state").asInt());
+//                        //实时监控机场内外舱盖状态
+//                        Integer coverState = jsonNode.get("cover_state").asInt();
+//                        if(coverState != null && (coverState == 1 || coverState == 0)) {
+//                            // 执行0和1的互转逻辑,结果赋值给变量(保持Integer类型)
+//                            Integer newCoverState = coverState == 0 ? 1 : 0;
+//                            _deviceService.setRedisCameraPosition(device.getDeviceSn(), newCoverState);
+//                        }
+
+                    }
+                    receiver.setType(MessageType.STATUS.getType() + "-network");
+                } else if (dockOsdData.contains("job_number")) {
+                    receiver.setType(MessageType.STATUS.getType() + "-job");
+                }
+                dockOsd = mapper.writeValueAsString(receiver);
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+            wsMessage.getData().setHost(data);
+            sendMessageService.sendBatch(webSessions, wsMessage);
+            // 发送给websocket客户端
+            _dockWebSocketHandler.sendMessageToSubscribers(device.getDeviceSn(), MessageType.STATUS.getType(), dockOsd, "");
+        }
+    }
+}
diff --git a/ard-sample/src/main/java/com/ard/sample/dock1/manage/service/impl/Dock1FlightSetupExceptionHandler.java b/ard-sample/src/main/java/com/ard/sample/dock1/manage/service/impl/Dock1FlightSetupExceptionHandler.java
new file mode 100644
index 0000000..c997f22
--- /dev/null
+++ b/ard-sample/src/main/java/com/ard/sample/dock1/manage/service/impl/Dock1FlightSetupExceptionHandler.java
@@ -0,0 +1,68 @@
+/*
+ * 【机场1】航线准备异常通知消费者(flight_setup_exception_notify)。
+ * Dock1/M30 在 flighttask_prepare 失败时会发送该事件;共享层原枚举未注册此方法,
+ * 消息会被路由到 default 通道后丢弃。这里新增独立通道与 dock1 消费者,
+ * 仅处理 Dock1 设备,机场2/3 收到同名事件时保持原行为(只记录日志)。
+ */
+package com.ard.sample.dock1.manage.service.impl;
+
+import com.ard.sample.component.mqtt.model.ChannelName;
+import com.ard.sample.component.mqtt.model.CommonTopicReceiver;
+import com.ard.sample.component.websocket.enums.MessageType;
+import com.ard.sample.component.websocket.handler.DockWebSocketHandler;
+import com.ard.sample.manage.model.dto.DeviceDTO;
+import com.ard.sample.manage.service.IDeviceService;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.integration.annotation.ServiceActivator;
+import org.springframework.messaging.MessageHeaders;
+import org.springframework.stereotype.Service;
+
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * 机场1航线准备异常通知处理器。
+ *
+ * @author ard
+ */
+@Slf4j
+@Service
+public class Dock1FlightSetupExceptionHandler {
+
+    /**
+     * 机场1:Dock1 设备类型。
+     */
+    private static final int DOCK1_DEVICE_TYPE = 1;
+
+    @Autowired
+    private IDeviceService deviceService;
+
+    @Autowired
+    private DockWebSocketHandler dockWebSocketHandler;
+
+    @Autowired
+    private ObjectMapper mapper;
+
+    @ServiceActivator(inputChannel = ChannelName.INBOUND_EVENTS_FLIGHT_SETUP_EXCEPTION_NOTIFY)
+    public void handleFlightSetupException(CommonTopicReceiver receiver, MessageHeaders headers) {
+        String dockSn = receiver.getGateway();
+        Optional<DeviceDTO> deviceOpt = deviceService.getDeviceBySn(dockSn);
+        if (deviceOpt.isEmpty() || !Objects.equals(DOCK1_DEVICE_TYPE, deviceOpt.get().getType())) {
+            // 机场2/3 的事件仍按原逻辑只记录日志,不改变其行为。
+            log.info("非机场1设备的航线准备异常通知,忽略。sn={}, method={}, data={}",
+                    dockSn, receiver.getMethod(), receiver.getData());
+            return;
+        }
+
+        log.warn("机场1航线准备异常:sn={}, bid={}, data={}", dockSn, receiver.getBid(), receiver.getData());
+        try {
+            // 原样推送给前端,便于机场1页面展示准备失败原因。
+            dockWebSocketHandler.sendMessageToSubscribers(dockSn, MessageType.STATUS.getType(),
+                    mapper.writeValueAsString(receiver), "");
+        } catch (Exception e) {
+            log.error("机场1航线准备异常通知推送失败:sn={}", dockSn, e);
+        }
+    }
+}
diff --git a/ard-sample/src/main/java/com/ard/sample/dock1/manage/service/impl/Dock1GatewayOSDServiceImpl.java b/ard-sample/src/main/java/com/ard/sample/dock1/manage/service/impl/Dock1GatewayOSDServiceImpl.java
new file mode 100644
index 0000000..3780846
--- /dev/null
+++ b/ard-sample/src/main/java/com/ard/sample/dock1/manage/service/impl/Dock1GatewayOSDServiceImpl.java
@@ -0,0 +1,70 @@
+/*
+ * 【机场1】从 dock2 原类 com.ard.sample.manage.service.impl.GatewayOSDServiceImpl 复制/独立适配(DJI Dock 1 / M30)。
+ * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。
+ */
+package com.ard.sample.dock1.manage.service.impl;
+
+import com.ard.sample.component.mqtt.model.CommonTopicReceiver;
+import com.ard.sample.component.websocket.config.ConcurrentWebSocketSession;
+import com.ard.sample.component.websocket.model.BizCodeEnum;
+import com.ard.sample.component.websocket.model.CustomWebSocketMessage;
+import com.ard.sample.manage.model.dto.DeviceDTO;
+import com.ard.sample.manage.model.dto.TelemetryDTO;
+import com.ard.sample.manage.model.dto.TelemetryDeviceDTO;
+import com.ard.sample.manage.model.enums.DeviceDomainEnum;
+import com.ard.sample.manage.model.receiver.OsdGatewayReceiver;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.stereotype.Service;
+
+import java.util.Collection;
+import com.ard.sample.manage.service.impl.AbstractTSAService;
+import com.ard.sample.manage.service.impl.GatewayOSDServiceImpl;
+
+/**
+ * @author sean
+ * @version 0.3
+ * @date 2022/2/21
+ */
+@Service
+public class Dock1GatewayOSDServiceImpl extends AbstractTSAService {
+
+    public Dock1GatewayOSDServiceImpl(@Autowired @Qualifier("dock1DeviceOSDServiceImpl") AbstractTSAService tsaService) {
+        super(tsaService);
+    }
+
+    @Override
+    public void pushTelemetryData(Collection<ConcurrentWebSocketSession> sessions,
+                                  CustomWebSocketMessage<TelemetryDTO> message, Object osdData) {
+        if (osdData instanceof OsdGatewayReceiver) {
+            OsdGatewayReceiver data = (OsdGatewayReceiver) osdData;
+            TelemetryDTO telemetry = message.getData();
+            telemetry.setHost(TelemetryDeviceDTO.builder()
+                    .latitude(data.getLatitude())
+                    .longitude(data.getLongitude())
+                    .build());
+            this.sendMessageService.sendBatch(sessions, message);
+            return;
+        }
+        tsaService.pushTelemetryData(sessions, message, osdData);
+    }
+
+    @Override
+    public void handleOSD(CommonTopicReceiver receiver, DeviceDTO device,
+                          Collection<ConcurrentWebSocketSession> webSessions,
+                          CustomWebSocketMessage<TelemetryDTO> wsMessage) {
+        if (DeviceDomainEnum.GATEWAY.getVal() == device.getDomain()) {
+
+            wsMessage.setBizCode(BizCodeEnum.GATEWAY_OSD.getCode());
+            OsdGatewayReceiver data = mapper.convertValue(receiver.getData(), OsdGatewayReceiver.class);
+            wsMessage.getData().setHost(data);
+
+            this.sendMessageService.sendBatch(webSessions, wsMessage);
+
+            this.pushTelemetryData(data, device.getDeviceSn());
+            return;
+        }
+
+        tsaService.handleOSD(receiver, device, webSessions, wsMessage);
+    }
+}
diff --git a/ard-sample/src/main/java/com/ard/sample/dock1/manage/service/impl/Dock1LiveStreamServiceImpl.java b/ard-sample/src/main/java/com/ard/sample/dock1/manage/service/impl/Dock1LiveStreamServiceImpl.java
new file mode 100644
index 0000000..4f4d709
--- /dev/null
+++ b/ard-sample/src/main/java/com/ard/sample/dock1/manage/service/impl/Dock1LiveStreamServiceImpl.java
@@ -0,0 +1,544 @@
+/*
+ * 【机场1】从 dock2 原类 com.ard.sample.manage.service.impl.LiveStreamServiceImpl 复制/独立适配(DJI Dock 1 / M30)。
+ * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。
+ */
+package com.ard.sample.dock1.manage.service.impl;
+
+import com.ard.sample.common.error.CommonErrorEnum;
+import com.ard.sample.common.error.LiveErrorEnum;
+import com.ard.sample.common.model.ResponseResult;
+import com.ard.sample.component.mqtt.model.CommonTopicResponse;
+import com.ard.sample.component.mqtt.model.ServiceReply;
+import com.ard.sample.component.mqtt.service.IMessageSenderService;
+import com.ard.common.core.redis.RedisConst;
+import com.ard.common.core.redis.RedisOpsUtils;
+import com.ard.sample.manage.model.dto.*;
+import com.ard.sample.manage.model.enums.DeviceDomainEnum;
+import com.ard.sample.manage.model.enums.LiveStreamMethodEnum;
+import com.ard.sample.manage.model.enums.LiveUrlTypeEnum;
+import com.ard.sample.manage.model.enums.LiveVideoQualityEnum;
+import com.ard.sample.manage.model.param.DeviceQueryParam;
+import com.ard.sample.manage.model.receiver.CapacityDeviceReceiver;
+import com.ard.sample.manage.model.receiver.LiveCapacityReceiver;
+import com.ard.sample.manage.service.ICapacityCameraService;
+import com.ard.sample.manage.service.IDeviceService;
+import com.ard.sample.dock1.manage.service.ILiveStreamService;
+import com.ard.vtdu.config.MediamtxConfig;
+import com.ard.vtdu.domain.ArdVtdu;
+import com.ard.vtdu.service.IArdVtduService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+
+import javax.annotation.Resource;
+import java.lang.reflect.Field;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+
+import static com.ard.sample.component.mqtt.model.TopicConst.*;
+import com.ard.sample.manage.service.impl.LiveStreamServiceImpl;
+
+/**
+ * @author sean.zhou
+ * @version 0.1
+ * @date 2021/11/22
+ */
+@Slf4j
+@Service
+//@Transactional
+public class Dock1LiveStreamServiceImpl implements ILiveStreamService {
+
+    /**
+     * 机场1:Dock1 设备类型。
+     */
+    private static final int DOCK1_DOCK_TYPE = 1;
+
+    /**
+     * 机场1:M30 / M30T 飞行器设备类型。
+     */
+    private static final int DOCK1_AIRCRAFT_TYPE = 67;
+
+    @Autowired
+    private ICapacityCameraService capacityCameraService;
+
+    @Autowired
+    private IDeviceService deviceService;
+
+    @Autowired
+    private IMessageSenderService messageSender;
+
+    @Resource
+    private IArdVtduService ardVtduService;
+    @Resource
+    private MediamtxConfig mediamtxConfig;
+
+    @Override
+    public List<CapacityDeviceDTO> getLiveCapacity() {
+
+        // Query all devices in this workspace.
+        List<DeviceDTO> devicesList = deviceService.getDevicesByParams(
+                DeviceQueryParam.builder()
+                        .domains(List.of(DeviceDomainEnum.SUB_DEVICE.getVal(), DeviceDomainEnum.DOCK.getVal()))
+                        .build());
+
+        // 机场1:只返回 Dock1(type=1)和 M30/M30T(type=67)的直播能力,
+        // 避免 /dock1/live/capacity 混入机场2/3 的设备。
+        return devicesList.stream()
+                .filter(device -> Objects.equals(DOCK1_DOCK_TYPE, device.getType())
+                        || Objects.equals(DOCK1_AIRCRAFT_TYPE, device.getType()))
+                .filter(device -> RedisOpsUtils.checkExist(RedisConst.DEVICE_ONLINE_PREFIX + device.getDeviceSn()))
+                .map(device -> CapacityDeviceDTO.builder()
+                        .name(Objects.requireNonNullElse(device.getNickname(), device.getDeviceName()))
+                        .sn(device.getDeviceSn())
+                        .camerasList(capacityCameraService.getCapacityCameraByDeviceSn(device.getDeviceSn()))
+                        .build())
+                .collect(Collectors.toList());
+    }
+
+    @Override
+    public void saveLiveCapacity(LiveCapacityReceiver liveCapacityReceiver, Long timestamp) {
+        // Solve timing problems
+        if (liveCapacityReceiver == null) {
+            //2025-10-28 zwy,这里有个空指针异常问题,如果liveCapacityReceiver为空,则不应该执行下面的代码。
+            //机场设备在刚开机时,发送的state状态时,直播能力前三次发送的是空,可能是由于刚开机,等待各设备开机,第四次为正常数据。
+            return;
+        }
+        for (CapacityDeviceReceiver capacityDeviceReceiver : liveCapacityReceiver.getDeviceList()) {
+            long last = (long) Objects.requireNonNullElse(
+                    RedisOpsUtils.get(RedisConst.LIVE_CAPACITY + capacityDeviceReceiver.getSn()), 0L);
+            if (last > timestamp) {
+                return;
+            }
+            capacityCameraService.saveCapacityCameraReceiverList(
+                    capacityDeviceReceiver.getCameraList(),
+                    capacityDeviceReceiver.getSn(), timestamp);
+        }
+    }
+
+    @Override
+    public ResponseResult liveStart(LiveTypeDTO liveParam) {
+        String rtmpPublishUrl =
+                "rtmp://" + mediamtxConfig.getPushHost() + ":" + mediamtxConfig.getRtmpPort() + "/" + liveParam.getVideoId();
+        liveParam.setUrl(rtmpPublishUrl);
+        // Check if this lens is available live.
+        ResponseResult responseResult = this.checkBeforeLive(liveParam.getVideoId());
+        if (ResponseResult.CODE_SUCCESS != responseResult.getCode()) {
+            return responseResult;
+        }
+
+        // 将机场推流代码放到入库代码后面。
+        DeviceDTO data = (DeviceDTO) responseResult.getData();
+        // target topic
+        String respTopic = THING_MODEL_PRE + PRODUCT +
+                data.getDeviceSn() + SERVICES_SUF;
+        ServiceReply receiveReply=new ServiceReply();
+//        ServiceReply receiveReply = this.publishLiveStart(respTopic, liveParam);
+
+//        if (ResponseResult.CODE_SUCCESS != receiveReply.getResult()) {
+//            log.info("机场直播开启失败,错误码:{},错误描述:{}", receiveReply.getResult(), LiveErrorEnum.find(receiveReply.getResult()).getErrorMsg());
+//            return ResponseResult.error(receiveReply.getResult(), LiveErrorEnum.find(receiveReply.getResult()).getErrorMsg());
+//        }
+
+        LiveUrlTypeEnum urlType = LiveUrlTypeEnum.find(liveParam.getUrlType());
+        LiveDTO live = new LiveDTO();
+
+        switch (urlType) {
+            case RTMP:
+                String name = liveParam.getVideoId();
+                // 拼接地址
+                String rtspUrl =
+                        "rtsp://" + mediamtxConfig.getPlayHost() + ":" + mediamtxConfig.getRtspPort() + "/" + name;  //
+                // 拼接 webrtcUrl
+                String rtmpUrl =
+                        "rtmp://" + mediamtxConfig.getPlayHost() + ":" + mediamtxConfig.getRtmpPort() + "/" + name;  //
+                // 拼接 webrtcUrl
+                String protocol = mediamtxConfig.isHttps() ? "https://" : "http://";
+                String webrtcUrl =
+                        protocol + mediamtxConfig.getPlayHost() + ":" + mediamtxConfig.getWebrtcPort() + "/" + name;  // 拼接
+                // webrtcUrl
+
+                //不管插入数据库成不成功,都返回地址
+                live.setRtmpUrl(rtmpUrl);
+                live.setRtspUrl(rtspUrl);
+                live.setWebrtcUrl(webrtcUrl);
+
+                // 如果没有,插入新的记录
+                ArdVtdu vtdu =
+                        ArdVtdu.builder().name(name).rtspSource("publisher").rtspUrl(rtspUrl).rtmpUrl(rtmpUrl).webrtcUrl(webrtcUrl).build();
+                // 在停止直播时不删除Vtdu表记录,所以在开启直播时再插入会报错,
+                // 所以要先查询是否存在该记录,如果已经存在记录,直接返回直播地址
+                // 查询是否已经存在该记录
+                ArdVtdu ardVtdu = ardVtduService.selectArdVtduByName(vtdu.getName());
+                int i = 0;
+                log.info("机场视频插入前vtdu:{}",vtdu);
+                if(ardVtdu == null){
+                    i = ardVtduService.insertArdVtdu(vtdu);  // 保存到媒体表
+                    log.info("机场视频插入后 i:{}",i);
+                    if (i > 0) {
+                        live.setRtmpUrl(rtmpUrl);
+                        live.setRtspUrl(rtspUrl);
+                        live.setWebrtcUrl(webrtcUrl);
+                    }
+                }
+                else{
+                    i = ardVtduService.updateArdVtdu(vtdu);
+                    log.info("机场视频更新后 i:{}",i);
+                    if (i > 0) {
+                        live.setRtmpUrl(rtmpUrl);
+                        live.setRtspUrl(rtspUrl);
+                        live.setWebrtcUrl(webrtcUrl);
+                    }
+                }
+                //int i = ardVtduService.insertArdVtdu(vtdu);  // 保存到媒体表
+//                if (i > 0) {
+//                    live.setRtmpUrl(rtmpUrl);
+//                    live.setRtspUrl(rtspUrl);
+//                    live.setWebrtcUrl(webrtcUrl);
+//                }
+                receiveReply = this.publishLiveStart(respTopic, liveParam);
+
+                if (ResponseResult.CODE_SUCCESS != receiveReply.getResult()) {
+                    log.info("机场直播开启失败,错误码:{},错误描述:{}", receiveReply.getResult(), LiveErrorEnum.find(receiveReply.getResult()).getErrorMsg());
+                    return ResponseResult.error(receiveReply.getResult(), LiveErrorEnum.find(receiveReply.getResult()).getErrorMsg());
+                }
+                break;
+            case RTSP:
+                receiveReply = this.publishLiveStart(respTopic, liveParam);
+
+                if (ResponseResult.CODE_SUCCESS != receiveReply.getResult()) {
+                    log.info("机场直播开启失败,错误码:{},错误描述:{}", receiveReply.getResult(), LiveErrorEnum.find(receiveReply.getResult()).getErrorMsg());
+                    return ResponseResult.error(receiveReply.getResult(), LiveErrorEnum.find(receiveReply.getResult()).getErrorMsg());
+                }
+                Object url = Objects.requireNonNullElse(receiveReply.getOutput(), receiveReply.getInfo());
+                this.resolveUrlUser(String.valueOf(url), live);
+                break;
+            case UNKNOWN:
+                return ResponseResult.error(LiveErrorEnum.URL_TYPE_NOT_SUPPORTED);
+        }
+
+
+
+        // 开始直播时发送机场相机摄像头默认信息(舱外)到所有在线用户,只取机场sn,发机场的摄像头信息
+        if(data.getChildDeviceSn()!=null) {
+            String sn = data.getDeviceSn();
+            deviceService.setRedisCameraPosition(sn, 1);
+        }
+        return ResponseResult.success(live);
+    }
+
+    @Override
+    public ResponseResult liveStop(String videoId) {
+        ResponseResult<DeviceDTO> responseResult = this.checkBeforeLive(videoId);
+        if (responseResult.getCode() != 0) {
+            return responseResult;
+        }
+
+        String respTopic = THING_MODEL_PRE + PRODUCT + responseResult.getData().getDeviceSn() + SERVICES_SUF;
+
+        ServiceReply receiveReply = this.publishLiveStop(respTopic, videoId);
+        if (receiveReply.getResult() != 0) {
+            log.info("videoId:{},直播停止失败,错误码:{},错误描述:{}", videoId, receiveReply.getResult(), LiveErrorEnum.find(receiveReply.getResult()).getErrorMsg());
+            return ResponseResult.error(LiveErrorEnum.find(receiveReply.getResult()));
+        }
+        //注释掉删除流媒体管理库中的信息
+//        ardVtduService.deleteArdVtduByName(videoId);
+        return ResponseResult.success();
+    }
+
+    @Override
+    public ResponseResult liveSetQuality(LiveTypeDTO liveParam) {
+        if (liveParam.getVideoQuality() == null ||
+                LiveVideoQualityEnum.UNKNOWN == LiveVideoQualityEnum.find(liveParam.getVideoQuality())) {
+            return ResponseResult.error(LiveErrorEnum.ERROR_PARAMETERS);
+        }
+
+        ResponseResult<DeviceDTO> responseResult = this.checkBeforeLive(liveParam.getVideoId());
+
+        if (responseResult.getCode() != 0) {
+            return responseResult;
+        }
+
+        String respTopic = THING_MODEL_PRE + PRODUCT + responseResult.getData().getDeviceSn() + SERVICES_SUF;
+
+        ServiceReply receiveReply = this.publishLiveSetQuality(respTopic, liveParam);
+        if (ResponseResult.CODE_SUCCESS != receiveReply.getResult()) {
+            return ResponseResult.error(LiveErrorEnum.find(receiveReply.getResult()));
+        }
+
+        return ResponseResult.success();
+    }
+
+    @Override
+    public ResponseResult liveLensChange(LiveTypeDTO liveParam) {
+        // 机场1:C3 修订(2026-10-19)——原「单舱外相机」只针对机场相机切换(live_camera_change,
+        // 见下方 liveCameraChange);飞行器相机镜头(广角/变焦/红外)由 M30/M30T 支持,
+        // live_lens_change 恢复为与共享层一致的下发逻辑。
+        log.info("切换镜头参数:{}", liveParam);
+        ResponseResult<DeviceDTO> responseResult = this.checkBeforeLive(liveParam.getVideoId());
+        if (ResponseResult.CODE_SUCCESS != responseResult.getCode()) {
+            return responseResult;
+        }
+        if (DeviceDomainEnum.GATEWAY.getVal() == responseResult.getData().getDomain()) {
+            return ResponseResult.error(LiveErrorEnum.FUNCTION_NOT_SUPPORT);
+        }
+
+        String respTopic = THING_MODEL_PRE + PRODUCT + responseResult.getData().getDeviceSn() + SERVICES_SUF;
+
+        ServiceReply receiveReply = this.publishLiveLensChange(respTopic, liveParam);
+
+        if (ResponseResult.CODE_SUCCESS != receiveReply.getResult()) {
+            Integer code = receiveReply.getResult();
+            if (code == 325003) {
+                // 指令响应失败,请重试。低配机型不支持红外时返回该错误码
+                return ResponseResult.error(325003000, LiveErrorEnum.COMMAND_FAILED.getErrorMsg() + "请确认无人机是否支持此功能!");
+            }
+            return ResponseResult.error(LiveErrorEnum.find(receiveReply.getResult()));
+        }
+
+        return ResponseResult.success();
+    }
+
+    @Override
+    public ResponseResult<List<String>> getLiveLens(String sn) {
+        List<String> videoTypes = new ArrayList<>();
+        // 检查设备是否在线
+        if (!deviceService.checkDeviceOnline(sn)) {
+            log.info("SN: {}-设备不在线,请检查网络连接!", sn);
+            // 抛弃出异常,给调用者处理
+            //throw new Exception("设备不在线,请检查网络连接!");
+            return ResponseResult.error(CommonErrorEnum.DEVICE_OFFLINE.getErrorCode(),CommonErrorEnum.DEVICE_OFFLINE.getErrorMsg());
+        }
+        // 读取直播能力拼接参数
+        List<CapacityDeviceDTO> devicesList = getLiveCapacity();
+        if (devicesList == null || devicesList.isEmpty()) {
+            log.info("SN: {}-未获得设备直播能力!", sn);
+            // 抛弃出异常,给调用者处理
+            //throw new Exception("未获得设备直播能力!");
+            return ResponseResult.error(22222200,"未获得设备直播能力!");
+        }
+        for (CapacityDeviceDTO device : devicesList) {
+            if (!device.getSn().equals(sn)) {
+                continue;
+            }
+            List<CapacityCameraDTO> camerasList = device.getCamerasList();
+            if (camerasList == null || camerasList.isEmpty()) {
+                continue;
+            }
+            for (CapacityCameraDTO camera : camerasList) {
+                List<CapacityVideoDTO> videosList = camera.getVideosList();
+                if (videosList == null || videosList.isEmpty()) {
+                    continue;
+                }
+                // 机场1:M30 / M30T 相机(52-x-x / 53-x-x)优先返回 normal 码流的镜头类型,
+                // 避免循环取最后一条时默认落到红外;无 normal 码流时退回任意一条有效记录。
+                // 其他机型保持原逻辑。
+                boolean m30SeriesCamera = camera.getIndex() != null
+                        && (camera.getIndex().startsWith("52-") || camera.getIndex().startsWith("53-"));
+                for (CapacityVideoDTO video : videosList) {
+                    List<String> switchTypes = video.getSwitchVideoTypes();
+                    if (switchTypes == null || switchTypes.isEmpty()) {
+                        continue;
+                    }
+                    if (m30SeriesCamera) {
+                        if (video.getIndex() != null && video.getIndex().startsWith("normal")) {
+                            videoTypes = switchTypes;
+                            break;
+                        }
+                        if (videoTypes.isEmpty()) {
+                            videoTypes = switchTypes;
+                        }
+                        continue;
+                    }
+                    // 机场3中飞行器,没有name,需要其他条件确定主摄像头参数
+                    if (switchTypes.size() == 1 && camera.getName() == null) {
+                        continue;
+                    }
+                    videoTypes = switchTypes;
+                }
+            }
+        }
+
+        return ResponseResult.success(videoTypes);
+    }
+
+    @Override
+    public ResponseResult liveCameraChange(LiveTypeDTO liveParam) {
+        // 机场1:C3 dock1 为单舱外相机(camera_position 恒为 1),不支持切换相机,整个功能禁用。
+        // 如需恢复:参考共享层 com.ard.sample.manage.service.impl.LiveStreamServiceImpl#liveCameraChange
+        log.info("机场1:liveCameraChange 已禁用(单舱外相机),忽略参数:{}", liveParam);
+        return ResponseResult.error(LiveErrorEnum.FUNCTION_NOT_SUPPORT);
+    }
+
+
+    // 机场1:C3 修订——liveLensChange 已恢复(飞行器镜头切换),本方法为实际下发入口
+    private ServiceReply publishLiveLensChange(String respTopic, LiveTypeDTO liveParam) {
+        CommonTopicResponse<LiveTypeDTO> response = new CommonTopicResponse<>();
+        response.setTid(UUID.randomUUID().toString());
+        response.setBid(UUID.randomUUID().toString());
+        response.setMethod(LiveStreamMethodEnum.LIVE_LENS_CHANGE.getMethod());
+        response.setData(liveParam);
+
+        return messageSender.publishWithReply(respTopic, response);
+    }
+
+    // 机场1:C3 liveCameraChange 已禁用,本方法保留备用(如需恢复,见 liveCameraChange 方法头注释)
+    private ServiceReply publishLiveCameraChange(String respTopic, LiveTypeDTO liveParam) {
+        CommonTopicResponse<LiveTypeDTO> response = new CommonTopicResponse<>();
+        response.setTid(UUID.randomUUID().toString());
+        response.setBid(UUID.randomUUID().toString());
+        response.setMethod(LiveStreamMethodEnum.LIVE_CAMERA_CHANGE.getMethod());
+        response.setData(liveParam);
+
+        return messageSender.publishWithReply(respTopic, response);
+    }
+
+    /**
+     * Check if this lens is available live.
+     *
+     * @param videoId
+     * @return
+     */
+    private ResponseResult<DeviceDTO> checkBeforeLive(String videoId) {
+        if (!StringUtils.hasText(videoId)) {
+            return ResponseResult.error(LiveErrorEnum.ERROR_PARAMETERS);
+        }
+        String[] videoIdArr = videoId.split("/");
+        // drone sn / enumeration value of the location where the payload is mounted / payload lens
+        if (videoIdArr.length != 3) {
+            return ResponseResult.error(LiveErrorEnum.ERROR_PARAMETERS);
+        }
+
+        Optional<DeviceDTO> deviceOpt = deviceService.getDeviceBySn(videoIdArr[0]);
+        // Check if the gateway device connected to this drone exists
+        if (deviceOpt.isEmpty()) {
+            return ResponseResult.error(LiveErrorEnum.NO_AIRCRAFT);
+        }
+
+        if (DeviceDomainEnum.DOCK.getVal() == deviceOpt.get().getDomain()) {
+            if (!Objects.equals(DOCK1_DOCK_TYPE, deviceOpt.get().getType())) {
+                return ResponseResult.error(314013004, "该设备不属于机场1,请使用对应的机场接口操作!");
+            }
+            return ResponseResult.success(deviceOpt.get());
+        }
+        List<DeviceDTO> gatewayList = deviceService.getDevicesByParams(
+                DeviceQueryParam.builder()
+                        .childSn(videoIdArr[0])
+                        .build());
+        if (gatewayList.isEmpty()) {
+            return ResponseResult.error(LiveErrorEnum.NO_FLIGHT_CONTROL);
+        }
+        // 机场1:只允许 Dock1 下的飞行器走 /dock1 直播链路。
+        if (!Objects.equals(DOCK1_DOCK_TYPE, gatewayList.get(0).getType())) {
+            return ResponseResult.error(314013004, "该设备不属于机场1,请使用对应的机场接口操作!");
+        }
+
+        return ResponseResult.success(gatewayList.get(0));
+    }
+
+    /**
+     * When using rtsp live, the account and password are parsed from the information returned by the pilot.
+     *
+     * @param url
+     * @param live
+     */
+    private void resolveUrlUser(String url, LiveDTO live) {
+        if (!StringUtils.hasText(url)) {
+            return;
+        }
+        int start = url.indexOf("//");
+        int end = url.lastIndexOf("@");
+        String user = url.substring(start + 2, end);
+
+        url = url.replace(user + "@", "");
+        String[] userArr = user.split(":");
+//        live.setUsername(userArr[0]);
+//        live.setPassword(userArr[1]);
+//        live.setUrl(url);
+    }
+
+    /**
+     * When using GB28181 live, url parameters are resolved into objects.
+     *
+     * @param url
+     * @return
+     */
+    private LiveUrlGB28181DTO urlToGB28181(String url) {
+        String[] arr = url.split("\\=|\\&");
+        LiveUrlGB28181DTO gb28181 = new LiveUrlGB28181DTO();
+        try {
+            Class<LiveUrlGB28181DTO> clazz = LiveUrlGB28181DTO.class;
+            for (int i = 0; i < arr.length - 1; i += 2) {
+                Field field = clazz.getDeclaredField(arr[i]);
+                field.setAccessible(true);
+
+                if (field.getType().equals(Integer.class)) {
+                    field.set(gb28181, Integer.valueOf(arr[i + 1]));
+                    continue;
+                }
+                field.set(gb28181, arr[i + 1]);
+            }
+        } catch (NoSuchFieldException | IllegalAccessException e) {
+            e.printStackTrace();
+        }
+        return gb28181;
+    }
+
+    /**
+     * Send a message to the pilot via mqtt to start the live streaming.
+     *
+     * @param topic
+     * @param liveParam
+     * @return
+     */
+    private ServiceReply publishLiveStart(String topic, LiveTypeDTO liveParam) {
+        CommonTopicResponse<LiveTypeDTO> response = new CommonTopicResponse<>();
+        response.setTid(UUID.randomUUID().toString());
+        response.setBid(UUID.randomUUID().toString());
+        response.setData(liveParam);
+        response.setMethod(LiveStreamMethodEnum.LIVE_START_PUSH.getMethod());
+
+        return messageSender.publishWithReply(topic, response);
+    }
+
+    /**
+     * Send a message to the pilot via mqtt to set quality.
+     *
+     * @param respTopic
+     * @param liveParam
+     * @return
+     */
+    private ServiceReply publishLiveSetQuality(String respTopic, LiveTypeDTO liveParam) {
+        Map<String, Object> data = new ConcurrentHashMap<>(Map.of(
+                "video_id", liveParam.getVideoId(),
+                "video_quality", liveParam.getVideoQuality()));
+        CommonTopicResponse<Map<String, Object>> response = new CommonTopicResponse<>();
+        response.setTid(UUID.randomUUID().toString());
+        response.setBid(UUID.randomUUID().toString());
+        response.setMethod(LiveStreamMethodEnum.LIVE_SET_QUALITY.getMethod());
+        response.setData(data);
+
+        return messageSender.publishWithReply(respTopic, response);
+    }
+
+    /**
+     * Send a message to the pilot via mqtt to stop the live streaming.
+     *
+     * @param topic
+     * @param videoId
+     * @return
+     */
+    private ServiceReply publishLiveStop(String topic, String videoId) {
+        Map<String, String> data = new ConcurrentHashMap<>(Map.of("video_id", videoId));
+        CommonTopicResponse<Map<String, String>> response = new CommonTopicResponse<>();
+        response.setTid(UUID.randomUUID().toString());
+        response.setBid(UUID.randomUUID().toString());
+        response.setData(data);
+        response.setMethod(LiveStreamMethodEnum.LIVE_STOP_PUSH.getMethod());
+
+        return messageSender.publishWithReply(topic, response);
+    }
+
+}
\ No newline at end of file
diff --git a/ard-sample/src/main/java/com/ard/sample/dock1/manage/service/impl/Dock1WaylineTrajectoryServiceImpl.java b/ard-sample/src/main/java/com/ard/sample/dock1/manage/service/impl/Dock1WaylineTrajectoryServiceImpl.java
new file mode 100644
index 0000000..5909c35
--- /dev/null
+++ b/ard-sample/src/main/java/com/ard/sample/dock1/manage/service/impl/Dock1WaylineTrajectoryServiceImpl.java
@@ -0,0 +1,274 @@
+/*
+ * 【机场1】从 dock2 原类 com.ard.sample.manage.service.impl.WaylineTrajectoryServiceImpl 复制/独立适配(DJI Dock 1 / M30)。
+ * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。
+ */
+package com.ard.sample.dock1.manage.service.impl;
+
+import cn.hutool.core.util.SerializeUtil;
+import com.ard.common.core.text.Convert;
+import com.ard.sample.component.mqtt.model.EventsReceiver;
+import com.ard.sample.manage.mapper.IWaylineTrajectoryMapper;
+import com.ard.sample.manage.model.dto.WaylineTrajectoryDTO;
+import com.ard.sample.manage.model.entity.WaylineTrajectoryEntity;
+import com.ard.sample.dock1.manage.service.IWaylineTrajectoryService;
+import com.ard.sample.wayline.model.dto.WaylineJobDTO;
+import com.ard.sample.wayline.model.dto.WaylineTaskProgressReceiver;
+import com.ard.sample.dock1.wayline.service.IWaylineJobService;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import com.ard.sample.manage.service.impl.WaylineTrajectoryServiceImpl;
+
+@Service
+@Slf4j
+public class Dock1WaylineTrajectoryServiceImpl implements IWaylineTrajectoryService {
+
+    @Resource
+    private IWaylineTrajectoryMapper _waylineTrajectoryMapper;
+
+    @Autowired
+    private IWaylineJobService _waylineJobService;
+
+    @Autowired
+    protected ObjectMapper mapper;
+    /**
+     * 轨迹点列表缓存
+     */
+    private final ConcurrentHashMap<String, List<String>> cacheTrajectoryMap = new ConcurrentHashMap<>();
+
+    /**
+     * 任务状态数据缓存
+     */
+    private final ConcurrentHashMap<String, List<String>> cacheTaskMap = new ConcurrentHashMap<>();
+
+    /**
+     * 给数据加个进度
+     */
+//    private final ConcurrentHashMap<String, String> cacheProgress = new ConcurrentHashMap<>();
+    /**
+     * 分片保存阈值条数
+     */
+    private static final int CHUNK_SIZE = 300;
+
+    /**
+     * 增加list数据,type为:0时,表示轨迹点列表;type为:1时,表示任务状态数据。
+     * @param sn       设备序列号
+     * @param type     类型
+     * @param dataJson json数据
+     */
+    @Override
+    public void setList(String sn, String type, String dataJson) {
+        List<String> listTrajectory = cacheTrajectoryMap.computeIfAbsent(sn, k -> new ArrayList<>());
+        List<String> listTask = cacheTaskMap.computeIfAbsent(sn, k -> new ArrayList<>());
+//        String progress = cacheProgress.computeIfAbsent(sn, k -> "0");
+        // 取进度值,就为了判断ok一个任务的结束
+//        Integer pro = Convert.toInt(cacheProgress.get(sn));
+        // 取jobid、status字段值
+        String jobId = "";
+        String isOK = "";
+        if (type.equals("0")) {
+            // 飞行轨迹数据
+            if(!listTask.isEmpty()) {
+                listTrajectory.add(dataJson);
+                log.info("SN:{} 轨迹点数据添加到列表缓存,当前列表数量:{}", sn, listTrajectory.size());
+            }
+            else {
+                cacheTrajectoryMap.put(sn, new ArrayList<>());
+                log.info("SN:{} 任务状态数据为空,当前无任务,不保存轨迹点数据", sn);
+            }
+//            log.info("SN:{} 轨迹点数据:{}", sn, dataJson);
+        }
+        if (type.equals("1")) {
+            // 任务状态数据
+            listTask.add(dataJson);
+            log.info("SN:{} 任务状态数据添加到列表缓存,当前列表数量:{}", sn, listTask.size());
+        }
+        // jobid、status字段赋值
+        if (!listTask.isEmpty()) {
+            String json = listTask.get(listTask.size() - 1);
+            log.info("SN:{} 当前任务记录最后一条数据:{}", sn, json);
+            try {
+                EventsReceiver<WaylineTaskProgressReceiver> eventsReceiver = mapper.readValue(json,
+                    new TypeReference<EventsReceiver<WaylineTaskProgressReceiver>>() {
+                    });
+            jobId = eventsReceiver.getOutput().getExt().getFlightId();
+            isOK = eventsReceiver.getOutput().getStatus();
+
+            log.info("SN:{} 当前任务jobId:{},任务执行进度isOK:{}", sn, jobId, isOK);
+
+            } catch (Exception e) {
+                log.error("SN:{} 解析任务状态JSON失败: {}", sn, e.getMessage());
+                log.error("SN:{} JSON内容: {}", sn, json);
+            }
+        }
+        else {
+            // 当前任务状态数据为空时,清空缓存
+            log.info("SN:{} 任务状态数据为空,当前无任务清空缓存", sn);
+            // 清空缓存
+            cacheTrajectoryMap.put(sn, new ArrayList<>());
+            cacheTaskMap.put(sn, new ArrayList<>());
+            return;
+        }
+//        listTrajectory.size() >= CHUNK_SIZE ||
+        if (listTask.size() >= CHUNK_SIZE) {
+            // 分片保存
+            save(sn, jobId, "0");
+            log.info("SN:{} 当前任务保存成功,jobId:{}, listTrajectory.size():{},listTask.size():{}",
+                    sn, jobId, listTrajectory.size(), listTask.size());
+//            log.info("当前任务保存成功,jobId:{},进度:{}",jobId , progress);
+        } else {
+            // 判断任务状态json中的status字段,如果为OK,则保存数据,一条完整的航线数据结束。
+            try {
+                if (isOK.equals("ok")) {
+                    String progress = "ok";
+                    if (getWaylineTrajectoryByProgress(jobId, progress)) {
+                        log.info("SN:{} 当前任务已完成,不再保存数据!jobId:{},progress:{}", sn, jobId, progress);
+                        // 清空缓存
+                        cacheTrajectoryMap.put(sn, new ArrayList<>());
+                        cacheTaskMap.put(sn, new ArrayList<>());
+                        return;
+                    }
+                    save(sn, jobId, progress);
+                    log.info("SN:{} 当前任务保存成功,jobId:{},进度:{}", sn,jobId , progress);
+                }
+            } catch (Exception e) {
+                log.error("SN:{} json解析异常!{}", sn, e.getMessage());
+            }
+        }
+    }
+
+    @Async
+    public void save(String sn, String JobId, String progress) {
+//        List<String> trajectoryList = cacheTrajectoryMap.get(sn);
+//        List<String> taskList = cacheTaskMap.get(sn);
+//        String progress = cacheProgress.get(sn);
+        // 关键修复:原子性remove获取缓存(拿到当前所有数据)+ 清空缓存,避免异步竞态
+        List<String> trajectoryList = cacheTrajectoryMap.remove(sn);
+        List<String> taskList = cacheTaskMap.remove(sn);
+        // 处理remove返回null的情况(缓存中无该sn)
+        trajectoryList = trajectoryList == null ? new ArrayList<>() : trajectoryList;
+        taskList = taskList == null ? new ArrayList<>() : taskList;
+        // 序列化前打印关键日志(确认拿到的数据)
+        log.info("SN:{} save方法原子性获取缓存 - 轨迹数据size:{},任务数据size:{}",
+                sn, trajectoryList.size(), taskList.size());
+
+        // 空数据直接返回
+        if (trajectoryList.isEmpty() && taskList.isEmpty()) {
+            log.warn("SN:{} 原子性获取的缓存数据为空,无需保存!jobId:{}", sn, JobId);
+            return;
+        }
+        try {
+            log.info("SN:{} 开始保存轨迹点列表数据!jobId:{},轨迹数量:{},任务状态数量:{}",
+                    sn, JobId, trajectoryList.size(), taskList.size());
+            byte[] blobTrajectory = SerializeUtil.serialize(trajectoryList);
+            byte[] blobTask = SerializeUtil.serialize(taskList);
+            log.info("SN:{} 序列化结果 - 轨迹字节长度:{},任务字节长度:{}",
+                    sn, blobTrajectory.length, blobTask.length);
+
+            Optional<WaylineJobDTO> waylineJobDTO = _waylineJobService.getJobByJobId(JobId);
+            WaylineTrajectoryEntity TrajectoryEntity = new WaylineTrajectoryEntity();
+            if(waylineJobDTO.isPresent()) {
+                TrajectoryEntity.setJobId(JobId);
+                TrajectoryEntity.setName(waylineJobDTO.get().getJobName());
+                TrajectoryEntity.setFileId(waylineJobDTO.get().getFileId());
+                TrajectoryEntity.setFileName(waylineJobDTO.get().getFileName());
+                TrajectoryEntity.setDockSn(sn);
+                TrajectoryEntity.setProgress(progress);
+                TrajectoryEntity.setCreateTime(System.currentTimeMillis());
+                TrajectoryEntity.setUpdateTime(System.currentTimeMillis());
+                TrajectoryEntity.setTrajectoryList(blobTrajectory);
+                TrajectoryEntity.setTaskList(blobTask);
+                // 保存轨迹点列表数据
+                int i = _waylineTrajectoryMapper.insert(TrajectoryEntity);
+                if (i > 0) {
+                    log.info("保存轨迹点列表数据成功!jobId:{}", JobId);
+                    // 清空缓存
+                    cacheTrajectoryMap.put(sn, new ArrayList<>());
+                    cacheTaskMap.put(sn, new ArrayList<>());
+//                if (progress.equals("ok")) {
+//                    cacheProgress.put(sn, "0");
+//                }
+                }
+            }
+        } catch (Exception e) {
+            log.error("SN:{} 保存数据异常!jobId:{}, 异常信息:{}", sn, JobId, e.getMessage(), e);
+            // 异常时把数据放回缓存(避免数据丢失)
+            if (!trajectoryList.isEmpty()) {
+                cacheTrajectoryMap.computeIfAbsent(sn, k -> new ArrayList<>()).addAll(trajectoryList);
+            }
+            if (!taskList.isEmpty()) {
+                cacheTaskMap.computeIfAbsent(sn, k -> new ArrayList<>()).addAll(taskList);
+            }
+        }
+    }
+
+    /**
+     * 返回航线轨迹完整的数据,拼接的轨迹点列表和任务状态数据。
+     *
+     * @param jobId 任务ID
+     * @return 航线轨迹完整的数据
+     */
+    @Override
+    public WaylineTrajectoryDTO getWayLineTrajectorybyJobID(String jobId) {
+
+        List<WaylineTrajectoryEntity> waylineTrajectoryEntityList = new ArrayList<>(_waylineTrajectoryMapper.selectList(new LambdaQueryWrapper<WaylineTrajectoryEntity>()
+                .eq(jobId != null, WaylineTrajectoryEntity::getJobId, jobId)
+                .orderByAsc(WaylineTrajectoryEntity::getId)));
+
+        return TrajectoryEntityConvertToDTO(waylineTrajectoryEntityList);
+    }
+
+    /**
+     * 根据作业ID和进度获取航线轨迹
+     *
+     * @param jobId    作业ID,可为null
+     * @param progress 进度值,可为null
+     * @return 当存在符合条件的航线轨迹时返回true,否则返回false
+     */
+    public Boolean getWaylineTrajectoryByProgress(String jobId, String progress) {
+        List<WaylineTrajectoryEntity> waylineTrajectoryEntityList = new ArrayList<>(_waylineTrajectoryMapper.selectList(new LambdaQueryWrapper<WaylineTrajectoryEntity>()
+                .eq(jobId != null, WaylineTrajectoryEntity::getJobId, jobId)
+                .eq(progress != null, WaylineTrajectoryEntity::getProgress, progress)
+                .orderByAsc(WaylineTrajectoryEntity::getId)));
+
+        return !waylineTrajectoryEntityList.isEmpty();
+    }
+
+
+    public WaylineTrajectoryDTO TrajectoryEntityConvertToDTO(List<WaylineTrajectoryEntity> entity) {
+        if (entity == null) {
+            return null;
+        }
+        WaylineTrajectoryDTO dto = new WaylineTrajectoryDTO();
+        try {
+            dto.setId(entity.get(0).getId());
+            dto.setJobId(entity.get(0).getJobId());
+            dto.setName(entity.get(0).getName());
+            dto.setFileId(entity.get(0).getFileId());
+            dto.setFileName(entity.get(0).getFileName());
+            dto.setDockSn(entity.get(0).getDockSn());
+            dto.setProgress(entity.get(0).getProgress());
+            dto.setCreateTime(entity.get(0).getCreateTime());
+            dto.setUpdateTime(entity.get(0).getUpdateTime());
+            for (WaylineTrajectoryEntity e : entity) {
+                dto.getTrajectoryList().addAll(SerializeUtil.deserialize(e.getTrajectoryList(), List.class));
+                dto.getTaskList().addAll(SerializeUtil.deserialize(e.getTaskList(), List.class));
+            }
+        }
+        catch (Exception e) {
+            log.error("获取任务id轨迹点列表数据失败!{}", e.getMessage());
+        }
+
+        return dto;
+    }
+}
diff --git a/ard-sample/src/main/java/com/ard/sample/dock1/wayline/model/dto/Dock1WaylineTaskCreateDTO.java b/ard-sample/src/main/java/com/ard/sample/dock1/wayline/model/dto/Dock1WaylineTaskCreateDTO.java
new file mode 100644
index 0000000..f647506
--- /dev/null
+++ b/ard-sample/src/main/java/com/ard/sample/dock1/wayline/model/dto/Dock1WaylineTaskCreateDTO.java
@@ -0,0 +1,79 @@
+/*
+ * 【机场1】从 dock2 原类 com.ard.sample.wayline.model.dto.WaylineTaskCreateDTO 复制/独立适配(DJI Dock 1 / M30)。
+ * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。
+ */
+package com.ard.sample.dock1.wayline.model.dto;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import com.ard.sample.wayline.model.dto.WaylineSimulateMissionDTO;
+import com.ard.sample.wayline.model.dto.WaylineTaskCreateDTO;
+import com.ard.sample.wayline.model.dto.WaylineTaskFileDTO;
+
+/**
+ * @author sean
+ * @version 1.1
+ * @date 2022/6/1
+ */
+// 序列化时忽略所有 null 字段
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class Dock1WaylineTaskCreateDTO {
+
+    /**
+     * 计划 ID
+     * */
+    @JsonProperty("flight_id")
+    private String flightId;
+
+    /**
+     * 任务类型	"0":"立即任务","1":"定时任务","2":"条件任务"
+     * 立即任务和定时任务均由execute_time指定执行时间;
+     * 条件任务支持ready_conditions字段指定任务就绪条件,
+     * 设备可在指定时间段内满足就绪条件后即可执行;
+     * 立即任务媒体上传优先级最高,
+     * 定时任务和条件任务媒体上传优先级相同
+     * */
+    @JsonProperty("task_type")
+    private Integer taskType;
+
+    // 机场1:C4 dock1(M30)官方无 wayline_precision_type(waylineType)字段,已剔除不下发
+
+    /**
+     * 开始执行时间
+     * 任务开始执行时间毫秒时间戳。可选字段。当 task_type 为 0 或 1 时必填,为 2 时非必填。
+     * */
+    @JsonProperty("execute_time")
+    private Long executeTime;
+
+    /**
+     * 航线文件对象
+     * */
+    @JsonProperty("file")
+    private WaylineTaskFileDTO file;
+
+    /**
+     * 返航高度	"max":1500,"min":20,"unit_name":"米 / m"
+     * */
+    @JsonProperty("rth_altitude")
+    private Integer rthAltitude;
+
+    // 机场1:C4 dock1(M30)官方无 rth_mode(rthMode)字段,已剔除不下发
+
+    /**
+     * 遥控器失控动作	"0":"返航","1":"悬停","2":"降落"
+     * 失控动作,当前固定传的值是 0,即返航。注意该枚举值定义跟飞控跟机场定义的不一致,机场端会进行转换。
+     * */
+    @JsonProperty("out_of_control_action")
+    private Integer outOfControlAction;
+
+//    @JsonProperty("simulate_mission")
+//    private WaylineSimulateMissionDTO simulateMission;
+}
diff --git a/ard-sample/src/main/java/com/ard/sample/dock1/wayline/service/IFlightTaskService.java b/ard-sample/src/main/java/com/ard/sample/dock1/wayline/service/IFlightTaskService.java
new file mode 100644
index 0000000..24956c9
--- /dev/null
+++ b/ard-sample/src/main/java/com/ard/sample/dock1/wayline/service/IFlightTaskService.java
@@ -0,0 +1,22 @@
+/*
+ * 【机场1】从 dock2 原类 com.ard.sample.wayline.service.IFlightTaskService 复制/独立适配(DJI Dock 1 / M30)。
+ * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。
+ */
+package com.ard.sample.dock1.wayline.service;
+
+import com.ard.sample.component.mqtt.model.CommonTopicReceiver;
+import org.springframework.messaging.MessageHeaders;
+
+/**
+ * @author sean
+ * @version 1.1
+ * @date 2022/6/9
+ */
+public interface IFlightTaskService {
+
+    /**
+     * Handle the progress messages of the flight tasks reported by the dock.
+     * @param receiver
+     */
+    void handleProgress(CommonTopicReceiver receiver, MessageHeaders headers);
+}
diff --git a/ard-sample/src/main/java/com/ard/sample/dock1/wayline/service/IWaylineFileService.java b/ard-sample/src/main/java/com/ard/sample/dock1/wayline/service/IWaylineFileService.java
new file mode 100644
index 0000000..650499f
--- /dev/null
+++ b/ard-sample/src/main/java/com/ard/sample/dock1/wayline/service/IWaylineFileService.java
@@ -0,0 +1,80 @@
+/*
+ * 【机场1】从 dock2 原类 com.ard.sample.wayline.service.IWaylineFileService 复制/独立适配(DJI Dock 1 / M30)。
+ * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。
+ */
+package com.ard.sample.dock1.wayline.service;
+
+import com.ard.sample.common.model.PaginationData;
+import com.ard.sample.wayline.model.dto.WaylineFileDTO;
+import com.ard.sample.wayline.model.param.WaylineQueryParam;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.net.URL;
+import java.sql.SQLException;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * @author sean
+ * @version 0.3
+ * @date 2021/12/22
+ */
+public interface IWaylineFileService {
+
+    /**
+     * Perform paging queries based on query parameters.
+     * @param param
+     * @return
+     */
+    PaginationData<WaylineFileDTO> getWaylinesByParam(WaylineQueryParam param);
+
+    /**
+     * Query the information of this wayline file according to the wayline file id.
+     * @param waylineId
+     * @return
+     */
+    Optional<WaylineFileDTO> getWaylineByWaylineId(String waylineId);
+
+    /**
+     * Get the download address of the file object.
+     * @param waylineId
+     * @return
+     */
+    URL getObjectUrl(String waylineId) throws SQLException;
+
+    /**
+     * Save the basic information of the wayline file.
+     * @param metadata
+     * @return
+     */
+    Integer saveWaylineFile(WaylineFileDTO metadata);
+
+    /**
+     * Updates whether the file is collected or not based on the passed parameters.
+     * @param ids          wayline id
+     * @param isFavorite   Whether the wayline file is favorited or not.
+     * @return
+     */
+    Boolean markFavorite(List<String> ids, Boolean isFavorite);
+
+    /**
+     * Batch query for duplicate file names in workspace.
+     * @param names
+     * @return
+     */
+    List<String> getDuplicateNames(List<String> names);
+
+    /**
+     * Delete the wayline file based on the wayline id.
+     * @param waylineId
+     */
+    Boolean deleteByWaylineId(String waylineId);
+
+    /**
+     * Import kmz wayline file.
+     * @param file
+     * @param creator
+     * @return
+     */
+    void importKmzFile(MultipartFile file, String creator);
+}
diff --git a/ard-sample/src/main/java/com/ard/sample/dock1/wayline/service/IWaylineJobService.java b/ard-sample/src/main/java/com/ard/sample/dock1/wayline/service/IWaylineJobService.java
new file mode 100644
index 0000000..3bba79f
--- /dev/null
+++ b/ard-sample/src/main/java/com/ard/sample/dock1/wayline/service/IWaylineJobService.java
@@ -0,0 +1,129 @@
+/*
+ * 【机场1】从 dock2 原类 com.ard.sample.wayline.service.IWaylineJobService 复制/独立适配(DJI Dock 1 / M30)。
+ * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。
+ */
+package com.ard.sample.dock1.wayline.service;
+
+import com.ard.common.core.domain.model.LoginUser;
+import com.ard.sample.common.model.PaginationData;
+import com.ard.sample.common.model.ResponseResult;
+import com.ard.sample.component.mqtt.model.CommonTopicReceiver;
+import com.ard.sample.wayline.model.dto.WaylineJobDTO;
+import com.ard.sample.wayline.model.enums.WaylineJobStatusEnum;
+import com.ard.sample.wayline.model.param.CreateJobParam;
+import org.springframework.messaging.MessageHeaders;
+import java.sql.SQLException;
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * @author sean
+ * @version 1.1
+ * @date 2022/6/1
+ */
+public interface IWaylineJobService {
+
+    /**
+     * Create wayline job in the database.
+     * @param param
+     * @param username      user info
+     * @param beginTime     The time the job started.
+     * @param endTime       The time the job ended.
+     * @return
+     */
+    Optional<WaylineJobDTO> createWaylineJob(CreateJobParam param, String username, Long beginTime, Long endTime);
+
+    /**
+     * Create a sub-task based on the information of the parent task.
+
+     * @param parentId
+     * @return
+     */
+    Optional<WaylineJobDTO> createWaylineJobByParent(String parentId);
+
+    /**
+     * Issue wayline mission to the dock.
+     * @param param
+     * @param loginUser   user info
+     * @return
+     */
+    ResponseResult publishFlightTask(CreateJobParam param, LoginUser loginUser) throws SQLException;
+
+    ResponseResult publishFlightTaskPrepare(Optional<WaylineJobDTO> waylineJobOpt) throws SQLException;
+
+    /**
+     * Execute the task immediately.
+     * @param jobId
+     * @throws SQLException
+     * @return
+     */
+    Boolean executeFlightTask(String jobId);
+
+    /**
+     * Cancel the task Base on job Ids.
+     * @param jobId
+     * @throws SQLException
+     */
+    ResponseResult cancelFlightTask(Collection<String> jobId);
+//    void cancelFlightTask(Collection<String> jobIds);
+
+    /**
+     * Cancel the dock tasks that have been issued but have not yet been executed.
+     * @param dockSn
+     * @param jobIds
+     */
+    ResponseResult publishCancelTask(String dockSn, List<String> jobIds);
+
+    /**
+     * 删除任务
+     * */
+    void deleteTask(String id);
+
+    /**
+     * Query wayline jobs based on conditions.
+     * @param jobIds
+     * @param status
+     * @return
+     */
+    List<WaylineJobDTO> getJobsByConditions( Collection<String> jobIds, WaylineJobStatusEnum status);
+
+    /**
+     * Query job information based on job id.
+     * @param jobId
+     * @return job information
+     */
+    Optional<WaylineJobDTO> getJobByJobId(String jobId);
+
+    /**
+     * Update job data.
+     * @param dto
+     * @return
+     */
+    Boolean updateJob(WaylineJobDTO dto);
+
+    /**
+     * Paginate through all jobs in this workspace.
+     * @param page
+     * @param pageSize
+     * @return
+     */
+    PaginationData<WaylineJobDTO> getJobsByWorkspaceId(long page, long pageSize);
+
+    PaginationData<WaylineJobDTO> getJobsList(long page, long pageSize, Integer status,
+                                              Long startTime, Long endTime, Integer taskType,
+                                              String name, Boolean isComplete);
+
+    /**
+     * Process to get interface data of flight mission resources.
+     * @param receiver
+     * @param headers
+     */
+    void flightTaskResourceGet(CommonTopicReceiver receiver, MessageHeaders headers);
+
+    /**
+     * Set the media files for this job to upload immediately.
+     * @param jobId
+     */
+    void uploadMediaHighestPriority(String jobId);
+}
diff --git a/ard-sample/src/main/java/com/ard/sample/dock1/wayline/service/impl/Dock1FlightTaskServiceImpl.java b/ard-sample/src/main/java/com/ard/sample/dock1/wayline/service/impl/Dock1FlightTaskServiceImpl.java
new file mode 100644
index 0000000..30f5029
--- /dev/null
+++ b/ard-sample/src/main/java/com/ard/sample/dock1/wayline/service/impl/Dock1FlightTaskServiceImpl.java
@@ -0,0 +1,288 @@
+/*
+ * 【机场1】从 dock2 原类 com.ard.sample.wayline.service.impl.FlightTaskServiceImpl 复制/独立适配(DJI Dock 1 / M30)。
+ * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。
+ */
+package com.ard.sample.dock1.wayline.service.impl;
+
+import com.ard.common.utils.uuid.IdUtils;
+import com.ard.route.domain.route.ArdRouteInfo;
+import com.ard.route.service.IArdRouteInfoService;
+import com.ard.route.service.IArdRouteMissionService;
+import com.ard.sample.common.error.CommonErrorEnum;
+import com.ard.sample.common.model.ResponseResult;
+import com.ard.sample.component.mqtt.model.*;
+import com.ard.sample.component.mqtt.service.IMessageSenderService;
+import com.ard.common.core.redis.RedisConst;
+import com.ard.common.core.redis.RedisOpsUtils;
+import com.ard.sample.component.websocket.enums.MessageType;
+import com.ard.sample.component.websocket.handler.DockWebSocketHandler;
+import com.ard.sample.component.websocket.model.BizCodeEnum;
+import com.ard.sample.component.websocket.model.CustomWebSocketMessage;
+import com.ard.sample.component.websocket.service.ISendMessageService;
+import com.ard.sample.component.websocket.service.IWebSocketManageService;
+import com.ard.sample.manage.model.dto.DeviceDTO;
+import com.ard.sample.manage.model.enums.UserTypeEnum;
+// 机场1:任务状态(type="1")与轨迹点(type="0")必须写入同一个共享轨迹服务缓存。
+import com.ard.sample.manage.service.IWaylineTrajectoryService;
+import com.ard.sample.media.model.MediaFileCountDTO;
+import com.ard.sample.wayline.model.dto.WaylineJobDTO;
+import com.ard.sample.wayline.model.dto.WaylineTaskProgressExt;
+import com.ard.sample.wayline.model.dto.WaylineTaskProgressReceiver;
+import com.ard.sample.wayline.model.entity.WaylineJobHistoryEntity;
+import com.ard.sample.wayline.model.enums.WaylineJobStatusEnum;
+import com.ard.sample.dock1.wayline.service.IFlightTaskService;
+import com.ard.sample.wayline.service.IWaylineJobHistoryService;
+import com.ard.sample.dock1.wayline.service.IWaylineJobService;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.http.HttpStatus;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.integration.mqtt.support.MqttHeaders;
+import org.springframework.messaging.MessageHeaders;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+import com.ard.sample.wayline.service.impl.FlightTaskServiceImpl;
+
+/**
+ * @author sean
+ * @version 1.1
+ * @date 2022/6/9
+ */
+@Service
+@Slf4j
+public class Dock1FlightTaskServiceImpl implements IFlightTaskService {
+
+    @Autowired
+    private IMessageSenderService messageSender;
+
+    @Autowired
+    private ObjectMapper mapper;
+
+    @Autowired
+    private DockWebSocketHandler _dockWebSocketHandler;
+
+    @Autowired
+    private ISendMessageService websocketMessageService;
+
+    @Autowired
+    private IWebSocketManageService webSocketManageService;
+
+    @Autowired
+    private IWaylineJobService waylineJobService;
+
+    @Autowired
+    private IWaylineTrajectoryService _waylineTrajectoryService;
+
+    @Autowired
+    private IWaylineJobHistoryService _waylineJobHistoryService;
+
+    @Autowired
+    private IArdRouteMissionService _ardRouteMissionService;
+
+    @Autowired
+    private IArdRouteInfoService _ardRouteInfoService;
+
+    @Override
+    // 机场1:不复制原 ServiceActivator(inputChannel = ChannelName.INBOUND_EVENTS_FLIGHT_TASK_PROGRESS),消费职责由共享层原类承担
+    public void handleProgress(CommonTopicReceiver receiver, MessageHeaders headers) {
+        String receivedTopic = String.valueOf(headers.get(MqttHeaders.RECEIVED_TOPIC));
+        String dockSn = receivedTopic.substring((TopicConst.THING_MODEL_PRE + TopicConst.PRODUCT).length(),
+                receivedTopic.indexOf(TopicConst.EVENTS_SUF));
+        EventsReceiver<WaylineTaskProgressReceiver> eventsReceiver = mapper.convertValue(receiver.getData(),
+                new TypeReference<EventsReceiver<WaylineTaskProgressReceiver>>() {
+                });
+        eventsReceiver.setBid(receiver.getBid());
+        eventsReceiver.setSn(receiver.getGateway());
+        eventsReceiver.setGateway(receiver.getGateway());
+        eventsReceiver.setType(MessageType.STATUS.getType() + "-flighttask_progress");
+        eventsReceiver.setMethod(EventsMethodEnum.FLIGHT_TASK_PROGRESS.getMethod());
+
+
+        WaylineTaskProgressReceiver output = eventsReceiver.getOutput();
+
+        log.info("Task progress: {}", output.getProgress().toString());
+
+        if (eventsReceiver.getResult() != ResponseResult.CODE_SUCCESS) {
+            String errorMsg = CommonErrorEnum.getMsgByCode(eventsReceiver.getResult());
+            log.error("Task progress ===> Error code: {},Error Msg: {}", eventsReceiver.getResult(), errorMsg);
+        }
+
+        EventsResultStatusEnum statusEnum = EventsResultStatusEnum.find(output.getStatus());
+        String key = RedisConst.WAYLINE_JOB_RUNNING_PREFIX + dockSn;
+        RedisOpsUtils.setWithExpire(key, eventsReceiver, RedisConst.DEVICE_ALIVE_SECOND * RedisConst.DEVICE_ALIVE_SECOND);
+
+        if (statusEnum.getEnd()) {
+            WaylineJobDTO job = WaylineJobDTO.builder()
+                    .jobId(receiver.getBid())
+                    .status(WaylineJobStatusEnum.SUCCESS.getVal())
+                    .completedTime(LocalDateTime.now())
+                    .mediaCount(output.getExt().getMediaCount())
+                    .build();
+
+            // record the update of the media count.
+            if (Objects.nonNull(job.getMediaCount()) && job.getMediaCount() != 0) {
+                RedisOpsUtils.hashSet(RedisConst.MEDIA_FILE_PREFIX + receiver.getGateway(), job.getJobId(),
+                        MediaFileCountDTO.builder().jobId(receiver.getBid()).mediaCount(job.getMediaCount()).uploadedCount(0).build());
+            }
+
+            if (EventsResultStatusEnum.OK != statusEnum) {
+                job.setCode(eventsReceiver.getResult());
+                job.setStatus(WaylineJobStatusEnum.FAILED.getVal());
+            }
+
+            waylineJobService.updateJob(job);
+            RedisOpsUtils.del(key);
+            RedisOpsUtils.del(RedisConst.WAYLINE_JOB_PAUSED_PREFIX + receiver.getBid());
+        }
+
+        DeviceDTO device = (DeviceDTO) RedisOpsUtils.get(RedisConst.DEVICE_ONLINE_PREFIX + receiver.getGateway());
+        websocketMessageService.sendBatch(
+                webSocketManageService.getValueAndUserType(UserTypeEnum.WEB.getVal()),
+                CustomWebSocketMessage.builder()
+                        .data(eventsReceiver)
+                        .timestamp(System.currentTimeMillis())
+                        .bizCode(BizCodeEnum.FLIGHT_TASK_PROGRESS.getCode())
+                        .build());
+
+        if (receiver.getNeedReply() != null && receiver.getNeedReply() == 1) {
+            messageSender.publish(receivedTopic + TopicConst._REPLY_SUF,
+                    CommonTopicResponse.builder()
+                            .tid(receiver.getTid())
+                            .bid(receiver.getBid())
+                            .method(EventsMethodEnum.FLIGHT_TASK_PROGRESS.getMethod())
+                            .timestamp(System.currentTimeMillis())
+                            .data(RequestsReply.success())
+                            .build());
+        }
+        // 进度
+//        if (eventsReceiver.getOutput().getProgress().getPercent() >= 95) {
+//            return;
+//        }
+
+
+
+        String json = "";
+        try {
+            json = mapper.writeValueAsString(eventsReceiver);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        _dockWebSocketHandler.sendMessageToSubscribers(dockSn, MessageType.STATUS.getType(), json, "");
+
+        _waylineTrajectoryService.setList(dockSn,"1",json);
+
+        // 计划任务成功后,保存计划任务到历史记录
+        String status = eventsReceiver.getOutput().getStatus();
+        String jobId = eventsReceiver.getOutput().getExt().getFlightId();
+        if(status.equals("ok") && jobId != null) {
+            Optional<WaylineJobDTO> job = waylineJobService.getJobByJobId(jobId);
+            if(job.isPresent()) {
+                if (_waylineJobHistoryService.getJobByJobId(jobId).isPresent()) {
+                    return;
+                }
+                WaylineJobHistoryEntity waylineJobHistoryEntity = _waylineJobHistoryService.dto2Entity(job.get());
+                Optional<WaylineJobDTO> jobHistory = _waylineJobHistoryService.createWaylineJob(waylineJobHistoryEntity);
+                if(jobHistory.isPresent()) {
+                    log.info("任务执行成功,保存到历史记录。 {}", jobHistory);
+                }
+                // 同步更新4.2的计划任务的状态;等待夏吉建的接口。1:已执行 0:未执行
+                // 在任务中查找航线文件id,取航线文件信息中的状态,是3的修改状态
+                ArdRouteInfo ardRouteInfo = _ardRouteInfoService.selectArdRouteInfoById(job.get().getFileId());
+                if(ardRouteInfo != null && ardRouteInfo.getType().equals("3")) {
+                    _ardRouteMissionService.updateArdRouteMissionStatus(jobId, "1");
+                    log.info("任务航线-执行成功,更新航线状态。 jobId:{}", jobId);
+                }
+            }
+
+        }
+    }
+
+    /**
+     * 定时检查并处理航线定时任务
+     * <p>
+     * 该方法每5秒执行一次,从Redis有序集合中获取最早需要执行的任务,检查其执行时间:
+     * 1. 若任务已过期(超过当前时间30秒),则标记为失败并删除
+     * 2. 若任务在允许的时间窗口内(当前时间±30秒),则执行任务并删除
+     * </p>
+     *
+     * <p>任务ID格式:{workspace_id}:{dock_sn}:{job_id}</p>
+     */
+    // 机场1:不复制原 Scheduled 定时任务,定时职责由共享层原类承担
+    private void checkScheduledJob() {
+        Object jobIdValue = RedisOpsUtils.zGetMin(RedisConst.WAYLINE_JOB_TIMED_EXECUTE);
+        if (Objects.isNull(jobIdValue)) {
+            return;
+        }
+        log.info("检查 wayline 的定时任务。 {}", jobIdValue);
+        // format: {workspace_id}:{dock_sn}:{job_id}
+        String[] jobArr = String.valueOf(jobIdValue).split(RedisConst.DELIMITER);
+        double time = RedisOpsUtils.zScore(RedisConst.WAYLINE_JOB_TIMED_EXECUTE, jobIdValue);
+        // region 取定时任务信息websocket发给前端
+        // 定时任务执行前,判断有没有任务正在执行,在执行则不发送websocket消息
+        String key = RedisConst.WAYLINE_JOB_RUNNING_PREFIX + jobArr[0];
+        if (!RedisOpsUtils.checkExist(key)) {
+            // 取到待执行的定时任务ID,获取任务信息websocket发给前端
+            // pending status状态待执行
+            EventsReceiver<WaylineTaskProgressReceiver> eventsReceiver = new EventsReceiver<>();
+            WaylineTaskProgressReceiver waylineTaskProgressReceiver = new WaylineTaskProgressReceiver();
+            WaylineTaskProgressExt waylineTaskProgressExt = new WaylineTaskProgressExt();
+            waylineTaskProgressExt.setFlightId(jobArr[1]);
+            waylineTaskProgressReceiver.setStatus("pending");
+            waylineTaskProgressReceiver.setExt(waylineTaskProgressExt);
+            eventsReceiver.setOutput(waylineTaskProgressReceiver);
+            eventsReceiver.setResult(ResponseResult.CODE_SUCCESS);
+            eventsReceiver.setBid(IdUtils.simpleUUID());
+            eventsReceiver.setSn(jobArr[0]);
+            eventsReceiver.setGateway(jobArr[0]);
+            eventsReceiver.setType(MessageType.STATUS.getType() + "-flighttask_progress");
+            eventsReceiver.setMethod(EventsMethodEnum.FLIGHT_TASK_PROGRESS.getMethod());
+            String json = "";
+            try {
+                json = mapper.writeValueAsString(eventsReceiver);
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+            _dockWebSocketHandler.sendMessageToSubscribers(jobArr[0], MessageType.STATUS.getType(), json, "");
+            log.info("定时任务自定义状态为待执行,json: {}", json);
+        }
+        // endregion
+        long now = System.currentTimeMillis();
+        int offset = 30_000;
+
+        // 过期任务会直接删除。
+        if (time < now - offset) {
+            log.info("定时任务超时,jobId: {}", jobArr[1]);
+            RedisOpsUtils.zRemove(RedisConst.WAYLINE_JOB_TIMED_EXECUTE, jobIdValue);
+            waylineJobService.updateJob(WaylineJobDTO.builder()
+                    .jobId(jobArr[1])
+                    .status(WaylineJobStatusEnum.FAILED.getVal())
+                    .executeTime(LocalDateTime.now())
+                    .completedTime(LocalDateTime.now())
+                    .code(HttpStatus.SC_REQUEST_TIMEOUT).build());
+            return;
+        }
+
+        if (now <= time && time <= now + offset) {
+            try {
+                waylineJobService.executeFlightTask(jobArr[1]);
+            } catch (Exception e) {
+                log.info("计划中的任务交付失败了。jobId: {},异常信息:{}", jobArr[1], e.getMessage());
+                waylineJobService.updateJob(WaylineJobDTO.builder()
+                        .jobId(jobArr[1])
+                        .status(WaylineJobStatusEnum.FAILED.getVal())
+                        .executeTime(LocalDateTime.now())
+                        .completedTime(LocalDateTime.now())
+                        .code(HttpStatus.SC_INTERNAL_SERVER_ERROR).build());
+            } finally {
+                RedisOpsUtils.zRemove(RedisConst.WAYLINE_JOB_TIMED_EXECUTE, jobIdValue);
+            }
+        }
+    }
+}
diff --git a/ard-sample/src/main/java/com/ard/sample/dock1/wayline/service/impl/Dock1WaylineFileServiceImpl.java b/ard-sample/src/main/java/com/ard/sample/dock1/wayline/service/impl/Dock1WaylineFileServiceImpl.java
new file mode 100644
index 0000000..765c551
--- /dev/null
+++ b/ard-sample/src/main/java/com/ard/sample/dock1/wayline/service/impl/Dock1WaylineFileServiceImpl.java
@@ -0,0 +1,289 @@
+/*
+ * 【机场1】从 dock2 原类 com.ard.sample.wayline.service.impl.WaylineFileServiceImpl 复制/独立适配(DJI Dock 1 / M30)。
+ * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。
+ */
+package com.ard.sample.dock1.wayline.service.impl;
+
+import com.ard.common.config.OssConfiguration;
+import com.ard.oss.service.impl.OssServiceContext;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.ard.common.core.page.PageDomain;
+import com.ard.common.core.page.TableSupport;
+import com.ard.sample.common.model.Pagination;
+import com.ard.sample.common.model.PaginationData;
+import com.ard.sample.manage.model.enums.DeviceDomainEnum;
+import com.ard.sample.wayline.mapper.IWaylineFileMapper;
+import com.ard.sample.wayline.model.dto.KmzFileProperties;
+import com.ard.sample.wayline.model.dto.WaylineFileDTO;
+import com.ard.sample.wayline.model.entity.WaylineFileEntity;
+import com.ard.sample.wayline.model.param.WaylineQueryParam;
+import com.ard.sample.dock1.wayline.service.IWaylineFileService;
+import org.dom4j.Document;
+import org.dom4j.DocumentException;
+import org.dom4j.Node;
+import org.dom4j.io.SAXReader;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.DigestUtils;
+import org.springframework.util.StringUtils;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.sql.SQLException;
+import java.util.*;
+import java.util.stream.Collectors;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+import static com.ard.sample.wayline.model.dto.KmzFileProperties.WAYLINE_FILE_SUFFIX;
+import com.ard.sample.wayline.service.impl.WaylineFileServiceImpl;
+
+/**
+ * @author sean
+ * @version 0.3
+ * @date 2021/12/22
+ */
+@Service
+@Transactional
+public class Dock1WaylineFileServiceImpl implements IWaylineFileService {
+
+    @Autowired
+    private IWaylineFileMapper mapper;
+
+    @Autowired
+    private OssServiceContext ossService;
+
+    @Override
+    public PaginationData<WaylineFileDTO> getWaylinesByParam(WaylineQueryParam param) {
+        PageDomain pageDomain = TableSupport.buildPageRequest(); // 从请求获取分页参数
+        // Paging Query
+        Page<WaylineFileEntity> page = mapper.selectPage(
+                new Page<WaylineFileEntity>(pageDomain.getPageNum(), pageDomain.getPageSize()),
+                new LambdaQueryWrapper<WaylineFileEntity>()
+                        .eq(param.isFavorited(), WaylineFileEntity::getFavorited, param.isFavorited())
+                        .and(param.getTemplateType() != null, wrapper ->  {
+                                for (Integer type : param.getTemplateType()) {
+                                    wrapper.like(WaylineFileEntity::getTemplateTypes, type).or();
+                                }
+                        })
+                        // There is a risk of SQL injection
+                        .last(StringUtils.hasText(param.getOrderBy()), " order by " + param.getOrderBy())
+                );
+
+        // Wrap the results of a paging query into a custom paging object.
+        List<WaylineFileDTO> records = page.getRecords()
+                .stream()
+                .map(this::entityConvertToDTO)
+                .collect(Collectors.toList());
+
+        return new PaginationData<>(records, new Pagination(page));
+    }
+
+    @Override
+    public Optional<WaylineFileDTO> getWaylineByWaylineId(String waylineId) {
+        return Optional.ofNullable(
+                this.entityConvertToDTO(
+                        mapper.selectOne(
+                                new LambdaQueryWrapper<WaylineFileEntity>()
+                                    .eq(WaylineFileEntity::getWaylineId, waylineId))));
+    }
+
+    @Override
+    public URL getObjectUrl(String waylineId) throws SQLException {
+        Optional<WaylineFileDTO> waylineOpt = this.getWaylineByWaylineId(waylineId);
+        if (waylineOpt.isEmpty()) {
+            throw new SQLException(waylineId + " does not exist.");
+        }
+        return ossService.getObjectUrl(OssConfiguration.bucket, waylineOpt.get().getObjectKey());
+    }
+
+    @Override
+    public Integer saveWaylineFile(WaylineFileDTO metadata) {
+        WaylineFileEntity file = this.dtoConvertToEntity(metadata);
+        file.setWaylineId(UUID.randomUUID().toString());
+        if (!StringUtils.hasText(file.getSign())) {
+            try (InputStream object = ossService.getObject(OssConfiguration.bucket, metadata.getObjectKey())) {
+                if (object.available() == 0) {
+                    throw new RuntimeException("The file " + metadata.getObjectKey() +
+                            " does not exist in the bucket[" + OssConfiguration.bucket + "].");
+                }
+                file.setSign(DigestUtils.md5DigestAsHex(object));
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+        int insertId = mapper.insert(file);
+        return insertId > 0 ? file.getId() : insertId;
+    }
+
+    @Override
+    public Boolean markFavorite(List<String> waylineIds, Boolean isFavorite) {
+        if (waylineIds.isEmpty()) {
+            return false;
+        }
+        if (isFavorite == null) {
+            return true;
+        }
+        return mapper.update(null, new LambdaUpdateWrapper<WaylineFileEntity>()
+                .set(WaylineFileEntity::getFavorited, isFavorite)
+                .in(WaylineFileEntity::getWaylineId, waylineIds)) > 0;
+    }
+
+    @Override
+    public List<String> getDuplicateNames(List<String> names) {
+        return mapper.selectList(new LambdaQueryWrapper<WaylineFileEntity>()
+                .in(WaylineFileEntity::getName, names))
+                .stream()
+                .map(WaylineFileEntity::getName)
+                .collect(Collectors.toList());
+    }
+
+    @Override
+    public Boolean deleteByWaylineId(String waylineId) {
+        Optional<WaylineFileDTO> waylineOpt = this.getWaylineByWaylineId(waylineId);
+        if (waylineOpt.isEmpty()) {
+            return true;
+        }
+        WaylineFileDTO wayline = waylineOpt.get();
+        boolean isDel = mapper.delete(new LambdaUpdateWrapper<WaylineFileEntity>()
+                    .eq(WaylineFileEntity::getWaylineId, waylineId))
+                > 0;
+        if (!isDel) {
+            return false;
+        }
+        return ossService.deleteObject(OssConfiguration.bucket, wayline.getObjectKey());
+    }
+
+    @Override
+    public void importKmzFile(MultipartFile file, String creator) {
+        Optional<WaylineFileDTO> waylineFileOpt = validKmzFile(file);
+        if (waylineFileOpt.isEmpty()) {
+            throw new RuntimeException("The file format is incorrect.");
+        }
+
+        try {
+            WaylineFileDTO waylineFile = waylineFileOpt.get();
+            waylineFile.setUsername(creator);
+            // 使用 try-with-resources 确保流关闭
+            try (InputStream input = file.getInputStream()) {
+                ossService.putObject(OssConfiguration.bucket, waylineFile.getObjectKey(), input);
+            }
+            this.saveWaylineFile(waylineFile);
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    private Optional<WaylineFileDTO> validKmzFile(MultipartFile file) {
+        String filename = file.getOriginalFilename();
+        if (Objects.nonNull(filename) && !filename.endsWith(WAYLINE_FILE_SUFFIX)) {
+            throw new RuntimeException("The file format is incorrect.");
+        }
+        try (ZipInputStream unzipFile = new ZipInputStream(file.getInputStream(), StandardCharsets.UTF_8)) {
+
+            ZipEntry nextEntry = unzipFile.getNextEntry();
+            while (Objects.nonNull(nextEntry)) {
+                boolean isWaylines = (KmzFileProperties.FILE_DIR_FIRST + "/" + KmzFileProperties.FILE_DIR_SECOND_WAYLINES).equals(nextEntry.getName());
+                if (!isWaylines) {
+                    nextEntry = unzipFile.getNextEntry();
+                    continue;
+                }
+                SAXReader reader = new SAXReader();
+                Document document = reader.read(unzipFile);
+                if (!StandardCharsets.UTF_8.name().equals(document.getXMLEncoding())) {
+                    throw new RuntimeException("The file encoding format is incorrect.");
+                }
+
+                Node droneNode = document.selectSingleNode("//" + KmzFileProperties.TAG_WPML_PREFIX + KmzFileProperties.TAG_DRONE_INFO);
+                Node payloadNode = document.selectSingleNode("//" + KmzFileProperties.TAG_WPML_PREFIX + KmzFileProperties.TAG_PAYLOAD_INFO);
+                if (Objects.isNull(droneNode) || Objects.isNull(payloadNode)) {
+                    throw new RuntimeException("The file format is incorrect.");
+                }
+
+                String type = droneNode.valueOf(KmzFileProperties.TAG_WPML_PREFIX + KmzFileProperties.TAG_DRONE_ENUM_VALUE);
+                String subType = droneNode.valueOf(KmzFileProperties.TAG_WPML_PREFIX + KmzFileProperties.TAG_DRONE_SUB_ENUM_VALUE);
+                String payloadType = payloadNode.valueOf(KmzFileProperties.TAG_WPML_PREFIX + KmzFileProperties.TAG_PAYLOAD_ENUM_VALUE);
+                String payloadSubType = payloadNode.valueOf(KmzFileProperties.TAG_WPML_PREFIX + KmzFileProperties.TAG_PAYLOAD_SUB_ENUM_VALUE);
+                String templateId = document.valueOf("//" + KmzFileProperties.TAG_WPML_PREFIX + KmzFileProperties.TAG_TEMPLATE_ID);
+
+                if (!StringUtils.hasText(type) || !StringUtils.hasText(subType) ||
+                        !StringUtils.hasText(payloadSubType) || !StringUtils.hasText(payloadType) ||
+                        !StringUtils.hasText(templateId)) {
+                    throw new RuntimeException("The file format is incorrect.");
+                }
+
+                return Optional.of(WaylineFileDTO.builder()
+                        .droneModelKey(String.format("%s-%s-%s", DeviceDomainEnum.SUB_DEVICE.getVal(), type, subType))
+                        .payloadModelKeys(List.of(String.format("%s-%s-%s",DeviceDomainEnum.PAYLOAD.getVal(), payloadType, payloadSubType)))
+                        .objectKey(OssConfiguration.objectDirPrefix + File.separator + filename)
+                        .name(filename.substring(0, filename.lastIndexOf(WAYLINE_FILE_SUFFIX)))
+                        .sign(DigestUtils.md5DigestAsHex(file.getInputStream()))
+                        .templateTypes(List.of(Integer.parseInt(templateId)))
+                        .build());
+            }
+
+        } catch (IOException | DocumentException e) {
+            e.printStackTrace();
+        }
+        return Optional.empty();
+    }
+    /**
+     * Convert database entity objects into wayline data transfer object.
+     * @param entity
+     * @return
+     */
+    private WaylineFileDTO entityConvertToDTO(WaylineFileEntity entity) {
+        if (entity == null) {
+            return null;
+        }
+        return WaylineFileDTO.builder()
+                .droneModelKey(entity.getDroneModelKey())
+                .favorited(entity.getFavorited())
+                .name(entity.getName())
+                .payloadModelKeys(entity.getPayloadModelKeys() != null ?
+                        Arrays.asList(entity.getPayloadModelKeys().split(",")) : null)
+                .templateTypes(Arrays.stream(entity.getTemplateTypes().split(","))
+                        .map(Integer::parseInt)
+                        .collect(Collectors.toList()))
+                .username(entity.getUsername())
+                .objectKey(entity.getObjectKey())
+                .sign(entity.getSign())
+                .updateTime(entity.getUpdateTime())
+                .waylineId(entity.getWaylineId())
+                .build();
+
+    }
+
+    /**
+     * Convert the received wayline object into a database entity object.
+     * @param file
+     * @return
+     */
+    private WaylineFileEntity dtoConvertToEntity(WaylineFileDTO file) {
+        WaylineFileEntity.WaylineFileEntityBuilder builder = WaylineFileEntity.builder();
+
+        if (file != null) {
+            builder.droneModelKey(file.getDroneModelKey())
+                    .name(file.getName())
+                    .username(file.getUsername())
+                    .objectKey(file.getObjectKey())
+                    // Separate multiple payload data with ",".
+                    .payloadModelKeys(String.join(",", file.getPayloadModelKeys()))
+                    .templateTypes(file.getTemplateTypes().stream()
+                            .map(String::valueOf)
+                            .collect(Collectors.joining(",")))
+                    .favorited(file.getFavorited())
+                    .sign(file.getSign())
+                    .build();
+        }
+
+        return builder.build();
+    }
+}
diff --git a/ard-sample/src/main/java/com/ard/sample/dock1/wayline/service/impl/Dock1WaylineJobServiceImpl.java b/ard-sample/src/main/java/com/ard/sample/dock1/wayline/service/impl/Dock1WaylineJobServiceImpl.java
new file mode 100644
index 0000000..ab50b7b
--- /dev/null
+++ b/ard-sample/src/main/java/com/ard/sample/dock1/wayline/service/impl/Dock1WaylineJobServiceImpl.java
@@ -0,0 +1,1160 @@
+/*
+ * 【机场1】从 dock2 原类 com.ard.sample.wayline.service.impl.WaylineJobServiceImpl 复制/独立适配(DJI Dock 1 / M30)。
+ * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。
+ */
+package com.ard.sample.dock1.wayline.service.impl;
+
+import com.ard.common.config.OssConfiguration;
+import com.ard.oss.service.impl.OssServiceContext;
+import com.ard.route.domain.route.ArdRouteInfo;
+import com.ard.route.service.IArdRouteInfoService;
+import com.ard.sample.config.DockConfiguration;
+import com.ard.sample.utils.TimestampUtils;
+import com.baomidou.mybatisplus.core.conditions.Wrapper;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.ard.common.core.domain.model.LoginUser;
+import com.ard.sample.common.error.CommonErrorEnum;
+import com.ard.sample.common.model.Pagination;
+import com.ard.sample.common.model.PaginationData;
+import com.ard.sample.common.model.ResponseResult;
+import com.ard.sample.component.mqtt.model.*;
+import com.ard.sample.component.mqtt.service.IMessageSenderService;
+import com.ard.common.core.redis.RedisConst;
+import com.ard.common.core.redis.RedisOpsUtils;
+import com.ard.sample.manage.model.dto.DeviceDTO;
+import com.ard.sample.manage.model.enums.DeviceDomainEnum;
+import com.ard.sample.manage.model.param.DeviceQueryParam;
+import com.ard.sample.manage.service.IDeviceService;
+import com.ard.sample.media.model.MediaFileCountDTO;
+import com.ard.sample.media.model.MediaMethodEnum;
+import com.ard.sample.media.service.IFileService;
+import com.ard.sample.wayline.mapper.IWaylineJobMapper;
+import com.ard.sample.wayline.model.dto.*;
+import com.ard.sample.wayline.model.entity.WaylineJobEntity;
+import com.ard.sample.wayline.model.enums.WaylineJobStatusEnum;
+import com.ard.sample.wayline.model.enums.WaylineMethodEnum;
+import com.ard.sample.wayline.model.enums.WaylineTaskTypeEnum;
+import com.ard.sample.wayline.model.param.CreateJobParam;
+import com.ard.sample.dock1.wayline.service.IWaylineJobService;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.ibatis.annotations.Param;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.integration.mqtt.support.MqttHeaders;
+import org.springframework.messaging.MessageHeaders;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Isolation;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+import javax.annotation.Resource;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.sql.SQLException;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.util.*;
+import java.util.stream.Collectors;
+
+import static com.ard.route.util.ToolUtils.kmzPath;
+import static com.ard.route.util.ToolUtils.subStringMinioUrlName;
+import com.ard.sample.dock1.wayline.model.dto.Dock1WaylineTaskCreateDTO;
+import com.ard.sample.wayline.service.impl.WaylineJobServiceImpl;
+
+/**
+ * @author sean
+ * @version 1.1
+ * @date 2022/6/1
+ */
+@Service
+@Transactional
+@Slf4j
+@SuppressWarnings("all")
+public class Dock1WaylineJobServiceImpl implements IWaylineJobService {
+
+    /**
+     * 机场1 设备类型(DJI Dock 1)。
+     */
+    private static final int DOCK1_DEVICE_TYPE = 1;
+
+    @Autowired
+    private IWaylineJobMapper mapper;
+
+    //    @Autowired
+//    private IWaylineFileService waylineFileService;
+    @Autowired
+    private TimestampUtils _timestampUtils;
+
+    @Autowired
+    private IArdRouteInfoService _ardRouteInfoService;
+
+    @Resource
+    private OssServiceContext _ossService;
+
+    @Autowired
+    private IDeviceService deviceService;
+
+    @Autowired
+    private IMessageSenderService messageSender;
+
+    @Autowired
+    private ObjectMapper objectMapper;
+
+    @Autowired
+    private IFileService fileService;
+
+    /**
+     * 插入航路作业
+     *
+     * @param jobEntity 航路作业实体
+     * @return 航路作业DTO的Optional包装对象,如果插入失败则返回Optional.empty()
+     */
+    private Optional<WaylineJobDTO> insertWaylineJob(WaylineJobEntity jobEntity) {
+        int id = mapper.insert(jobEntity);
+        if (id <= 0) {
+            return Optional.empty();
+        }
+        return Optional.ofNullable(this.entity2Dto(jobEntity));
+    }
+
+    /**
+     * 创建航线任务
+     *
+     * @param param     任务参数
+     * @param username  用户名
+     * @param beginTime 开始时间
+     * @param endTime   结束时间
+     * @return Optional<WaylineJobDTO> 返回创建的航线任务对象,若创建失败则返回Optional.empty()
+     */
+    @Override
+    public Optional<WaylineJobDTO> createWaylineJob(CreateJobParam param, String username, Long beginTime, Long endTime) {
+        if (Objects.isNull(param)) {
+            return Optional.empty();
+        }
+        // Immediate tasks, allocating time on the backend.
+        WaylineJobEntity jobEntity = WaylineJobEntity.builder()
+                .name(param.getName())
+                .dockSn(param.getDockSn())
+                .fileId(param.getFileId())
+                .username(username)
+                .jobId(UUID.randomUUID().toString())
+                .beginTime(beginTime)
+                .endTime(endTime)
+                .status(WaylineJobStatusEnum.PENDING.getVal())
+                .taskType(param.getTaskType())
+                .waylineType(param.getWaylineType())
+                .outOfControlAction(param.getOutOfControlAction())
+                .rthAltitude(param.getRthAltitude())
+                .rthMode(param.getRth_mode())
+                .mediaCount(0)
+                .parentId(param.getParentId())
+                .loopSum(param.getLoopSum())
+                .build();
+
+        return insertWaylineJob(jobEntity);
+    }
+
+    /**
+     * 根据父作业ID创建新的作业
+     *
+     * @param parentId 父作业ID
+     * @return 新创建的作业的DTO对象,如果父作业不存在,则返回Optional.empty()
+     */
+    @Override
+    public Optional<WaylineJobDTO> createWaylineJobByParent(String parentId) {
+        Optional<WaylineJobDTO> parentJobOpt = this.getJobByJobId(parentId);
+        if (parentJobOpt.isEmpty()) {
+            return Optional.empty();
+        }
+        WaylineJobEntity jobEntity = this.dto2Entity(parentJobOpt.get());
+        jobEntity.setJobId(UUID.randomUUID().toString());
+        jobEntity.setErrorCode(null);
+        jobEntity.setCompletedTime(null);
+        jobEntity.setExecuteTime(null);
+        jobEntity.setStatus(WaylineJobStatusEnum.PENDING.getVal());
+        jobEntity.setParentId(parentId);
+
+        return this.insertWaylineJob(jobEntity);
+    }
+
+    /**
+     * 发布飞行任务
+     *
+     * @param param     任务参数
+     * @param loginUser 登录用户
+     * @return ResponseResult 返回结果
+     * @throws SQLException SQL异常
+     */
+    @Override
+    public ResponseResult publishFlightTask(CreateJobParam param, LoginUser loginUser) throws SQLException {
+        if (WaylineTaskTypeEnum.IMMEDIATE.getVal() == param.getTaskType()) {
+            param.setExecuteTime(System.currentTimeMillis());
+        }
+        if (param.getExecuteTime() < _timestampUtils.plusMinutesByMillis(System.currentTimeMillis(), 10) && param.getTaskType() == WaylineTaskTypeEnum.TIMED.getVal()) {
+            return ResponseResult.error(1234,"定时任务执行时间不能小于当前时间的后10分钟!");
+        }
+        // 机场1:共享 WaylineLoopTaskServiceImpl 在生成循环子任务时会固定下发 rth_mode=1,
+        // 该字段 Dock1(M30)的 flighttask_prepare 不支持。在共享调度器按机型分流之前,
+        // 机场1 不创建循环任务,避免向设备下发非法参数。
+        if (param.getTaskType() == WaylineTaskTypeEnum.LOOP.getVal()) {
+            return ResponseResult.error(314013003, "机场1暂不支持循环任务,请选择立即执行或定时任务!");
+        }
+        // 机场1:/dock1 接口只允许给 Dock1(domain=3, type=1)创建任务,
+        // 避免误把机场2/3 的 SN 传进来,导致任务落到错误链路。
+        if (!isDock1Device(param.getDockSn())) {
+            return ResponseResult.error(314013004, "该设备不属于机场1,请使用对应的机场接口操作!");
+        }
+        param.setRth_mode(1);
+        Optional<WaylineJobDTO> waylineJobOpt = this.createWaylineJob(param, loginUser.getUsername(),
+                param.getExecuteTime(), param.getExecuteTime());
+
+        if (waylineJobOpt.isEmpty()) {
+            return ResponseResult.error(314013000, "创建机场任务失败!");
+        }
+
+        WaylineJobDTO waylineJob = waylineJobOpt.get();
+
+        boolean isOnline = deviceService.checkDeviceOnline(waylineJob.getDockSn());
+        if (!isOnline) {
+            return ResponseResult.error(CommonErrorEnum.DEVICE_OFFLINE.getErrorMsg());
+        }
+
+        // get wayline file
+//        Optional<WaylineFileDTO> waylineFile = waylineFileService.getWaylineByWaylineId(waylineJob.getFileId());
+//        if (waylineFile.isEmpty()) {
+//            return ResponseResult.error("机场任务航线不存在!");
+//        }
+        // 换航线表
+        ArdRouteInfo ardRouteOpt = _ardRouteInfoService.selectArdRouteInfoById(waylineJob.getFileId());
+        if (ardRouteOpt == null || ardRouteOpt.getPath() == null || ardRouteOpt.getPath().isEmpty()) {
+            return ResponseResult.error(314013001, "机场任务航线不存在!");
+        }
+
+        // get file url
+//        URL url = waylineFileService.getObjectUrl(waylineFile.get().getWaylineId());
+        URL url = null;
+        String minioUrl = subStringMinioUrlName(ardRouteOpt.getPath());
+        String md5 = "";
+        String httpUrl = "";
+        try (InputStream inputStream = _ossService.getObject(kmzPath, minioUrl)) {
+            md5 = DigestUtils.md5Hex(inputStream);
+            url = new URL(ardRouteOpt.getPath());
+            log.info("机场任务航线文件url地址转换前:{}", url);
+            httpUrl = url.toString().replace(OssConfiguration.endpoint, DockConfiguration.endpoint);
+            log.info("机场任务航线文件url地址转换后:{}", httpUrl);
+            log.info("机场任务航线文件md5值:{}", md5);
+        } catch (Exception e) {
+            log.error("机场任务航线文件地址错误!{}", e.getMessage());
+            return ResponseResult.error(314013002, "机场任务航线文件地址错误!" + e.getMessage());
+        }
+
+        log.info("机场任务-rth_mode的值:{}", waylineJob.getRthMode());
+
+        Dock1WaylineTaskCreateDTO flightTask = Dock1WaylineTaskCreateDTO.builder()
+                .flightId(waylineJob.getJobId())
+                .executeTime(waylineJob.getBeginTime().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli())
+                .taskType(waylineJob.getTaskType())
+                // 机场1:C4 dock1(M30)无 wayline_precision_type,不下发
+                .rthAltitude(waylineJob.getRthAltitude())
+                .outOfControlAction(waylineJob.getOutOfControlAction())
+                // 机场1:C4 dock1(M30)无 rth_mode,不下发
+                .file(WaylineTaskFileDTO.builder()
+//                        .url(url.toString().replace("https://192.168.10.103:9001", "http://192.168.10.103:9011"))
+//                        .fingerprint(waylineFile.get().getSign())
+//                        .replace("https://", "http://")
+//                        .replace("9001", "9011")
+                        .url(httpUrl)
+                        .fingerprint(md5)
+                        .build())
+//                .simulateMission(WaylineSimulateMissionDTO.builder()
+//                        .isEnable(1)
+//                        .latitude(46.55819013988211)
+//                        .longitude(125.14652370145113)
+//                        .build())
+                .build();
+
+        String topic = TopicConst.THING_MODEL_PRE + TopicConst.PRODUCT +
+                waylineJob.getDockSn() + TopicConst.SERVICES_SUF;
+        CommonTopicResponse<Object> response = CommonTopicResponse.builder()
+                .tid(UUID.randomUUID().toString())
+                .bid(waylineJob.getJobId())
+                .timestamp(System.currentTimeMillis())
+                .data(flightTask)
+                .method(WaylineMethodEnum.FLIGHT_TASK_PREPARE.getMethod())
+                .build();
+
+        // 下发参数打印
+        try {
+            String json = objectMapper.writeValueAsString(response);
+            log.info("机场下发航线任务的参数response:{}", json);
+        } catch (Exception e) {
+            log.error("机场下发航线任务的参数response异常:{}", e.getMessage());
+        }
+
+        ServiceReply serviceReply = messageSender.publishWithReply(topic, response);
+
+        log.info("下发任务结果: {}", serviceReply.getResult());
+
+        if (ResponseResult.CODE_SUCCESS != serviceReply.getResult()) {
+            String errorMsg = CommonErrorEnum.getMsgByCode(serviceReply.getResult());
+            log.info("Prepare task ====> Error code: {},Error Msg:{}", serviceReply.getResult(), errorMsg);
+            this.updateJob(WaylineJobDTO.builder()
+                    .jobId(waylineJob.getJobId())
+                    .executeTime(LocalDateTime.now())
+                    .status(WaylineJobStatusEnum.FAILED.getVal())
+                    .completedTime(LocalDateTime.now())
+                    .code(serviceReply.getResult()).build());
+            //return ResponseResult.error("Prepare task ====> Error code: " + serviceReply.getResult());
+            return ResponseResult.error(serviceReply.getResult(), errorMsg);
+        }
+
+        // 飞行命令先注释掉
+        // Issue an immediate task execution command.
+        if (WaylineTaskTypeEnum.IMMEDIATE.getVal() == waylineJob.getTaskType()) {
+            if (!executeFlightTask(waylineJob.getJobId())) {
+                return ResponseResult.error("无法执行作业。");
+            }
+        }
+
+        if (WaylineTaskTypeEnum.TIMED.getVal() == waylineJob.getTaskType()) {
+            boolean isAdd = RedisOpsUtils.zAdd(RedisConst.WAYLINE_JOB_TIMED_EXECUTE,
+                    waylineJob.getDockSn() + RedisConst.DELIMITER + waylineJob.getJobId(),
+                    waylineJob.getBeginTime().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
+            if (!isAdd) {
+                return ResponseResult.error("无法创建计划作业。");
+            }
+        }
+
+        return ResponseResult.success();
+    }
+
+    public ResponseResult publishFlightTaskPrepare(Optional<WaylineJobDTO> waylineJobOpt) throws SQLException {
+
+        if (waylineJobOpt.isEmpty()) {
+            return ResponseResult.error(314013000, "创建机场任务失败!");
+        }
+        WaylineJobDTO waylineJob = waylineJobOpt.get();
+        boolean isOnline = deviceService.checkDeviceOnline(waylineJob.getDockSn());
+        if (!isOnline) {
+            return ResponseResult.error("机场不在线!");
+        }
+        // 换航线表
+        ArdRouteInfo ardRouteOpt = _ardRouteInfoService.selectArdRouteInfoById(waylineJob.getFileId());
+        if (ardRouteOpt == null || ardRouteOpt.getPath() == null || ardRouteOpt.getPath().isEmpty()) {
+            return ResponseResult.error(314013001, "机场任务航线不存在!");
+        }
+        // get file url
+        URL url = null;
+        String minioUrl = subStringMinioUrlName(ardRouteOpt.getPath());
+        String md5 = "";
+        String httpUrl = "";
+        try (InputStream inputStream = _ossService.getObject(kmzPath, minioUrl)) {
+            md5 = DigestUtils.md5Hex(inputStream);
+            url = new URL(ardRouteOpt.getPath());
+            log.info("机场任务航线文件url地址转换前:{}", url);
+            httpUrl = url.toString().replace(OssConfiguration.endpoint, DockConfiguration.endpoint);
+            log.info("机场任务航线文件url地址转换后:{}", httpUrl);
+            log.info("机场任务航线文件md5值:{}", md5);
+        } catch (Exception e) {
+            log.error("机场任务航线文件地址错误!{}", e.getMessage());
+            return ResponseResult.error(314013002, "机场任务航线文件地址错误!" + e.getMessage());
+        }
+
+        Dock1WaylineTaskCreateDTO flightTask = Dock1WaylineTaskCreateDTO.builder()
+                .flightId(waylineJob.getJobId())
+                .executeTime(waylineJob.getBeginTime().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli())
+                .taskType(waylineJob.getTaskType())
+                // 机场1:C4 dock1(M30)无 wayline_precision_type,不下发
+                .rthAltitude(waylineJob.getRthAltitude())
+                .outOfControlAction(waylineJob.getOutOfControlAction())
+                // 机场1:C4 dock1(M30)无 rth_mode,不下发
+                .file(WaylineTaskFileDTO.builder()
+                        .url(httpUrl)
+//                        .fingerprint(waylineFile.get().getSign())
+                        .fingerprint(md5)
+                        .build())
+//                .simulateMission(WaylineSimulateMissionDTO.builder()
+//                        .isEnable(1)
+//                        .latitude(46.558831038)
+//                        .longitude(125.148072319)
+//                        .build())
+                .build();
+
+        // 下发参数打印
+        try {
+            String json = objectMapper.writeValueAsString(flightTask);
+            log.info("机场下发循环任务子任务-定时任务的航线任务的参数flightTask:{}", json);
+        } catch (Exception e) {
+            log.error("机场下发循环任务子任务-定时任务的航线任务的参数flightTask异常:{}", e.getMessage());
+        }
+
+        String topic = TopicConst.THING_MODEL_PRE + TopicConst.PRODUCT +
+                waylineJob.getDockSn() + TopicConst.SERVICES_SUF;
+        CommonTopicResponse<Object> response = CommonTopicResponse.builder()
+                .tid(UUID.randomUUID().toString())
+                .bid(waylineJob.getJobId())
+                .timestamp(System.currentTimeMillis())
+                .data(flightTask)
+                .method(WaylineMethodEnum.FLIGHT_TASK_PREPARE.getMethod())
+                .build();
+
+        ServiceReply serviceReply = messageSender.publishWithReply(topic, response);
+
+        log.info("下发循环任务子任务-定时任务的结果: {}", serviceReply.getResult());
+        log.info("机场循环任务子任务-定时任务的航线文件url地址:{}", httpUrl);
+        log.info("机场循环任务子任务-定时任务的航线文件md5值:{}", md5);
+
+        if (ResponseResult.CODE_SUCCESS != serviceReply.getResult()) {
+            String errorMsg = CommonErrorEnum.getMsgByCode(serviceReply.getResult());
+            log.info("Prepare task ====> Error code: {},Error Msg: {}", serviceReply.getResult(), errorMsg);
+            this.updateJob(WaylineJobDTO.builder()
+                    .jobId(waylineJob.getJobId())
+                    .executeTime(LocalDateTime.now())
+                    .status(WaylineJobStatusEnum.FAILED.getVal())
+                    .completedTime(LocalDateTime.now())
+                    .code(serviceReply.getResult()).build());
+//            return ResponseResult.error("Prepare task ====> Error code: " + serviceReply.getResult());
+            return ResponseResult.error(serviceReply.getResult(), errorMsg);
+        }
+
+        // 飞行命令先注释掉
+        // Issue an immediate task execution command.
+//        if (WaylineTaskTypeEnum.IMMEDIATE.getVal() == waylineJob.getTaskType()) {
+//            if (!executeFlightTask(waylineJob.getJobId())) {
+//                return ResponseResult.error("无法执行作业。");
+//            }
+//        }
+
+        if (WaylineTaskTypeEnum.TIMED.getVal() == waylineJob.getTaskType()) {
+            boolean isAdd = RedisOpsUtils.zAdd(RedisConst.WAYLINE_JOB_TIMED_EXECUTE,
+                    waylineJob.getDockSn() + RedisConst.DELIMITER + waylineJob.getJobId(),
+                    waylineJob.getBeginTime().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
+            if (!isAdd) {
+                return ResponseResult.error("无法创建计划作业。");
+            }
+        }
+
+        return ResponseResult.success();
+    }
+
+    /**
+     * 执行飞行任务
+     *
+     * @param jobId 任务ID
+     * @return 如果任务执行成功返回true,否则返回false
+     * @throws IllegalArgumentException 如果任务不存在
+     * @throws RuntimeException         如果设备处于离线状态
+     */
+    @Override
+    public Boolean executeFlightTask(String jobId) {
+        // get job
+        Optional<WaylineJobDTO> waylineJob = this.getJobByJobId(jobId);
+        if (waylineJob.isEmpty()) {
+            throw new IllegalArgumentException("计划任务不存在。");
+        }
+
+        boolean isOnline = deviceService.checkDeviceOnline(waylineJob.get().getDockSn());
+        if (!isOnline) {
+//            throw new RuntimeException("Dock is offline.");
+            throw new RuntimeException(CommonErrorEnum.DEVICE_OFFLINE.getErrorMsg());
+        }
+
+        WaylineJobDTO job = waylineJob.get();
+        Dock1WaylineTaskCreateDTO flightTask = Dock1WaylineTaskCreateDTO.builder().flightId(jobId).build();
+        // flightTask中,只有任务id是有值的,其他字段不返回
+        String topic = TopicConst.THING_MODEL_PRE + TopicConst.PRODUCT +
+                job.getDockSn() + TopicConst.SERVICES_SUF;
+        CommonTopicResponse<Object> response = CommonTopicResponse.builder()
+                .tid(UUID.randomUUID().toString())
+                .bid(jobId)
+                .timestamp(System.currentTimeMillis())
+                .data(flightTask)
+                .method(WaylineMethodEnum.FLIGHT_TASK_EXECUTE.getMethod())
+                .build();
+
+        // 下发参数打印
+        try {
+            String json = objectMapper.writeValueAsString(response);
+            log.info("机场下发-执行飞行任务-的参数response:{}", json);
+        } catch (Exception e) {
+            log.error("机场下发-执行飞行任务-的参数response异常:{}", e.getMessage());
+        }
+
+        ServiceReply serviceReply = messageSender.publishWithReply(topic, response);
+
+        log.info("下发-执行飞行任务-结果: {}", serviceReply.getResult());
+
+        if (ResponseResult.CODE_SUCCESS != serviceReply.getResult()) {
+            String errorMsg = CommonErrorEnum.getMsgByCode(serviceReply.getResult());
+            log.info("Execute job ====> Error code: {},Error Msg:{}", serviceReply.getResult(), errorMsg);
+            this.updateJob(WaylineJobDTO.builder()
+                    .jobId(jobId)
+                    .executeTime(LocalDateTime.now())
+                    .status(WaylineJobStatusEnum.FAILED.getVal())
+                    .completedTime(LocalDateTime.now())
+                    .code(serviceReply.getResult()).build());
+            return false;
+        }
+
+        this.updateJob(WaylineJobDTO.builder()
+                .jobId(jobId)
+                .executeTime(LocalDateTime.now())
+                .status(WaylineJobStatusEnum.IN_PROGRESS.getVal())
+                .build());
+        RedisOpsUtils.setWithExpire(RedisConst.WAYLINE_JOB_RUNNING_PREFIX + job.getDockSn(),
+                EventsReceiver.<WaylineTaskProgressReceiver>builder().bid(jobId).sn(job.getDockSn()).build(),
+                RedisConst.DEVICE_ALIVE_SECOND * RedisConst.DEVICE_ALIVE_SECOND);
+        return true;
+    }
+
+    /**
+     * 取消飞行任务
+     *
+     * @param jobIds 需要取消的任务ID集合
+     * @throws IllegalArgumentException 如果任务状态不正确,抛出非法参数异常
+     */
+    @Override
+    public ResponseResult cancelFlightTask(Collection<String> jobIds) {
+        if (CollectionUtils.isEmpty(jobIds)) {
+            return ResponseResult.error(98, "任务ID不能为空");
+        }
+        // 机场1:按机场分组,逐个下发取消,不能在第一组处理完后直接 return,
+        // 否则同一请求中包含多个机场(或多个 SN)时,后面的任务不会被取消。
+        Map<String, Set<String>> dockJobs = new LinkedHashMap<>();
+        for (String jobId : jobIds) {
+            Optional<WaylineJobDTO> jobOpt = this.getJobByJobId(jobId);
+            if (jobOpt.isEmpty()) {
+                return ResponseResult.error("任务不存在,jobId: " + jobId);
+            }
+            WaylineJobDTO job = jobOpt.get();
+            // 机场1:/dock1 接口只允许取消机场1(Dock1)的任务,避免通过 jobId 误取消机场2/3 的任务。
+            if (!isDock1Device(job.getDockSn())) {
+                return ResponseResult.error(314013004, "该任务不属于机场1,请使用对应的机场接口操作!");
+            }
+            // 循环主任务只是模板,真正的执行体是子任务:主任务本地置为已取消,执行中的子任务下发取消。
+            if (job.getTaskType() != null && job.getTaskType() == WaylineTaskTypeEnum.LOOP.getVal()) {
+                for (WaylineJobDTO child : this.getJobByJobParentId(jobId)) {
+                    if (isJobEnded(child.getStatus())) {
+                        continue;
+                    }
+                    if (Objects.equals(child.getStatus(), WaylineJobStatusEnum.IN_PROGRESS.getVal())) {
+                        dockJobs.computeIfAbsent(child.getDockSn(), k -> new LinkedHashSet<>()).add(child.getJobId());
+                    } else {
+                        this.cancelJobLocally(child.getJobId(), child.getDockSn());
+                    }
+                }
+                this.cancelJobLocally(jobId, job.getDockSn());
+                continue;
+            }
+            if (isJobEnded(job.getStatus())) {
+                continue;
+            }
+            dockJobs.computeIfAbsent(job.getDockSn(), k -> new LinkedHashSet<>()).add(jobId);
+        }
+
+        ResponseResult errorResult = null;
+        for (Map.Entry<String, Set<String>> entry : dockJobs.entrySet()) {
+            ResponseResult result = this.publishCancelTask(entry.getKey(), new ArrayList<>(entry.getValue()));
+            if (ResponseResult.CODE_SUCCESS != result.getCode() && errorResult == null) {
+                errorResult = result;
+            }
+        }
+        return errorResult == null ? ResponseResult.success() : errorResult;
+    }
+
+    /**
+     * 机场1:判断 SN 是否为 Dock1 机场设备(domain=3, type=1)。
+     *
+     * @param sn 设备序列号
+     * @return true 表示属于机场1
+     */
+    private boolean isDock1Device(String sn) {
+        if (!StringUtils.hasText(sn)) {
+            return false;
+        }
+        Optional<DeviceDTO> deviceOpt = deviceService.getDeviceBySn(sn);
+        return deviceOpt.isPresent()
+                && Objects.equals(DeviceDomainEnum.DOCK.getVal(), deviceOpt.get().getDomain())
+                && Objects.equals(DOCK1_DEVICE_TYPE, deviceOpt.get().getType());
+    }
+
+    /**
+     * 机场1:任务是否已经处于终态(成功/取消/失败/未知)。
+     */
+    private boolean isJobEnded(Integer status) {
+        if (status == null) {
+            return false;
+        }
+        return Boolean.TRUE.equals(WaylineJobStatusEnum.find(status).getEnd());
+    }
+
+    /**
+     * 机场1:只在本地把任务标记为已取消,并清理定时执行队列,不删除历史记录。
+     */
+    private void cancelJobLocally(String jobId, String dockSn) {
+        this.updateJob(WaylineJobDTO.builder()
+                .jobId(jobId)
+                .status(WaylineJobStatusEnum.CANCEL.getVal())
+                .completedTime(LocalDateTime.now())
+                .build());
+        if (dockSn != null) {
+            RedisOpsUtils.zRemove(RedisConst.WAYLINE_JOB_TIMED_EXECUTE,
+                    dockSn + RedisConst.DELIMITER + jobId);
+        }
+    }
+
+// region 代码备份
+//    @Override
+//    public void cancelFlightTask(Collection<String> jobIds) {
+//        List<WaylineJobDTO> waylineJobs = getJobsByConditions(jobIds, WaylineJobStatusEnum.PENDING);
+//
+////        Set<String> waylineJobIds = waylineJobs.stream().map(WaylineJobDTO::getJobId).collect(Collectors.toSet());
+////        // Check if the task status is correct.
+////        boolean isErr = !jobIds.removeAll(waylineJobIds) || !jobIds.isEmpty();
+////        if (isErr) {
+////            throw new IllegalArgumentException("任务开始后,无法取消。" + Arrays.toString(jobIds.toArray()));
+//////            throw new IllegalArgumentException("These tasks have an incorrect status and cannot be canceled. "
+//////                    + Arrays.toString(jobIds.toArray()));
+////        }
+//
+//        // Group job id by dock sn.
+//        Map<String, List<String>> dockJobs = waylineJobs.stream()
+//                .collect(Collectors.groupingBy(WaylineJobDTO::getDockSn,
+//                        Collectors.mapping(WaylineJobDTO::getJobId, Collectors.toList())));
+//        dockJobs.forEach((dockSn, idList) -> this.publishCancelTask(dockSn, idList));
+//
+//    }
+
+//@Override
+//public void publishCancelTask(String dockSn, List<String> jobIds) {
+//
+//    boolean isOnline = deviceService.checkDeviceOnline(dockSn);
+//    if (!isOnline) {
+//        Integer errorCode = CommonErrorEnum.DEVICE_OFFLINE.getErrorCode();
+//
+//        //return ResponseResult.error(errorCode, CommonErrorEnum.getMsgByCode(errorCode)));
+//        throw new RuntimeException(CommonErrorEnum.DEVICE_OFFLINE.getErrorMsg());
+//    }
+//    String topic = TopicConst.THING_MODEL_PRE + TopicConst.PRODUCT + dockSn + TopicConst.SERVICES_SUF;
+//
+//    CommonTopicResponse<Object> response = CommonTopicResponse.builder()
+//            .tid(UUID.randomUUID().toString())
+//            .bid(UUID.randomUUID().toString())
+//            .timestamp(System.currentTimeMillis())
+//            .data(Map.of(MapKeyConst.FLIGHT_IDS, jobIds))
+//            .method(WaylineMethodEnum.FLIGHT_TASK_CANCEL.getMethod())
+//            .build();
+//
+//    ServiceReply serviceReply = messageSender.publishWithReply(topic, response);
+//    if (ResponseResult.CODE_SUCCESS != serviceReply.getResult()) {
+//        String errorMsg = CommonErrorEnum.getMsgByCode(serviceReply.getResult());
+//        log.info("取消飞行任务失败 ====> 设备sn: {}, 错误码: {}, {}", dockSn, serviceReply.getResult(), errorMsg);
+//        //throw new RuntimeException("Failed to cancel the wayline job of " + dockSn);
+//        throw new RuntimeException("设备:" + dockSn + "取消飞行任务失败,错误码:" + serviceReply.getResult() + "," + errorMsg);
+//    }
+//
+//    for (String jobId : jobIds) {
+//        this.updateJob(WaylineJobDTO.builder()
+//                .jobId(jobId)
+//                .status(WaylineJobStatusEnum.CANCEL.getVal())
+//                .completedTime(LocalDateTime.now())
+//                .build());
+//        RedisOpsUtils.zRemove(RedisConst.WAYLINE_JOB_TIMED_EXECUTE, RedisConst.DELIMITER + dockSn + RedisConst.DELIMITER + jobId);
+//        // 原流程是修改表中数据的status状态值,现在的精力是直接删除
+//        this.deleteTask(jobId);
+//    }
+//
+//}
+// endregion
+
+    /**
+     * 发布取消任务
+     *
+     * @param dockSn 设备序列号
+     * @param jobIds 任务ID列表
+     * @throws RuntimeException 如果设备离线或取消任务失败,则抛出运行时异常
+     */
+    @Override
+    public ResponseResult publishCancelTask(String dockSn, List<String> jobIds) {
+        if (CollectionUtils.isEmpty(jobIds)) {
+            return ResponseResult.success();
+        }
+        for (String jobId : jobIds) {
+            Optional<WaylineJobDTO> waylineJobDTO = this.getJobByJobId(jobId);
+            if (waylineJobDTO.isEmpty()) {
+                return ResponseResult.error("任务不存在,jobId: " + jobId);
+            }
+        }
+        boolean isOnline = deviceService.checkDeviceOnline(dockSn);
+        if (!isOnline) {
+            Integer errorCode = CommonErrorEnum.DEVICE_OFFLINE.getErrorCode();
+            return ResponseResult.error(errorCode, CommonErrorEnum.getMsgByCode(errorCode));
+        }
+        String topic = TopicConst.THING_MODEL_PRE + TopicConst.PRODUCT + dockSn + TopicConst.SERVICES_SUF;
+
+        CommonTopicResponse<Object> response = CommonTopicResponse.builder()
+                .tid(UUID.randomUUID().toString())
+                .bid(UUID.randomUUID().toString())
+                .timestamp(System.currentTimeMillis())
+                .data(Map.of(MapKeyConst.FLIGHT_IDS, jobIds))
+                .method(WaylineMethodEnum.FLIGHT_TASK_CANCEL.getMethod())
+                .build();
+
+        ServiceReply serviceReply = messageSender.publishWithReply(topic, response);
+        if (ResponseResult.CODE_SUCCESS != serviceReply.getResult()) {
+            String errorMsg = CommonErrorEnum.getMsgByCode(serviceReply.getResult());
+            log.info("取消飞行任务失败 ====> 设备sn: {}, 错误码: {}, {}", dockSn, serviceReply.getResult(), errorMsg);
+            return ResponseResult.error(serviceReply.getResult(), errorMsg);
+        }
+
+        // 机场1:取消成功后只更新状态并清理定时队列,保留历史记录,避免“取消”等同于“删除”。
+        for (String jobId : jobIds) {
+            this.cancelJobLocally(jobId, dockSn);
+        }
+        return ResponseResult.success();
+    }
+
+    @Override
+    public void deleteTask(String jobid) {
+        mapper.delete(new LambdaQueryWrapper<WaylineJobEntity>()
+                .eq(WaylineJobEntity::getJobId, jobid));
+    }
+
+    /**
+     * 根据条件查询作业列表
+     *
+     * @param jobIds 作业ID集合
+     * @param status 作业状态枚举
+     * @return 满足条件的作业DTO列表
+     */
+    @Override
+    public List<WaylineJobDTO> getJobsByConditions(Collection<String> jobIds, WaylineJobStatusEnum status) {
+        return mapper.selectList(
+                        new LambdaQueryWrapper<WaylineJobEntity>()
+                                // 机场1:原实现误用 ne(取的是“非该状态”的任务),修正为 eq。
+                                .eq(Objects.nonNull(status), WaylineJobEntity::getStatus, status.getVal())
+                                .and(!CollectionUtils.isEmpty(jobIds),
+                                        wrapper -> jobIds.forEach(id -> wrapper.eq(WaylineJobEntity::getJobId, id).or())))
+                .stream()
+                .map(this::entity2Dto)
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * 根据任务ID获取任务详情
+     *
+     * @param jobId 任务ID
+     * @return Optional<WaylineJobDTO> 包含任务详情的Optional对象,如果不存在则返回空的Optional
+     */
+    @Override
+    public Optional<WaylineJobDTO> getJobByJobId(String jobId) {
+        WaylineJobEntity jobEntity = mapper.selectOne(
+                new LambdaQueryWrapper<WaylineJobEntity>()
+                        .eq(WaylineJobEntity::getJobId, jobId));
+        return Optional.ofNullable(entity2Dto(jobEntity));
+    }
+
+    public List<WaylineJobDTO> getJobByJobParentId(String parentId) {
+        List<WaylineJobEntity> jobEntities = mapper.selectList(
+                new LambdaQueryWrapper<WaylineJobEntity>()
+                        .eq(WaylineJobEntity::getParentId, parentId));
+        if (CollectionUtils.isEmpty(jobEntities)) {
+            return Collections.emptyList(); // 返回不可变空列表,更安全
+        }
+        return jobEntities.stream()
+                .map(this::entity2Dto) // 调用原有的单个转换方法
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * 更新作业信息
+     *
+     * @param dto 作业信息传输对象
+     * @return 更新成功返回true,否则返回false
+     */
+    @Override
+    public Boolean updateJob(WaylineJobDTO dto) {
+        return mapper.update(this.dto2Entity(dto),
+                new LambdaUpdateWrapper<WaylineJobEntity>()
+                        .eq(WaylineJobEntity::getJobId, dto.getJobId())) > 0;
+    }
+
+    /**
+     * 根据工作区ID获取工作线任务分页数据
+     *
+     * @param page     页码,从1开始
+     * @param pageSize 每页显示条数
+     * @return PaginationData 包含工作线任务分页数据的对象
+     */
+    @Override
+    public PaginationData<WaylineJobDTO> getJobsByWorkspaceId(long page, long pageSize) {
+        Page<WaylineJobEntity> pageData = mapper.selectPage(
+                new Page<WaylineJobEntity>(page, pageSize),
+                new LambdaQueryWrapper<WaylineJobEntity>()
+                        .orderByDesc(WaylineJobEntity::getId));
+        List<WaylineJobDTO> records = pageData.getRecords()
+                .stream()
+                .map(this::entity2Dto)
+                .collect(Collectors.toList());
+
+        return new PaginationData<WaylineJobDTO>(records, new Pagination(pageData));
+    }
+
+    /**
+     * 根据条件筛选获取任务分页数据
+     *
+     * @param page     页码,从1开始
+     * @param pageSize 每页显示条数
+     * @return PaginationData 包含工作线任务分页数据的对象
+     */
+    @Override
+    public PaginationData<WaylineJobDTO> getJobsList(long page, long pageSize, Integer status,
+                                                     Long startTime, Long endTime, Integer taskType, String name, Boolean isComplete) {
+        // 机场1:只查询机场1(Dock1)设备下的任务,避免 /dock1 接口返回机场2/3 的任务。
+        List<String> dock1SnList = deviceService.getDevicesByParams(DeviceQueryParam.builder()
+                        .deviceType(DOCK1_DEVICE_TYPE)
+                        .domains(Collections.singletonList(DeviceDomainEnum.DOCK.getVal()))
+                        .build())
+                .stream()
+                .map(DeviceDTO::getDeviceSn)
+                .collect(Collectors.toList());
+        if (CollectionUtils.isEmpty(dock1SnList)) {
+            Page<WaylineJobEntity> emptyPage = new Page<>(page, pageSize);
+            return new PaginationData<>(Collections.emptyList(), new Pagination(emptyPage));
+        }
+        Page<WaylineJobEntity> pageData = mapper.selectPage(
+                new Page<WaylineJobEntity>(page, pageSize),
+                new LambdaQueryWrapper<WaylineJobEntity>()
+                        .in(WaylineJobEntity::getDockSn, dock1SnList)
+                        .isNull(WaylineJobEntity::getParentId)
+                        // 实际开始时间筛选,允许为空,为空则不进行筛选
+                        .ge(startTime != null, WaylineJobEntity::getBeginTime, startTime)
+                        .le(endTime != null, WaylineJobEntity::getBeginTime, endTime)
+                        // 任务类型筛选,允许为空,为空则不进行筛选
+                        .eq(taskType != null, WaylineJobEntity::getTaskType, taskType)
+                        // 任务名称筛选,允许为空,为空则不进行筛选;....模糊查询
+                        .like(name != null, WaylineJobEntity::getName, "%" + name + "%")
+                        // 根据是否完成筛选,允许为空,为空则不进行筛选
+                        .eq(isComplete != null && isComplete, WaylineJobEntity::getStatus, 3)
+                        .ne(isComplete != null && !isComplete, WaylineJobEntity::getStatus, 3)
+                        // 任务状态筛选,允许为空,为空则不进行筛选
+                        .eq(status != null, WaylineJobEntity::getStatus, status)
+                        .orderByDesc(WaylineJobEntity::getBeginTime));
+        List<WaylineJobDTO> records = pageData.getRecords()
+                .stream()
+                .map(this::entity2Dto)
+                .peek(job -> {
+                    List<WaylineJobDTO> childOpt = this.getJobByJobParentId(job.getJobId());
+                    job.setChildren(childOpt);
+                })
+                .collect(Collectors.toList());
+
+        return new PaginationData<WaylineJobDTO>(records, new Pagination(pageData));
+    }
+
+    public List<WaylineJobEntity> selectList(Long taskType, Long executeTime, String parentId) {
+        return mapper.selectList(new LambdaQueryWrapper<WaylineJobEntity>()
+                .eq(taskType != null, WaylineJobEntity::getTaskType, taskType)
+                .ne(WaylineJobEntity::getStatus, WaylineJobStatusEnum.SUCCESS.getVal())
+                .eq(executeTime != null, WaylineJobEntity::getBeginTime, executeTime)
+                .eq(parentId != null, WaylineJobEntity::getParentId, parentId)
+                .ne(WaylineJobEntity::getStatus, 3)
+                .orderByDesc(WaylineJobEntity::getId));
+    }
+
+    /**
+     * 获取所有待定状态的定时任务,未执行即将要执行的。
+     */
+    public List<WaylineJobEntity> selectScheduledTaskList() {
+        // "1":"定时任务"
+        return mapper.selectList(new LambdaQueryWrapper<WaylineJobEntity>()
+                .eq(WaylineJobEntity::getTaskType, 1)
+                .eq(WaylineJobEntity::getStatus, WaylineJobStatusEnum.PENDING.getVal())
+                .gt(WaylineJobEntity::getBeginTime, System.currentTimeMillis())
+                .orderByDesc(WaylineJobEntity::getBeginTime));
+    }
+
+
+    /**
+     * 根据航班任务ID获取航班任务资源
+     *
+     * @param receiver 接收到的消息对象
+     * @param headers  消息头信息
+     * @Override 覆盖父类方法
+     * @ServiceActivator(inputChannel = ChannelName.INBOUND_REQUESTS_FLIGHT_TASK_RESOURCE_GET, outputChannel = ChannelName.OUTBOUND)
+     * 指定输入通道和输出通道
+     * @Transactional(isolation = Isolation.READ_UNCOMMITTED)
+     * 设置事务隔离级别为未提交读
+     */
+    @Override
+    // 机场1:不复制原 ServiceActivator(inputChannel = ChannelName.INBOUND_REQUESTS_FLIGHT_TASK_RESOURCE_GET),消费职责由共享层原类承担
+    @Transactional(isolation = Isolation.READ_UNCOMMITTED)
+    public void flightTaskResourceGet(CommonTopicReceiver receiver, MessageHeaders headers) {
+        Map<String, String> jobIdMap = objectMapper.convertValue(receiver.getData(), new TypeReference<Map<String, String>>() {
+        });
+        String jobId = jobIdMap.get(MapKeyConst.FLIGHT_ID);
+
+        log.info("机场通过flightTaskResourceGet获取航线文件地址,jobId: " + jobId);
+
+        CommonTopicResponse.CommonTopicResponseBuilder<RequestsReply> builder = CommonTopicResponse.<RequestsReply>builder()
+                .tid(receiver.getTid())
+                .bid(receiver.getBid())
+                .method(RequestsMethodEnum.FLIGHT_TASK_RESOURCE_GET.getMethod())
+                .timestamp(System.currentTimeMillis());
+
+        String topic = headers.get(MqttHeaders.RECEIVED_TOPIC).toString() + TopicConst._REPLY_SUF;
+
+        DeviceDTO device = (DeviceDTO) RedisOpsUtils.get(RedisConst.DEVICE_ONLINE_PREFIX + receiver.getGateway());
+        Optional<WaylineJobDTO> waylineJobOpt = this.getJobByJobId(jobId);
+        if (waylineJobOpt.isEmpty()) {
+            builder.data(RequestsReply.error(CommonErrorEnum.ILLEGAL_ARGUMENT));
+            messageSender.publish(topic, builder.build());
+            return;
+        }
+
+        WaylineJobDTO waylineJob = waylineJobOpt.get();
+
+        // get wayline file
+//        Optional<WaylineFileDTO> waylineFile = waylineFileService.getWaylineByWaylineId(waylineJob.getFileId());
+//        if (waylineFile.isEmpty()) {
+//            builder.data(RequestsReply.error(CommonErrorEnum.ILLEGAL_ARGUMENT));
+//            messageSender.publish(topic, builder.build());
+//            return;
+//        }
+        // 修改航线文件名称,从ard_route_info航线表中取;
+        // 换航线表
+        ArdRouteInfo ardRouteOpt = _ardRouteInfoService.selectArdRouteInfoById(waylineJob.getFileId());
+        if (ardRouteOpt == null) {
+            builder.data(RequestsReply.error(CommonErrorEnum.ILLEGAL_ARGUMENT));
+            messageSender.publish(topic, builder.build());
+            return;
+        }
+
+        // get file url
+        URL url = null;
+        String minioUrl = subStringMinioUrlName(ardRouteOpt.getPath());
+        String md5 = "";
+        String httpUrl = "";
+        try (InputStream inputStream = _ossService.getObject(kmzPath, minioUrl)) {
+            md5 = DigestUtils.md5Hex(inputStream);
+            url = new URL(ardRouteOpt.getPath());
+
+            log.info("机场通过flightTaskResourceGet获取航线文件地址转换前:{}", url);
+            httpUrl = url.toString().replace(OssConfiguration.endpoint, DockConfiguration.endpoint);
+            log.info("机场通过flightTaskResourceGet获取航线文件地址转换后:{}", httpUrl);
+
+            log.info("机场通过flightTaskResourceGet获取航线文件地址,jobId: " + jobId + " 返回信息为url:" + httpUrl + " md5:" + md5);
+//        }
+//        try {
+//            url = waylineFileService.getObjectUrl(waylineFile.get().getWaylineId());
+//            url = new URL(ardRouteOpt.getUpPath());
+
+//            Map<String, Object> jsonResourceGet = new HashMap<>();
+//            jsonResourceGet.put("url", url.toString().replace("https//192.168.10.103:9001", "http//192.168.10.103:9002"));
+//            jsonResourceGet.put("fingerprint", md5);
+//            .replace("https://", "http://")
+//            .replace("9001", "9011")
+
+            builder.data(RequestsReply.success(Dock1WaylineTaskCreateDTO.builder()
+                    .file(WaylineTaskFileDTO.builder()
+                            .url(httpUrl)
+                            .fingerprint(md5)
+                            .build())
+                    .build()));
+
+        } catch (NullPointerException | IOException e) {
+            e.printStackTrace();
+            builder.data(RequestsReply.error(CommonErrorEnum.ILLEGAL_ARGUMENT));
+            messageSender.publish(topic, builder.build());
+            return;
+        }
+
+        // 下发参数打印
+        try {
+            String json = objectMapper.writeValueAsString(builder.build());
+            log.info("机场通过flighttask_resource_get获取航线文件的参数flightTask:{}", json);
+        } catch (Exception e) {
+            log.error("机场通过flighttask_resource_get获取航线文件的参数flightTask异常:{}", e.getMessage());
+        }
+
+        messageSender.publish(topic, builder.build());
+
+    }
+
+    /**
+     * 上传媒体文件的最高优先级
+     *
+     * @param jobId 作业ID
+     * @throws RuntimeException 如果作业ID不合法或设置优先级失败
+     */
+    @Override
+    public void uploadMediaHighestPriority(String jobId) {
+        Optional<WaylineJobDTO> jobOpt = getJobByJobId(jobId);
+        if (jobOpt.isEmpty()) {
+            throw new RuntimeException(CommonErrorEnum.ILLEGAL_ARGUMENT.getErrorMsg());
+        }
+        // 机场1:只允许对 Dock1 的任务设置媒体上传优先级。
+        if (!isDock1Device(jobOpt.get().getDockSn())) {
+            throw new RuntimeException("该任务不属于机场1,请使用对应的机场接口操作!");
+        }
+
+        String dockSn = jobOpt.get().getDockSn();
+        String key = RedisConst.MEDIA_HIGHEST_PRIORITY_PREFIX + dockSn;
+        if (RedisOpsUtils.checkExist(key) &&
+                jobId.equals(((MediaFileCountDTO) RedisOpsUtils.get(key)).getJobId())) {
+            return;
+        }
+
+        ServiceReply reply = messageSender.publishWithReply(TopicConst.THING_MODEL_PRE + TopicConst.PRODUCT + dockSn + TopicConst.SERVICES_SUF,
+                CommonTopicResponse.builder()
+                        .tid(UUID.randomUUID().toString())
+                        .bid(UUID.randomUUID().toString())
+                        .timestamp(System.currentTimeMillis())
+                        .method(MediaMethodEnum.UPLOAD_FLIGHT_TASK_MEDIA_PRIORITIZE.getMethod())
+                        .data(Map.of(MapKeyConst.FLIGHT_ID, jobId))
+                        .build());
+        if (ResponseResult.CODE_SUCCESS != reply.getResult()) {
+            String errorMsg = CommonErrorEnum.getMsgByCode(reply.getResult());
+            throw new RuntimeException("未能设置媒体作业上传优先级. Error Code: " + reply.getResult() + ", Error Msg: " + errorMsg);
+        }
+    }
+
+    /**
+     * 将WaylineJobDTO对象转换为WaylineJobEntity对象
+     *
+     * @param dto WaylineJobDTO对象
+     * @return WaylineJobEntity对象
+     */
+    private WaylineJobEntity dto2Entity(WaylineJobDTO dto) {
+        WaylineJobEntity.WaylineJobEntityBuilder builder = WaylineJobEntity.builder();
+        if (dto == null) {
+            return builder.build();
+        }
+        if (Objects.nonNull(dto.getBeginTime())) {
+            builder.beginTime(dto.getBeginTime().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
+        }
+        if (Objects.nonNull(dto.getEndTime())) {
+            builder.endTime(dto.getEndTime().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
+        }
+        if (Objects.nonNull(dto.getExecuteTime())) {
+            builder.executeTime(dto.getExecuteTime().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
+        }
+        if (Objects.nonNull(dto.getCompletedTime())) {
+            builder.completedTime(dto.getCompletedTime().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
+        }
+        return builder.status(dto.getStatus())
+                .mediaCount(dto.getMediaCount())
+                .name(dto.getJobName())
+                .errorCode(dto.getCode())
+                .jobId(dto.getJobId())
+                .fileId(dto.getFileId())
+                .dockSn(dto.getDockSn())
+                .taskType(dto.getTaskType())
+                .waylineType(dto.getWaylineType())
+                .username(dto.getUsername())
+                .rthAltitude(dto.getRthAltitude())
+                .outOfControlAction(dto.getOutOfControlAction())
+                .parentId(dto.getParentId())
+                .loopSum(dto.getLoopSum())
+                .build();
+    }
+
+    /**
+     * 将实体对象转换为数据传输对象
+     *
+     * @param entity WaylineJobEntity 实体对象
+     * @return WaylineJobDTO 数据传输对象
+     */
+    private WaylineJobDTO entity2Dto(WaylineJobEntity entity) {
+        if (entity == null) {
+            return null;
+        }
+        // 修改航线文件名称,从ard_route_info航线表中取;
+        String fileId = entity.getFileId();
+        String fileName = "";
+        try {
+            if (fileId != null) {
+                ArdRouteInfo ardRouteInfo = _ardRouteInfoService.selectArdRouteInfoById(fileId);
+                if (ardRouteInfo != null) {
+                    fileName = Optional.ofNullable(ardRouteInfo.getName()).orElse("");
+                }
+            }
+        } catch (Exception e) {
+            log.info("获取航线名称失败,fileId:{},异常原因:{}", fileId, e.getMessage());
+            fileName = "";
+        }
+        WaylineJobDTO.WaylineJobDTOBuilder builder = WaylineJobDTO.builder()
+                .jobId(entity.getJobId())
+                .jobName(entity.getName())
+                .fileId(entity.getFileId())
+//                .fileName(waylineFileService.getWaylineByWaylineId(entity.getFileId())
+//                        .orElse(WaylineFileDTO.builder().build()).getName())
+                .fileName(fileName)
+                .dockSn(entity.getDockSn())
+                .dockName(deviceService.getDeviceBySn(entity.getDockSn())
+                        .orElse(DeviceDTO.builder().build()).getNickname())
+                .username(entity.getUsername())
+                .status(WaylineJobStatusEnum.IN_PROGRESS.getVal() == entity.getStatus() &&
+                        RedisOpsUtils.checkExist(RedisConst.WAYLINE_JOB_PAUSED_PREFIX + entity.getJobId()) ?
+                        WaylineJobStatusEnum.PAUSED.getVal() : entity.getStatus())
+                .code(entity.getErrorCode())
+                .beginTime(LocalDateTime.ofInstant(Instant.ofEpochMilli(entity.getBeginTime()), ZoneId.systemDefault()))
+                .endTime(Objects.nonNull(entity.getEndTime()) ?
+                        LocalDateTime.ofInstant(Instant.ofEpochMilli(entity.getEndTime()), ZoneId.systemDefault()) : null)
+                .executeTime(Objects.nonNull(entity.getExecuteTime()) ?
+                        LocalDateTime.ofInstant(Instant.ofEpochMilli(entity.getExecuteTime()), ZoneId.systemDefault()) : null)
+                .completedTime(WaylineJobStatusEnum.find(entity.getStatus()).getEnd() ?
+                        LocalDateTime.ofInstant(Instant.ofEpochMilli(entity.getUpdateTime()), ZoneId.systemDefault()) : null)
+                .taskType(entity.getTaskType())
+                .waylineType(entity.getWaylineType())
+                .rthAltitude(entity.getRthAltitude())
+                .rthMode(entity.getRthMode())
+                .outOfControlAction(entity.getOutOfControlAction())
+                .mediaCount(entity.getMediaCount())
+                .loopSum(entity.getLoopSum());
+
+
+        if (Objects.nonNull(entity.getEndTime())) {
+            builder.endTime(LocalDateTime.ofInstant(Instant.ofEpochMilli(entity.getEndTime()), ZoneId.systemDefault()));
+        }
+        if (WaylineJobStatusEnum.IN_PROGRESS.getVal() == entity.getStatus() && RedisOpsUtils.getExpire(entity.getJobId()) > 0) {
+            EventsReceiver<WaylineTaskProgressReceiver> taskProgress = (EventsReceiver<WaylineTaskProgressReceiver>) RedisOpsUtils.get(RedisConst.WAYLINE_JOB_RUNNING_PREFIX + entity.getDockSn());
+            if (Objects.nonNull(taskProgress.getOutput()) && Objects.nonNull(taskProgress.getOutput().getProgress())) {
+                builder.progress(taskProgress.getOutput().getProgress().getPercent());
+            }
+        }
+
+        if (entity.getMediaCount() == 0) {
+            return builder.build();
+        }
+
+        // sync the number of media files
+        String key = RedisConst.MEDIA_HIGHEST_PRIORITY_PREFIX + entity.getDockSn();
+        String countKey = RedisConst.MEDIA_FILE_PREFIX + entity.getDockSn();
+        Object mediaFileCount = RedisOpsUtils.hashGet(countKey, entity.getJobId());
+        if (Objects.nonNull(mediaFileCount)) {
+            builder.uploadedCount(((MediaFileCountDTO) mediaFileCount).getUploadedCount())
+                    .uploading(RedisOpsUtils.checkExist(key) && entity.getJobId().equals(((MediaFileCountDTO) RedisOpsUtils.get(key)).getJobId()));
+            return builder.build();
+        }
+
+        int uploadedSize = fileService.getFilesByWorkspaceAndJobId(entity.getJobId()).size();
+        // All media for this job have been uploaded.
+        if (uploadedSize >= entity.getMediaCount()) {
+            return builder.uploadedCount(uploadedSize).build();
+        }
+        RedisOpsUtils.hashSet(countKey, entity.getJobId(),
+                MediaFileCountDTO.builder()
+                        .jobId(entity.getJobId())
+                        .mediaCount(entity.getMediaCount())
+                        .uploadedCount(uploadedSize).build());
+        return builder.build();
+    }
+}
diff --git a/ard-sample/src/main/java/com/ard/sample/manage/model/enums/DockModelEnum.java b/ard-sample/src/main/java/com/ard/sample/manage/model/enums/DockModelEnum.java
new file mode 100644
index 0000000..7b0a9fa
--- /dev/null
+++ b/ard-sample/src/main/java/com/ard/sample/manage/model/enums/DockModelEnum.java
@@ -0,0 +1,64 @@
+package com.ard.sample.manage.model.enums;
+
+import lombok.Getter;
+
+/**
+ * 【机场1】机场机型代际枚举(Dock1 适配共享层新增)。
+ *
+ * <p>判据为 device_model_key(三段 {@code domain-type-sub_type},落库逻辑见
+ * DeviceServiceImpl.bindDevice2Entity)中的 type 段:
+ * 机场设备(domain=3)Dock1=1 / Dock2=2;飞行器(domain=0)M30 系列=67 / M3D 系列=91。
+ *
+ * @author ard
+ * @date 2026/10/16
+ */
+@Getter
+public enum DockModelEnum {
+
+    /** 机场1(DJI Dock 1):飞行器为 M30/M30T(type=67,sub_type 0=M30 / 1=M30T) */
+    DOCK1(1, 67),
+
+    /** 机场2(DJI Dock 2):飞行器为 M3D/M3TD(type=91,sub_type 0=M3D / 1=M3TD) */
+    DOCK2(2, 91);
+
+    /** 机场设备(domain=3)的 device type */
+    private final int dockType;
+
+    /** 飞行器(domain=0)的 device type */
+    private final int droneType;
+
+    DockModelEnum(int dockType, int droneType) {
+        this.dockType = dockType;
+        this.droneType = droneType;
+    }
+
+    /**
+     * 按机场设备的 device type 解析机型。
+     *
+     * @param type 机场设备 type(dock1=1 / dock2=2)
+     * @return 对应机型;未知返回 null
+     */
+    public static DockModelEnum fromDockType(int type) {
+        for (DockModelEnum model : values()) {
+            if (model.dockType == type) {
+                return model;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 按飞行器的 device type 解析机型。
+     *
+     * @param type 飞行器 type(M30 系列=67 / M3D 系列=91)
+     * @return 对应机型;未知返回 null
+     */
+    public static DockModelEnum fromDroneType(int type) {
+        for (DockModelEnum model : values()) {
+            if (model.droneType == type) {
+                return model;
+            }
+        }
+        return null;
+    }
+}
diff --git a/ard-sample/src/main/java/com/ard/sample/manage/service/impl/DeviceServiceImpl.java b/ard-sample/src/main/java/com/ard/sample/manage/service/impl/DeviceServiceImpl.java
index b768415..a607a8b 100644
--- a/ard-sample/src/main/java/com/ard/sample/manage/service/impl/DeviceServiceImpl.java
+++ b/ard-sample/src/main/java/com/ard/sample/manage/service/impl/DeviceServiceImpl.java
@@ -44,7 +44,7 @@
 import lombok.extern.slf4j.Slf4j;
 import org.apache.poi.ss.formula.functions.T;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.dao.DuplicateKeyException;
 import org.springframework.integration.annotation.ServiceActivator;
 import org.springframework.integration.mqtt.support.MqttHeaders;
 import org.springframework.messaging.Message;
@@ -116,9 +116,9 @@
     @Resource
     private AuthorizeService authorizeService;
 
+    // 机场1:OSD 按机型分发(原为 @Qualifier("gatewayOSDServiceImpl") ITSAService tsaService,直连 dock2 链头)
     @Autowired
-    @Qualifier("gatewayOSDServiceImpl")
-    private ITSAService tsaService;
+    private DockOsdDispatcher dockOsdDispatcher;
 
     @Resource
     private ISysDeptService sysDeptService;
@@ -359,8 +359,13 @@
             return;
         }
 
-        DeviceDTO subDevice =
-                getDevicesByParams(DeviceQueryParam.builder().deviceSn(gateway.getChildDeviceSn()).build()).get(0);
+        List<DeviceDTO> subDevices =
+                getDevicesByParams(DeviceQueryParam.builder().deviceSn(gateway.getChildDeviceSn()).build());
+        if (CollectionUtils.isEmpty(subDevices)) {
+            log.warn("网关 {} 的子设备 {} 不存在,跳过拓扑拼接", gateway.getDeviceSn(), gateway.getChildDeviceSn());
+            return;
+        }
+        DeviceDTO subDevice = subDevices.get(0);
         subDevice.setStatus(RedisOpsUtils.checkExist(RedisConst.DEVICE_ONLINE_PREFIX + subDevice.getDeviceSn()));
         gateway.setChildren(subDevice);
 
@@ -410,8 +415,13 @@
         if (!StringUtils.hasText(gateway.getChildDeviceSn())) {
             return;
         }
-        DeviceDTO subDevice =
-                getDevicesByParams(DeviceQueryParam.builder().deviceSn(gateway.getChildDeviceSn()).build()).get(0);
+        List<DeviceDTO> subDevices =
+                getDevicesByParams(DeviceQueryParam.builder().deviceSn(gateway.getChildDeviceSn()).build());
+        if (CollectionUtils.isEmpty(subDevices)) {
+            log.warn("网关 {} 的子设备 {} 不存在,跳过拓扑拼接", gateway.getDeviceSn(), gateway.getChildDeviceSn());
+            return;
+        }
+        DeviceDTO subDevice = subDevices.get(0);
         subDevice.setStatus(RedisOpsUtils.checkExist(RedisConst.DEVICE_ONLINE_PREFIX + subDevice.getDeviceSn()));
         gateway.setChildren(subDevice);
     }
@@ -437,7 +447,7 @@
                                 })
                                 .eq(param.getBoundStatus() != null, DeviceEntity::getBoundStatus,
                                         param.getBoundStatus())
-                                .like(param.getNickname()!=null, DeviceEntity::getNickname, "%" + param.getNickname() + "%")
+                                .like(StringUtils.hasText(param.getNickname()), DeviceEntity::getNickname, "%" + param.getNickname() + "%")
                                 .orderBy(param.isOrderBy(),
                                         param.isAsc(), DeviceEntity::getId))
                 .stream()
@@ -662,7 +672,8 @@
                     .getValueAndUserType(UserTypeEnum.WEB.getVal());
 
 
-            tsaService.handleOSD(receiver, device, webSessions, wsMessage);
+            // 机场1:OSD 按机型分发(dock1=67 → dock1 链;其余透传 dock2 原链)
+            dockOsdDispatcher.dispatch(receiver, device, webSessions, wsMessage);
 
         } catch (NullPointerException e) {
             log.error("处理OSD消息NPE,headers={}, payload={}",
@@ -746,7 +757,22 @@
         }
         // 调用验证方法
 
-        return mapper.insert(entity) > 0 ? Optional.of(entity) : Optional.empty();
+        try {
+            return mapper.insert(entity) > 0 ? Optional.of(entity) : Optional.empty();
+        } catch (DuplicateKeyException e) {
+            // 机场首次上线时 MQTT QoS1 可能重复投递 update_topo(或多线程并发),
+            // 两个线程同时查到“设备不存在”后会并发 insert,后插入者触发唯一键冲突。
+            // 此时数据已被另一线程写入,退化为更新即可,避免整条状态消息处理失败。
+            DeviceEntity exist = mapper.selectOne(
+                    new LambdaQueryWrapper<DeviceEntity>()
+                            .eq(DeviceEntity::getDeviceSn, entity.getDeviceSn()));
+            if (exist == null) {
+                throw e;
+            }
+            entity.setId(exist.getId());
+            mapper.updateById(entity);
+            return Optional.of(exist);
+        }
     }
 
     /**
diff --git a/ard-sample/src/main/java/com/ard/sample/manage/service/impl/DockModelResolver.java b/ard-sample/src/main/java/com/ard/sample/manage/service/impl/DockModelResolver.java
new file mode 100644
index 0000000..f40e3a1
--- /dev/null
+++ b/ard-sample/src/main/java/com/ard/sample/manage/service/impl/DockModelResolver.java
@@ -0,0 +1,41 @@
+package com.ard.sample.manage.service.impl;
+
+import com.ard.sample.manage.model.dto.DeviceDTO;
+import com.ard.sample.manage.model.enums.DeviceDomainEnum;
+import com.ard.sample.manage.model.enums.DockModelEnum;
+
+/**
+ * 【机场1】机型解析器(Dock1 适配共享层新增)。
+ *
+ * <p>从 DeviceDTO 的 domain/type(即 device_model_key 三段 domain-type-sub_type 的落库结果)
+ * 解析设备所属的机场代际:机场设备按 type 判 1/2,飞行器按 type 判 67/91。
+ * 无法识别(遥控器、未绑定设备、type 为空等)返回 null——调用方应兜底走 dock2 原链,
+ * 保证 dock2 与改造前行为完全一致。
+ *
+ * @author ard
+ * @date 2026/10/16
+ */
+public final class DockModelResolver {
+
+    private DockModelResolver() {
+    }
+
+    /**
+     * 解析设备所属机型。
+     *
+     * @param device 在线设备(DeviceDTO 的 domain/type/subType 由绑定/上线时 device_model_key 三段解析落库)
+     * @return {@link DockModelEnum#DOCK1} / {@link DockModelEnum#DOCK2};无法识别返回 null
+     */
+    public static DockModelEnum resolve(DeviceDTO device) {
+        if (device == null || device.getDomain() == null || device.getType() == null) {
+            return null;
+        }
+        if (DeviceDomainEnum.DOCK.getVal() == device.getDomain()) {
+            return DockModelEnum.fromDockType(device.getType());
+        }
+        if (DeviceDomainEnum.SUB_DEVICE.getVal() == device.getDomain()) {
+            return DockModelEnum.fromDroneType(device.getType());
+        }
+        return null;
+    }
+}
diff --git a/ard-sample/src/main/java/com/ard/sample/manage/service/impl/DockOnlineServiceImpl.java b/ard-sample/src/main/java/com/ard/sample/manage/service/impl/DockOnlineServiceImpl.java
index 8e56522..58c93c6 100644
--- a/ard-sample/src/main/java/com/ard/sample/manage/service/impl/DockOnlineServiceImpl.java
+++ b/ard-sample/src/main/java/com/ard/sample/manage/service/impl/DockOnlineServiceImpl.java
@@ -20,6 +20,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.TimeUnit;
 
 @Slf4j
@@ -51,25 +52,92 @@
             //String key = "online:" + device.getDeviceSn();
             //System.out.println("在线状态:"+RedisOpsUtils.checkExist(key));
             // 更新机场设备状态:"3"-"设备已离线",
-            if(!_deviceService.checkDeviceOnline(device.getDeviceSn())){
+            if (!_deviceService.checkDeviceOnline(device.getDeviceSn())) {
                 _deviceService.updateWorkType(device.getDeviceSn(), 3L);
                 RedisOpsUtils.set(RedisConst.OSD_STATUS_PREFIX + device.getDeviceSn(), 3);
                 RedisOpsUtils.del(RedisConst.WEBSOCKET_VIDEO_ADDRESS_PREFIX + device.getDeviceSn());
                 RedisOpsUtils.del(RedisConst.COVER_STATE_PREFIX + device.getDeviceSn());
-            }
-            else {
-                StartLiveStreaming(device.getDeviceSn());
-            }
-            if(!_deviceService.checkDeviceOnline(device.getChildDeviceSn())){
-                _deviceService.updateWorkType(device.getChildDeviceSn(), 6L);
-                RedisOpsUtils.del(RedisConst.WEBSOCKET_VIDEO_ADDRESS_PREFIX + device.getChildDeviceSn());
-                RedisOpsUtils.del(RedisConst.COVER_STATE_PREFIX + device.getChildDeviceSn());
-            }
-            else{
-                StartLiveStreaming(device.getChildDeviceSn());
+                clearLiveRetry(device.getDeviceSn());
+            } else {
+                tryStartLiveStreaming(device.getDeviceSn());
             }
 
+            // 机场1:未绑定飞行器时 childDeviceSn 为空,跳过,避免无效的在线判断和起流
+            String childDeviceSn = device.getChildDeviceSn();
+            if (childDeviceSn == null || childDeviceSn.isEmpty()) {
+                continue;
+            }
+            if (!_deviceService.checkDeviceOnline(childDeviceSn)) {
+                _deviceService.updateWorkType(childDeviceSn, 6L);
+                RedisOpsUtils.del(RedisConst.WEBSOCKET_VIDEO_ADDRESS_PREFIX + childDeviceSn);
+                RedisOpsUtils.del(RedisConst.COVER_STATE_PREFIX + childDeviceSn);
+                clearLiveRetry(childDeviceSn);
+            } else if (isDronePoweredOff(childDeviceSn)) {
+                // 飞行器舱内关机(6)或开机中(7)时相机尚未就绪,起流必然失败(如 513006),直接跳过
+                clearLiveRetry(childDeviceSn);
+            } else {
+                tryStartLiveStreaming(childDeviceSn);
+            }
         }
+    }
+
+    /**
+     * 按退避策略尝试开启直播。
+     * 原实现每 2 秒对未就绪设备执行一次“停-开”,开机阶段会刷出大量 513006/613011 日志;
+     * 这里失败后按 2s、4s、8s、16s、30s 递增间隔重试。
+     */
+    private void tryStartLiveStreaming(String sn) {
+        if (!canRetryLive(sn)) {
+            return;
+        }
+        markLiveRetry(sn, StartLiveStreaming(sn));
+    }
+
+    /**
+     * 飞行器是否处于关机或开机中状态。
+     * 6-舱内关机(机场 OSD 写入),7-开机中(下发 drone_open 时写入)。
+     */
+    private boolean isDronePoweredOff(String childDeviceSn) {
+        Object status = RedisOpsUtils.get(RedisConst.OSD_STATUS_PREFIX + childDeviceSn);
+        if (status == null) {
+            return false;
+        }
+        String value = String.valueOf(status);
+        return "6".equals(value) || "7".equals(value);
+    }
+
+    /**
+     * 连续失败次数与下次允许重试的时间戳。
+     */
+    private static class LiveRetryState {
+        private int failures;
+        private long nextRetryTime;
+    }
+
+    private static final long LIVE_RETRY_BASE_DELAY_MS = 2000L;
+    private static final long LIVE_RETRY_MAX_DELAY_MS = 30000L;
+
+    private final Map<String, LiveRetryState> liveRetryStates = new ConcurrentHashMap<>();
+
+    private boolean canRetryLive(String sn) {
+        LiveRetryState state = liveRetryStates.get(sn);
+        return state == null || System.currentTimeMillis() >= state.nextRetryTime;
+    }
+
+    private void markLiveRetry(String sn, boolean success) {
+        if (success) {
+            liveRetryStates.remove(sn);
+            return;
+        }
+        LiveRetryState state = liveRetryStates.computeIfAbsent(sn, k -> new LiveRetryState());
+        state.failures++;
+        long delay = Math.min(LIVE_RETRY_BASE_DELAY_MS << Math.min(state.failures - 1, 4), LIVE_RETRY_MAX_DELAY_MS);
+        state.nextRetryTime = System.currentTimeMillis() + delay;
+        log.info("SN: {}-直播启动失败(第 {} 次),{} 秒后重试", sn, state.failures, delay / 1000);
+    }
+
+    private void clearLiveRetry(String sn) {
+        liveRetryStates.remove(sn);
     }
 
     /**
@@ -126,7 +194,12 @@
         return jsonString;
     }
 
-    public void StartLiveStreaming(String sn) {
+    /**
+     * 开启直播(后台定时任务调用)。
+     *
+     * @return true-已存在可用地址或起流成功;false-起流失败
+     */
+    public boolean StartLiveStreaming(String sn) {
         //调用开始直播
         ResponseResult rlive = null;
         String resultLive = "";
@@ -136,12 +209,13 @@
             // 判断param是否为空,不为空则返回正确的直播参数,为null则未获取直播参数,不为空则继续操作
             if (param == null) {
                 log.info("SN: {}-未获取到直播参数param为null,无法开启直播!", sn);
-                return;
+                return false;
             }
             // 如果存在直播则读取直播地址返回
             if (RedisOpsUtils.checkExist(RedisConst.WEBSOCKET_VIDEO_ADDRESS_PREFIX + sn)) {
                 resultLive = String.valueOf(RedisOpsUtils.get(RedisConst.WEBSOCKET_VIDEO_ADDRESS_PREFIX + sn));
 //                log.info("SN: {}-redis中存在相机的播放地址信息,直接返回地址!", sn);
+                return true;
             } else {
 //                log.info("SN: {}-redis中未找到相机的播放地址信息!停止直播后再开启直播", sn);
                 _liveStreamHandler.StopLiveStreaming(sn);
@@ -159,11 +233,17 @@
 //                    if (isWeb) {
 //                        log.info("SN: {}-更新webrtc地址成功!", sn);
 //                    }
+                    // 定时任务是后台自动起流,前端并没有发订阅,必须主动把地址推给已订阅该设备的页面;
+                    // 否则飞行器起飞后相机就绪、后台起流成功,页面却收不到地址,只能刷新后才出画面
+                    _dockWebSocketHandler.sendMessageToSubscribers(sn, MessageType.LIVESTREAM.getType(), resultLive, "");
+                    return true;
                 }
+                log.info("SN: {}-开启直播失败!返回值:{}", sn, rlive);
             }
         } catch (Exception e) {
             log.info("订时任务推流-SN: {}-开启直播失败!原因:{}", sn, e.getMessage());
             e.printStackTrace();
         }
+        return false;
     }
 }
diff --git a/ard-sample/src/main/java/com/ard/sample/manage/service/impl/DockOsdDispatcher.java b/ard-sample/src/main/java/com/ard/sample/manage/service/impl/DockOsdDispatcher.java
new file mode 100644
index 0000000..c8f77a5
--- /dev/null
+++ b/ard-sample/src/main/java/com/ard/sample/manage/service/impl/DockOsdDispatcher.java
@@ -0,0 +1,60 @@
+package com.ard.sample.manage.service.impl;
+
+import com.ard.sample.component.mqtt.model.CommonTopicReceiver;
+import com.ard.sample.component.websocket.config.ConcurrentWebSocketSession;
+import com.ard.sample.component.websocket.model.CustomWebSocketMessage;
+import com.ard.sample.manage.model.dto.DeviceDTO;
+import com.ard.sample.manage.model.dto.TelemetryDTO;
+import com.ard.sample.manage.model.enums.DockModelEnum;
+import com.ard.sample.manage.service.ITSAService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.stereotype.Service;
+
+import java.util.Collection;
+
+/**
+ * 【机场1】OSD 入站分发器(Dock1 适配共享层新增,唯一侵入共享层的分流点)。
+ *
+ * <p>DeviceServiceImpl.handleOSD 收到 INBOUND_OSD 消息后委托本类,按设备机型分发到两条 OSD 处理链:
+ * <ul>
+ *   <li>DOCK1(67) → dock1 链:{@code dock1GatewayOSDServiceImpl → dock1DeviceOSDServiceImpl → dock1DockOSDServiceImpl}</li>
+ *   <li>其余(DOCK2(91) / 遥控器等无法识别来源)→ dock2 原链:
+ *       {@code gatewayOSDServiceImpl → deviceOSDServiceImpl → dockOSDServiceImpl}(行为与改造前完全一致)</li>
+ * </ul>
+ * dock1 链为普通 @Service(不带 @ServiceActivator),INBOUND_OSD 通道消费者仍只有 DeviceServiceImpl 一个。
+ *
+ * @author ard
+ * @date 2026/10/16
+ */
+@Service
+public class DockOsdDispatcher {
+
+    @Autowired
+    @Qualifier("gatewayOSDServiceImpl")
+    private ITSAService dock2TsaService;
+
+    @Autowired
+    @Qualifier("dock1GatewayOSDServiceImpl")
+    private ITSAService dock1TsaService;
+
+    /**
+     * 分发 OSD 消息到对应机型的处理链。
+     *
+     * @param receiver    OSD 报文
+     * @param device      消息来源设备(含 domain/type/subType)
+     * @param webSessions 目标 WebSocket 会话
+     * @param wsMessage   待推送的 WebSocket 消息
+     */
+    public void dispatch(CommonTopicReceiver receiver, DeviceDTO device,
+                         Collection<ConcurrentWebSocketSession> webSessions,
+                         CustomWebSocketMessage<TelemetryDTO> wsMessage) {
+        DockModelEnum model = DockModelResolver.resolve(device);
+        if (DockModelEnum.DOCK1 == model) {
+            dock1TsaService.handleOSD(receiver, device, webSessions, wsMessage);
+            return;
+        }
+        // dock2 及一切无法识别的来源:原链透传,保证 dock2 行为零变化
+        dock2TsaService.handleOSD(receiver, device, webSessions, wsMessage);
+    }
+}
diff --git a/ard-sample/src/main/java/com/ard/sample/media/service/impl/FileServiceImpl.java b/ard-sample/src/main/java/com/ard/sample/media/service/impl/FileServiceImpl.java
index ebf91a4..8982a89 100644
--- a/ard-sample/src/main/java/com/ard/sample/media/service/impl/FileServiceImpl.java
+++ b/ard-sample/src/main/java/com/ard/sample/media/service/impl/FileServiceImpl.java
@@ -114,17 +114,21 @@
      */
     @Override
     public PaginationData<MediaFileDTO> getJobFiles(long page, long pageSize,String jobId, String isType) {
-        Page<MediaFileEntity> pageData = mapper.selectPage(
-                new Page<MediaFileEntity>(page, pageSize),
-                new LambdaQueryWrapper<MediaFileEntity>()
-                        .eq(Objects.nonNull(jobId), MediaFileEntity::getJobId, jobId)
-//                        .orderByDesc(MediaFileEntity::getId));
-                        // isType=1 时匹配 V.jpeg
-                        .like("1".equals(isType), MediaFileEntity::getFileName, "V.jpeg")
-                        // isType=0 时匹配 V.mp4
-                        .like("0".equals(isType), MediaFileEntity::getFileName, "V.mp4")
-                        // 排序修改:按start_time正序(升序)排列
-                        .orderByAsc(MediaFileEntity::getStartTime)); // 关键改动1:替换排序方式
+        LambdaQueryWrapper<MediaFileEntity> queryWrapper = new LambdaQueryWrapper<MediaFileEntity>()
+                .eq(Objects.nonNull(jobId), MediaFileEntity::getJobId, jobId);
+        // 按文件扩展名区分图片/视频,不再写死镜头前缀:
+        // Dock2 可见光文件名为 _V.jpeg/_V.mp4;机场1(M30T)三光相机为 _W/_Z/_T.jpeg(广角/变焦/红外)及 _W.mp4。
+        // 原实现写死 V.jpeg/V.mp4,导致机场1的媒体文件在计划库中查询不到。
+        if ("1".equals(isType)) {
+            queryWrapper.and(w -> w.like(MediaFileEntity::getFileName, ".jpeg")
+                    .or().like(MediaFileEntity::getFileName, ".jpg"));
+        } else if ("0".equals(isType)) {
+            queryWrapper.like(MediaFileEntity::getFileName, ".mp4");
+        }
+        // 排序:按 start_time 正序(升序)排列
+        queryWrapper.orderByAsc(MediaFileEntity::getStartTime);
+
+        Page<MediaFileEntity> pageData = mapper.selectPage(new Page<MediaFileEntity>(page, pageSize), queryWrapper);
 
         List<MediaFileDTO> records = pageData.getRecords()
                 .stream()
diff --git a/ard-sample/src/main/java/com/ard/sample/storage/service/impl/StorageServiceImpl.java b/ard-sample/src/main/java/com/ard/sample/storage/service/impl/StorageServiceImpl.java
index 6842b2c..97d9d06 100644
--- a/ard-sample/src/main/java/com/ard/sample/storage/service/impl/StorageServiceImpl.java
+++ b/ard-sample/src/main/java/com/ard/sample/storage/service/impl/StorageServiceImpl.java
@@ -2,6 +2,7 @@
 
 import com.ard.common.config.OssConfiguration;
 import com.ard.oss.model.CredentialsDTO;
+import com.ard.sample.common.error.CommonErrorEnum;
 import com.ard.sample.component.mqtt.model.*;
 import com.ard.sample.component.mqtt.service.IMessageSenderService;
 import com.ard.oss.model.StsCredentialsDTO;
@@ -77,10 +78,20 @@
     @ServiceActivator(inputChannel = ChannelName.INBOUND_REQUESTS_STORAGE_CONFIG_GET, outputChannel = ChannelName.OUTBOUND)
     public void replyConfigGet(CommonTopicReceiver receiver, MessageHeaders headers) {
         // 20251219 这里是获取上传临时凭证的接口,稍候分析下是否满足条件,如果可以的话就直接回复临时凭证即可
+        RequestsReply reply;
+        try {
+            reply = RequestsReply.success(this.getSTSCredentials());
+        } catch (Exception e) {
+            // 机场1 上送 storage_config_get 时,若 MinIO STS 不可用,原实现会直接抛异常且不回复,
+            // 机场会每 5 秒重试一次并持续刷 error.log。这里回一个明确的失败应答,避免消息处理中断。
+            // 底层 MinIOServiceImpl 已打印完整堆栈,此处仅记录关键信息。
+            log.error("获取媒体上传临时凭证失败,gateway={},原因:{}", receiver.getGateway(), e.getMessage());
+            reply = RequestsReply.error(CommonErrorEnum.SYSTEM_ERROR);
+        }
         CommonTopicResponse<RequestsReply> response = CommonTopicResponse.<RequestsReply>builder()
                 .tid(receiver.getTid())
                 .bid(receiver.getBid())
-                .data(RequestsReply.success(this.getSTSCredentials()))
+                .data(reply)
                 .timestamp(System.currentTimeMillis())
                 .method(RequestsMethodEnum.STORAGE_CONFIG_GET.getMethod())
                 .build();
diff --git a/sql/dock1_dictionary.sql b/sql/dock1_dictionary.sql
new file mode 100644
index 0000000..7b9b1e0
--- /dev/null
+++ b/sql/dock1_dictionary.sql
@@ -0,0 +1,58 @@
+-- =====================================================================
+-- 机场1(DJI Dock 1 / M30)设备型号字典数据
+-- 表:manage_device_dictionary(DJI 上云线设备型号字典)
+--
+-- 背景:经核实 2026-10-16,2025-11-06 数据备份
+--       (sql/backup/20251106091248.nb3)中以下数据行已随
+--       DJI 官方 Demo 种子数据存在('DJI Dock'=id18 / 'Matrice 30'=id2 /
+--       'Matrice 30T'=id3 等),开发库无需执行即可直接联调。
+--       本脚本为幂等兜底:跨环境部署(dev / prod / daqing)若缺失则补齐,
+--       已存在则跳过,可重复执行。
+--
+-- 消费点(皆为共享层,由 dock1/dock2 两条链共用):
+--   DeviceServiceImpl#deviceGatewayConvertToDeviceEntity  :793  机场上线  domain=3
+--   DeviceServiceImpl#subDeviceConvertToDeviceEntity      :827  飞行器上线 domain=0
+--   DeviceServiceImpl#bindDevice2Entity                   :1373 设备绑定
+--   DevicePayloadServiceImpl                              :205  负载上线  domain=1
+--   CapacityCameraServiceImpl                             :68   相机能力上报 domain=1
+--   FileServiceImpl                                       :209  媒体上传  domain=1
+--
+-- 官方文档:https://developer.dji.com/doc/cloud-api-tutorial/cn/api-reference/
+--           dock-to-cloud/mqtt/dock/dock1/properties.html
+--           (device_model_key 三段式 domain-type-sub_type 即下列前 3 列)
+-- =====================================================================
+
+-- 机场1(Dock1):domain=3(机场), device_type=1, sub_type=0
+INSERT INTO `manage_device_dictionary` (`domain`, `device_type`, `sub_type`, `device_name`, `device_desc`)
+SELECT 3, 1, 0, 'DJI Dock', 'DJI Airport' FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM `manage_device_dictionary` WHERE `domain` = 3 AND `device_type` = 1 AND `sub_type` = 0);
+
+-- 飞行器 M30:domain=0(飞行器), device_type=67, sub_type=0
+INSERT INTO `manage_device_dictionary` (`domain`, `device_type`, `sub_type`, `device_name`, `device_desc`)
+SELECT 0, 67, 0, 'Matrice 30', NULL FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM `manage_device_dictionary` WHERE `domain` = 0 AND `device_type` = 67 AND `sub_type` = 0);
+
+-- 飞行器 M30T:domain=0, device_type=67, sub_type=1
+INSERT INTO `manage_device_dictionary` (`domain`, `device_type`, `sub_type`, `device_name`, `device_desc`)
+SELECT 0, 67, 1, 'Matrice 30T', NULL FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM `manage_device_dictionary` WHERE `domain` = 0 AND `device_type` = 67 AND `sub_type` = 1);
+
+-- M30 相机:domain=1(负载), device_type=52, sub_type=0
+INSERT INTO `manage_device_dictionary` (`domain`, `device_type`, `sub_type`, `device_name`, `device_desc`)
+SELECT 1, 52, 0, 'M30 Camera', NULL FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM `manage_device_dictionary` WHERE `domain` = 1 AND `device_type` = 52 AND `sub_type` = 0);
+
+-- M30T 相机:domain=1, device_type=53, sub_type=0
+INSERT INTO `manage_device_dictionary` (`domain`, `device_type`, `sub_type`, `device_name`, `device_desc`)
+SELECT 1, 53, 0, 'M30T Camera', NULL FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM `manage_device_dictionary` WHERE `domain` = 1 AND `device_type` = 53 AND `sub_type` = 0);
+
+-- 机场相机:domain=1, device_type=165, sub_type=0
+INSERT INTO `manage_device_dictionary` (`domain`, `device_type`, `sub_type`, `device_name`, `device_desc`)
+SELECT 1, 165, 0, 'DJI Dock Camera', NULL FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM `manage_device_dictionary` WHERE `domain` = 1 AND `device_type` = 165 AND `sub_type` = 0);
+
+-- M30 遥控器:domain=2(遥控器), device_type=119, sub_type=0
+INSERT INTO `manage_device_dictionary` (`domain`, `device_type`, `sub_type`, `device_name`, `device_desc`)
+SELECT 2, 119, 0, 'Matrice 30 Smart Controller', 'Remote control for M30' FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM `manage_device_dictionary` WHERE `domain` = 2 AND `device_type` = 119 AND `sub_type` = 0);
diff --git a/sql/dock1_sys_dict.sql b/sql/dock1_sys_dict.sql
new file mode 100644
index 0000000..bf452d4
--- /dev/null
+++ b/sql/dock1_sys_dict.sql
@@ -0,0 +1,34 @@
+-- =====================================================================
+-- 机场1(DJI Dock 1 / M30、M30T)前端航线机型字典兜底
+-- 表:sys_dict_type / sys_dict_data
+--
+-- 背景:
+--   uav-project3 的航线新建页(views/airway/components/createAirway.vue)
+--   与计划新建页(views/plan/components/addPlan.vue)都通过
+--   useDict('dji_m30_series') 读取 M30 系列机型选项。
+--   若部署环境的 sys_dict_data 中缺少 0-67-1(Matrice 30T),
+--   则无法在航线库中新建 M30T 航线,计划页也会显示“暂无类型”。
+--
+-- 说明:
+--   本脚本仅做幂等兜底:已存在同 dict_type/dict_value 时跳过,可重复执行。
+--   M30(0-67-0)与 M30T(0-67-1)的 device_type 均为 67,
+--   子类型 sub_type 分别为 0 / 1(见 sql/dock1_dictionary.sql)。
+-- =====================================================================
+
+-- 字典类型:dji_m30_series
+INSERT INTO `sys_dict_type` (`dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `remark`)
+SELECT '经纬 M30 系列', 'dji_m30_series', '0', 'admin', sysdate(), '机场1(Dock1)航线机型'
+FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM `sys_dict_type` WHERE `dict_type` = 'dji_m30_series');
+
+-- 机型:M30 -> 0-67-0
+INSERT INTO `sys_dict_data` (`dict_sort`, `dict_label`, `dict_value`, `dict_type`, `is_default`, `status`, `create_by`, `create_time`, `remark`)
+SELECT 1, 'Matrice 30', '0-67-0', 'dji_m30_series', 'N', '0', 'admin', sysdate(), '机场1 飞行器 M30'
+FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM `sys_dict_data` WHERE `dict_type` = 'dji_m30_series' AND `dict_value` = '0-67-0');
+
+-- 机型:M30T -> 0-67-1
+INSERT INTO `sys_dict_data` (`dict_sort`, `dict_label`, `dict_value`, `dict_type`, `is_default`, `status`, `create_by`, `create_time`, `remark`)
+SELECT 2, 'Matrice 30T', '0-67-1', 'dji_m30_series', 'N', '0', 'admin', sysdate(), '机场1 飞行器 M30T'
+FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM `sys_dict_data` WHERE `dict_type` = 'dji_m30_series' AND `dict_value` = '0-67-1');

--
Gitblit v1.9.3