From 17835b967b48f2676cce3bbed081c6580602ee1c Mon Sep 17 00:00:00 2001
From: sean.zhou <sean.zhou@dji.com>
Date: Mon, 21 Mar 2022 22:17:00 +0800
Subject: [PATCH] 1.0.0 initial

---
 src/main/java/com/dji/sample/manage/model/param/DeviceQueryParam.java                      |   32 
 src/main/java/com/dji/sample/storage/service/impl/MinIOStorageServiceImpl.java             |   34 
 src/main/java/com/dji/sample/manage/model/dto/TelemetryDeviceDTO.java                      |   32 
 src/main/java/com/dji/sample/component/websocket/model/CustomWebSocketMessage.java         |   30 
 src/main/java/com/dji/sample/manage/model/receiver/CapacityCameraReceiver.java             |   28 
 src/main/java/com/dji/sample/manage/dao/IDeviceDictionaryMapper.java                       |   13 
 src/main/java/com/dji/sample/component/websocket/config/WebSocketDefaultHandler.java       |   60 
 src/main/java/com/dji/sample/media/model/MediaFileDTO.java                                 |   34 
 src/main/java/com/dji/sample/common/model/PaginationData.java                              |   27 
 src/main/java/com/dji/sample/component/websocket/config/ConcurrentWebSocketSession.java    |   25 
 src/main/java/com/dji/sample/map/model/dto/ElementCreateDTO.java                           |   18 
 src/main/java/com/dji/sample/map/model/dto/WebSocketElementDelDTO.java                     |   25 
 src/main/java/com/dji/sample/manage/model/receiver/StatusGatewayReceiver.java              |   43 
 src/main/java/com/dji/sample/manage/service/impl/WorkspaceServiceImpl.java                 |   52 
 src/main/java/com/dji/sample/manage/model/entity/CameraVideoEntity.java                    |   37 
 src/main/java/com/dji/sample/component/GlobalExceptionHandler.java                         |   33 
 src/main/java/com/dji/sample/map/model/dto/ElementUpdateDTO.java                           |   17 
 src/main/java/com/dji/sample/manage/model/dto/LiveDTO.java                                 |   20 
 src/main/java/com/dji/sample/map/model/dto/ElementType.java                                |   43 
 src/main/java/com/dji/sample/manage/handler/StateLiveCapacityHandler.java                  |   39 
 src/main/java/com/dji/sample/manage/controller/LoginController.java                        |   47 
 src/main/java/com/dji/sample/component/websocket/service/impl/SendMessageServiceImpl.java  |   72 
 src/main/java/com/dji/sample/map/model/dto/GroupElementDTO.java                            |   38 
 src/main/java/com/dji/sample/manage/dao/IDeviceMapper.java                                 |   14 
 src/main/java/com/dji/sample/media/model/CredentialsDTO.java                               |   40 
 src/main/java/com/dji/sample/manage/controller/DeviceController.java                       |  138 
 src/main/java/com/dji/sample/manage/model/dto/CapacityDeviceDTO.java                       |   26 
 src/main/java/com/dji/sample/component/mqtt/config/MqttMessageChannel.java                 |   90 
 src/main/java/com/dji/sample/media/service/impl/FileServiceImpl.java                       |  112 
 src/main/java/com/dji/sample/manage/model/enums/LiveUrlTypeEnum.java                       |   41 
 src/main/java/com/dji/sample/manage/service/ITopologyService.java                          |   20 
 src/main/java/com/dji/sample/common/error/CommonErrorEnum.java                             |   40 
 src/main/java/com/dji/sample/manage/model/entity/DeviceDictionaryEntity.java               |   45 
 src/main/java/com/dji/sample/manage/service/IDevicePayloadService.java                     |   48 
 src/main/java/com/dji/sample/manage/controller/LiveStreamController.java                   |  113 
 src/main/java/com/dji/sample/manage/model/dto/LiveUrlAgoraDTO.java                         |   20 
 pom.xml                                                                                    |  148 
 src/main/java/com/dji/sample/component/ApplicationBootInitial.java                         |   40 
 src/main/java/com/dji/sample/common/util/JwtUtil.java                                      |  103 
 src/main/java/com/dji/sample/manage/model/receiver/BatteryStateReceiver.java               |   34 
 src/main/java/com/dji/sample/component/oss/service/impl/AliyunOssServiceImpl.java          |   68 
 src/main/java/com/dji/sample/manage/model/receiver/PositionStateReceiver.java              |   23 
 src/main/java/com/dji/sample/component/oss/service/impl/MinIOServiceImpl.java              |   66 
 src/main/java/com/dji/sample/component/mqtt/service/IMqttMessageGateway.java               |   33 
 src/main/java/com/dji/sample/manage/dao/IDevicePayloadMapper.java                          |   12 
 src/main/java/com/dji/sample/map/model/enums/ElementTypeEnum.java                          |   69 
 src/main/java/com/dji/sample/wayline/model/WaylineFileEntity.java                          |   57 
 src/main/java/com/dji/sample/component/mqtt/model/CommonTopicResponse.java                 |   33 
 src/main/java/com/dji/sample/media/service/IMediaService.java                              |   36 
 src/main/java/com/dji/sample/manage/model/dto/DeviceModelDTO.java                          |   27 
 src/main/java/com/dji/sample/manage/model/enums/StateDataEnum.java                         |   26 
 src/main/java/com/dji/sample/manage/handler/StateSplitter.java                             |   59 
 src/main/java/com/dji/sample/configuration/GlobalMVCConfigurer.java                        |   35 
 src/main/java/com/dji/sample/manage/model/receiver/RTKStateReceiver.java                   |   28 
 src/main/java/com/dji/sample/manage/model/entity/DeviceEntity.java                         |   69 
 src/main/java/com/dji/sample/manage/service/impl/AbstractTSAService.java                   |   59 
 src/main/java/com/dji/sample/media/model/FileUploadDTO.java                                |   26 
 src/main/java/com/dji/sample/media/model/FileMetadataDTO.java                              |   31 
 src/main/java/com/dji/sample/manage/service/ITSAService.java                               |   17 
 src/main/java/com/dji/sample/manage/model/receiver/OsdSubDeviceReceiver.java               |   55 
 src/main/resources/application.yml                                                         |   86 
 src/main/java/com/dji/sample/manage/service/impl/LiveStreamServiceImpl.java                |  326 +
 src/main/java/com/dji/sample/manage/service/impl/GatewayOSDServiceImpl.java                |   74 
 src/main/java/com/dji/sample/component/oss/model/AliyunOSSConfiguration.java               |  104 
 src/main/java/com/dji/sample/manage/model/enums/LiveMethodEnum.java                        |   27 
 src/main/java/com/dji/sample/component/mqtt/model/ChannelName.java                         |   46 
 src/main/java/com/dji/sample/map/model/dto/ElementPointDTO.java                            |   46 
 src/main/java/com/dji/sample/component/mqtt/service/IMqttTopicService.java                 |   38 
 src/main/java/com/dji/sample/manage/handler/StatusRouter.java                              |   70 
 src/main/java/com/dji/sample/manage/service/impl/DeviceServiceImpl.java                    |  553 +++
 src/main/java/com/dji/sample/CloudApiSampleApplication.java                                |   17 
 src/main/java/com/dji/sample/storage/controller/StorageController.java                     |   51 
 src/main/java/com/dji/sample/manage/model/dto/TopologyDTO.java                             |   24 
 src/main/java/com/dji/sample/component/websocket/config/AuthPrincipalHandler.java          |   70 
 src/main/java/com/dji/sample/common/error/IErrorInfo.java                                  |   22 
 src/main/java/com/dji/sample/media/model/PositionDTO.java                                  |   16 
 src/main/java/com/dji/sample/common/model/Pagination.java                                  |   36 
 src/main/java/com/dji/sample/manage/service/impl/DeviceOSDServiceImpl.java                 |   69 
 src/main/java/com/dji/sample/map/model/dto/ContentPropertyDTO.java                         |   25 
 src/main/java/com/dji/sample/component/GlobalScheduleService.java                          |   58 
 src/main/java/com/dji/sample/manage/model/enums/LiveVideoQualityEnum.java                  |   51 
 src/main/java/com/dji/sample/component/mqtt/service/impl/MqttTopicServiceImpl.java         |   44 
 src/main/java/com/dji/sample/component/oss/model/MinIOConfiguration.java                   |   93 
 .gitignore                                                                                 |   38 
 src/main/java/com/dji/sample/map/dao/IElementCoordinateMapper.java                         |   12 
 src/main/java/com/dji/sample/media/controller/MediaController.java                         |   73 
 src/main/java/com/dji/sample/component/mqtt/config/MqttInboundConfiguration.java           |   69 
 src/main/java/com/dji/sample/component/mqtt/model/TopicConst.java                          |   29 
 src/main/java/com/dji/sample/map/service/impl/GroupServiceImpl.java                        |   66 
 src/main/java/com/dji/sample/manage/model/dto/LiveUrlRTSPDTO.java                          |   18 
 src/main/java/com/dji/sample/manage/model/receiver/BatteryReceiver.java                    |   27 
 src/main/java/com/dji/sample/manage/dao/IUserMapper.java                                   |    8 
 src/main/java/com/dji/sample/manage/service/impl/UserServiceImpl.java                      |  141 
 src/main/java/com/dji/sample/manage/service/IWorkspaceService.java                         |   23 
 src/main/java/com/dji/sample/manage/model/entity/CapacityCameraEntity.java                 |   46 
 src/main/java/com/dji/sample/manage/model/receiver/CapacityDeviceReceiver.java             |   25 
 src/main/java/com/dji/sample/manage/model/entity/WorkspaceEntity.java                      |   33 
 src/main/java/com/dji/sample/map/model/entity/GroupElementEntity.java                      |   55 
 src/main/java/com/dji/sample/manage/model/receiver/StatusSubDeviceReceiver.java            |   32 
 src/main/java/com/dji/sample/map/model/dto/ResourceContentDTO.java                         |   25 
 src/main/java/com/dji/sample/wayline/model/WaylineFileDTO.java                             |   41 
 src/main/java/com/dji/sample/manage/controller/DevicePayloadController.java                |   66 
 src/main/java/com/dji/sample/manage/model/dto/DeviceDictionaryDTO.java                     |   28 
 src/main/java/com/dji/sample/media/service/impl/MediaServiceImpl.java                      |   41 
 src/main/java/com/dji/sample/manage/service/impl/DeviceDictionaryServiceImpl.java          |   59 
 src/main/java/com/dji/sample/manage/handler/AbstractStateTopicHandler.java                 |   35 
 src/main/java/com/dji/sample/component/websocket/model/BizCodeEnum.java                    |   37 
 src/main/java/com/dji/sample/map/service/IGroupService.java                                |   24 
 src/main/java/com/dji/sample/map/model/dto/ElementCoordinateDTO.java                       |   25 
 src/main/java/com/dji/sample/manage/service/impl/TopologyServiceImpl.java                  |   55 
 src/main/java/com/dji/sample/storage/service/IStorageService.java                          |   17 
 src/main/java/com/dji/sample/manage/model/dto/UserLoginDTO.java                            |   14 
 src/main/java/com/dji/sample/manage/service/impl/CapacityCameraServiceImpl.java            |  172 
 src/main/java/com/dji/sample/component/websocket/service/ISendMessageService.java          |   28 
 src/main/java/com/dji/sample/map/service/IGroupElementService.java                         |   54 
 src/main/java/com/dji/sample/manage/service/IUserService.java                              |   32 
 src/main/java/com/dji/sample/component/mybatis/MybatisPlusMetaObjectHandler.java           |   37 
 src/main/java/com/dji/sample/component/mqtt/model/CommonTopicReceiver.java                 |   29 
 src/main/java/com/dji/sample/manage/model/receiver/ServiceReplyReceiver.java               |   18 
 src/main/java/com/dji/sample/manage/dao/IWorkspaceMapper.java                              |    8 
 src/main/java/com/dji/sample/manage/model/dto/UserDTO.java                                 |   37 
 src/main/java/com/dji/sample/manage/model/receiver/LiveStatusReceiver.java                 |   23 
 src/main/java/com/dji/sample/map/model/entity/GroupEntity.java                             |   49 
 src/main/java/com/dji/sample/media/model/MediaFileEntity.java                              |   62 
 src/main/java/com/dji/sample/wayline/service/impl/WaylineFileServiceImpl.java              |  179 
 src/main/java/com/dji/sample/manage/model/dto/IconUrlDTO.java                              |   25 
 src/main/java/com/dji/sample/media/service/IFileService.java                               |   37 
 src/main/java/com/dji/sample/manage/model/entity/UserEntity.java                           |   41 
 src/main/java/com/dji/sample/map/model/dto/ElementPolygonDTO.java                          |   48 
 src/main/java/com/dji/sample/manage/dao/ICameraVideoMapper.java                            |   12 
 src/main/java/com/dji/sample/manage/service/impl/CameraVideoServiceImpl.java               |   99 
 src/main/java/com/dji/sample/manage/service/IDeviceDictionaryService.java                  |   24 
 src/main/java/com/dji/sample/manage/controller/TopologyController.java                     |   40 
 src/main/java/com/dji/sample/component/websocket/config/WebSocketDefaultFactory.java       |   20 
 src/main/java/com/dji/sample/manage/model/enums/IconUrlEnum.java                           |   37 
 src/main/java/com/dji/sample/manage/service/ILiveStreamService.java                        |   51 
 src/main/java/com/dji/sample/component/mqtt/config/MqttConfiguration.java                  |   63 
 src/main/java/com/dji/sample/map/service/IElementCoordinateService.java                    |   35 
 src/main/java/com/dji/sample/manage/model/enums/UserTypeEnum.java                          |   24 
 src/main/java/com/dji/sample/manage/controller/UserController.java                         |   29 
 src/main/java/com/dji/sample/map/service/impl/ElementCoordinateServiceImpl.java            |   94 
 src/main/java/com/dji/sample/component/mqtt/config/MqttOutboundConfiguration.java          |   47 
 src/main/java/com/dji/sample/component/mqtt/service/impl/MessageSenderServiceImpl.java     |   50 
 src/main/java/com/dji/sample/component/mqtt/service/IMessageSenderService.java             |   27 
 src/main/java/com/dji/sample/map/service/impl/GroupElementServiceImpl.java                 |  199 +
 src/main/java/com/dji/sample/manage/model/Chan.java                                        |   45 
 src/main/java/com/dji/sample/manage/model/receiver/WirelessLinkStateReceiver.java          |   22 
 src/main/java/com/dji/sample/map/dao/IGroupMapper.java                                     |   12 
 src/main/java/com/dji/sample/common/model/CustomClaim.java                                 |   88 
 src/main/java/com/dji/sample/manage/model/enums/DeviceDomainEnum.java                      |   63 
 src/main/java/com/dji/sample/manage/model/receiver/CapacityVideoReceiver.java              |   19 
 src/main/java/com/dji/sample/media/model/StsCredentialsDTO.java                            |   33 
 src/main/java/com/dji/sample/map/controller/WorkspaceElementController.java                |  184 +
 src/main/java/com/dji/sample/wayline/model/WaylineFileUploadDTO.java                       |   18 
 src/main/java/com/dji/sample/manage/model/dto/LiveTypeDTO.java                             |   26 
 src/main/java/com/dji/sample/map/model/dto/ElementLineStringDTO.java                       |   47 
 src/main/java/com/dji/sample/component/oss/service/IOssService.java                        |   27 
 src/main/java/com/dji/sample/manage/model/DeviceStatusManager.java                         |   20 
 src/main/java/com/dji/sample/manage/model/dto/LiveUrlGB28181DTO.java                       |   27 
 src/main/java/com/dji/sample/wayline/model/WaylineQueryParam.java                          |   30 
 sql/cloud_sample.sql                                                                       |  324 +
 src/main/java/com/dji/sample/map/service/impl/WorkspaceElementServiceImpl.java             |  109 
 src/main/java/com/dji/sample/configuration/GlobalThreadPoolConfiguration.java              |   42 
 src/main/java/com/dji/sample/map/model/entity/ElementCoordinateEntity.java                 |   41 
 src/main/java/com/dji/sample/manage/model/entity/DevicePayloadEntity.java                  |   56 
 src/main/java/com/dji/sample/component/websocket/model/WebSocketManager.java               |  112 
 src/main/java/com/dji/sample/manage/dao/ICapacityCameraMapper.java                         |   12 
 src/main/java/com/dji/sample/storage/service/impl/AliyunStorageServiceImpl.java            |   32 
 src/main/java/com/dji/sample/map/dao/IGroupElementMapper.java                              |   12 
 src/main/java/com/dji/sample/component/websocket/config/WebSocketMessageConfiguration.java |   39 
 src/main/java/com/dji/sample/common/error/LiveErrorEnum.java                               |   78 
 src/main/java/com/dji/sample/manage/handler/StateRouter.java                               |   92 
 src/main/java/com/dji/sample/manage/service/ICameraVideoService.java                       |   37 
 src/main/java/com/dji/sample/manage/model/dto/DeviceDTO.java                               |   47 
 src/main/java/com/dji/sample/component/mqtt/config/InboundMessageRouter.java               |   95 
 src/main/java/com/dji/sample/manage/handler/StatePayloadHandler.java                       |   55 
 src/main/java/com/dji/sample/common/model/ResponseResult.java                              |   69 
 src/main/java/com/dji/sample/manage/handler/StateDefaultHandler.java                       |   25 
 src/main/java/com/dji/sample/manage/model/receiver/DevicePayloadReceiver.java              |   29 
 src/main/java/com/dji/sample/map/model/dto/GroupDTO.java                                   |   38 
 src/main/java/com/dji/sample/media/dao/IFileMapper.java                                    |   12 
 src/main/java/com/dji/sample/wayline/service/IWaylineFileService.java                      |   65 
 src/main/java/com/dji/sample/wayline/controller/WaylineFileController.java                 |  150 
 src/main/java/com/dji/sample/media/controller/FileController.java                          |   36 
 src/main/java/com/dji/sample/map/model/dto/ElementResourceDTO.java                         |   26 
 src/main/java/com/dji/sample/manage/handler/StateDeviceBasicHandler.java                   |   34 
 src/main/java/com/dji/sample/media/model/FileExtensionDTO.java                             |   26 
 src/main/java/com/dji/sample/manage/controller/WorkspaceController.java                    |   41 
 src/main/java/com/dji/sample/manage/model/dto/CapacityCameraDTO.java                       |   34 
 src/main/java/com/dji/sample/manage/service/ICapacityCameraService.java                    |   45 
 src/main/java/com/dji/sample/manage/service/impl/DevicePayloadServiceImpl.java             |  157 
 src/main/java/com/dji/sample/manage/model/receiver/OsdGatewayReceiver.java                 |   31 
 src/main/java/com/dji/sample/manage/service/IDeviceService.java                            |  135 
 src/main/java/com/dji/sample/manage/model/dto/CapacityVideoDTO.java                        |   24 
 src/main/java/com/dji/sample/map/service/IWorkspaceElementService.java                     |   71 
 src/main/java/com/dji/sample/manage/model/dto/DevicePayloadDTO.java                        |   29 
 src/main/java/com/dji/sample/component/AuthInterceptor.java                                |   60 
 src/main/java/com/dji/sample/component/mqtt/model/TopicStateReceiver.java                  |   29 
 src/main/java/com/dji/sample/component/mybatis/MybatisPlusConfiguration.java               |   24 
 src/main/java/com/dji/sample/manage/model/dto/WorkspaceDTO.java                            |   33 
 src/main/java/com/dji/sample/manage/model/receiver/DeviceBasicReceiver.java                |   39 
 src/main/java/com/dji/sample/component/CorsFilter.java                                     |   35 
 src/main/java/com/dji/sample/manage/model/dto/TopologyDeviceDTO.java                       |   35 
 src/main/java/com/dji/sample/manage/model/dto/TelemetryDTO.java                            |   22 
 src/main/java/com/dji/sample/wayline/dao/IWaylineFileMapper.java                           |   12 
 205 files changed, 10,748 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..b53c3c1
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,38 @@
+HELP.md
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/
+/.mvn/
+/mvnw
+/mvnw.cmd
+/logs/
+/src/test/
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..5cbe4c6
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,148 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+	<parent>
+		<groupId>org.springframework.boot</groupId>
+		<artifactId>spring-boot-starter-parent</artifactId>
+		<version>2.4.11</version>
+		<relativePath/> <!-- lookup parent from repository -->
+	</parent>
+
+	<groupId>com.dji</groupId>
+	<artifactId>cloud-api-sample</artifactId>
+	<version>1.0.0</version>
+	<name>cloud-api-sample</name>
+
+	<properties>
+		<java.version>11</java.version>
+		<mybatis-plus.version>3.4.2</mybatis-plus.version>
+		<druid.version>1.2.6</druid.version>
+		<jwt.version>3.12.1</jwt.version>
+		<mqtt.version>5.5.5</mqtt.version>
+		<minio.version>8.3.7</minio.version>
+		<okhttp3.version>4.9.1</okhttp3.version>
+		<aliyun-sdk-sts.version>3.1.0</aliyun-sdk-sts.version>
+		<aliyun-oss.version>3.12.0</aliyun-oss.version>
+		<javax-activation.version>1.1.1</javax-activation.version>
+		<glassfish-jaxb.version>2.3.3</glassfish-jaxb.version>
+		<log4j2.version>2.15.0</log4j2.version>
+		<javax-jaxb.version>2.3.0</javax-jaxb.version>
+	</properties>
+
+	<dependencies>
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-web</artifactId>
+		</dependency>
+
+		<dependency>
+			<groupId>mysql</groupId>
+			<artifactId>mysql-connector-java</artifactId>
+		</dependency>
+
+		<dependency>
+			<groupId>org.projectlombok</groupId>
+			<artifactId>lombok</artifactId>
+			<optional>true</optional>
+		</dependency>
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-test</artifactId>
+			<scope>test</scope>
+		</dependency>
+
+		<dependency>
+			<groupId>com.baomidou</groupId>
+			<artifactId>mybatis-plus-boot-starter</artifactId>
+			<version>${mybatis-plus.version}</version>
+		</dependency>
+
+		<dependency>
+			<groupId>com.alibaba</groupId>
+			<artifactId>druid-spring-boot-starter</artifactId>
+			<version>${druid.version}</version>
+		</dependency>
+
+		<dependency>
+			<groupId>com.auth0</groupId>
+			<artifactId>java-jwt</artifactId>
+			<version>${jwt.version}</version>
+		</dependency>
+
+		<dependency>
+			<groupId>org.springframework.integration</groupId>
+			<artifactId>spring-integration-mqtt</artifactId>
+			<version>${mqtt.version}</version>
+		</dependency>
+
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-websocket</artifactId>
+		</dependency>
+        <dependency>
+            <groupId>org.jetbrains</groupId>
+            <artifactId>annotations</artifactId>
+            <version>RELEASE</version>
+            <scope>compile</scope>
+        </dependency>
+
+		<dependency>
+			<groupId>io.minio</groupId>
+			<artifactId>minio</artifactId>
+			<version>${minio.version}</version>
+		</dependency>
+
+		<dependency>
+			<groupId>com.squareup.okhttp3</groupId>
+			<artifactId>okhttp</artifactId>
+			<version>${okhttp3.version}</version>
+		</dependency>
+
+		<dependency>
+			<groupId>com.aliyun</groupId>
+			<artifactId>aliyun-java-sdk-sts</artifactId>
+			<version>${aliyun-sdk-sts.version}</version>
+		</dependency>
+
+		<dependency>
+			<groupId>com.aliyun.oss</groupId>
+			<artifactId>aliyun-sdk-oss</artifactId>
+			<version>${aliyun-oss.version}</version>
+		</dependency>
+
+		<dependency>
+			<groupId>javax.xml.bind</groupId>
+			<artifactId>jaxb-api</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>javax.activation</groupId>
+			<artifactId>activation</artifactId>
+			<version>${javax-activation.version}</version>
+		</dependency>
+		<!-- no more than 2.3.3-->
+		<dependency>
+			<groupId>org.glassfish.jaxb</groupId>
+			<artifactId>jaxb-runtime</artifactId>
+			<version>${glassfish-jaxb.version}</version>
+		</dependency>
+
+	</dependencies>
+
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.springframework.boot</groupId>
+				<artifactId>spring-boot-maven-plugin</artifactId>
+				<configuration>
+					<excludes>
+						<exclude>
+							<groupId>org.projectlombok</groupId>
+							<artifactId>lombok</artifactId>
+						</exclude>
+					</excludes>
+				</configuration>
+			</plugin>
+		</plugins>
+	</build>
+</project>
diff --git a/sql/cloud_sample.sql b/sql/cloud_sample.sql
new file mode 100644
index 0000000..4cfe7fd
--- /dev/null
+++ b/sql/cloud_sample.sql
@@ -0,0 +1,324 @@
+CREATE DATABASE  IF NOT EXISTS `cloud_sample` /*!40100 DEFAULT CHARACTER SET utf8 */;
+USE `cloud_sample`;
+
+/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
+/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
+/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
+SET NAMES utf8mb4;
+/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
+/*!40101 SET @OLD_SQL_MODE='NO_AUTO_VALUE_ON_ZERO', SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
+/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
+
+
+# manage_camera_video
+# ------------------------------------------------------------
+
+DROP TABLE IF EXISTS `manage_camera_video`;
+
+CREATE TABLE `manage_camera_video` (
+  `id` int unsigned NOT NULL AUTO_INCREMENT,
+  `camera_id` int NOT NULL,
+  `video_index` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '',
+  `video_type` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
+
+
+
+# manage_capacity_camera
+# ------------------------------------------------------------
+
+DROP TABLE IF EXISTS `manage_capacity_camera`;
+
+CREATE TABLE `manage_capacity_camera` (
+  `id` int unsigned NOT NULL AUTO_INCREMENT,
+  `device_sn` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '',
+  `name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT 'undefined',
+  `description` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
+  `camera_index` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '',
+  `coexist_video_number_max` int NOT NULL,
+  `available_video_number` int NOT NULL,
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
+
+
+
+# manage_device
+# ------------------------------------------------------------
+
+DROP TABLE IF EXISTS `manage_device`;
+
+CREATE TABLE `manage_device` (
+  `id` int unsigned NOT NULL AUTO_INCREMENT,
+  `device_sn` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '',
+  `device_name` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT 'undefined',
+  `workspace_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '',
+  `device_type` smallint NOT NULL,
+  `sub_type` smallint NOT NULL,
+  `domain` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '',
+  `version` smallint NOT NULL,
+  `device_index` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
+  `child_sn` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
+  `create_time` bigint NOT NULL,
+  `update_time` bigint NOT NULL,
+  `device_desc` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
+  `url_normal` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
+  `url_select` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `product_sn_UNIQUE` (`device_sn`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
+
+
+
+# manage_device_dictionary
+# ------------------------------------------------------------
+
+DROP TABLE IF EXISTS `manage_device_dictionary`;
+
+CREATE TABLE `manage_device_dictionary` (
+  `id` int unsigned NOT NULL AUTO_INCREMENT,
+  `domain` int NOT NULL,
+  `device_type` int NOT NULL,
+  `sub_type` int NOT NULL,
+  `device_name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '',
+  `device_desc` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
+
+LOCK TABLES `manage_device_dictionary` WRITE;
+/*!40000 ALTER TABLE `manage_device_dictionary` DISABLE KEYS */;
+
+INSERT INTO `manage_device_dictionary` (`id`, `domain`, `device_type`, `sub_type`, `device_name`, `device_desc`)
+VALUES
+	(1,0,60,0,'Matrice 300 RTK',NULL),
+	(16,2,56,0,'DJI Smart Controller','Remote control for M300'),
+	(17,1,20,0,'Z30',NULL),
+	(18,1,26,0,'XT2',NULL),
+	(19,1,39,0,'FPV',NULL),
+	(20,1,41,0,'XTS',NULL),
+	(21,1,42,0,'H20',NULL),
+	(22,1,43,0,'H20T',NULL),
+	(24,1,90742,0,'L1',NULL),
+	(27,1,50,0,'P1 24mm lens',NULL),
+	(28,1,50,1,'P1 35mm lens',NULL),
+	(29,1,50,2,'P1 50mm lens',NULL),
+	(30,0,67,0,'Matrice 30',NULL),
+    (31,0,67,1,'Matrice 30T',NULL),
+    (32,2,119,0,'DJI RC Plus','Remote control for M30'),
+    (33,1,52,0,'M30',NULL),
+    (34,1,53,1,'M30T',NULL);
+
+/*!40000 ALTER TABLE `manage_device_dictionary` ENABLE KEYS */;
+UNLOCK TABLES;
+
+
+# manage_device_payload
+# ------------------------------------------------------------
+
+DROP TABLE IF EXISTS `manage_device_payload`;
+
+CREATE TABLE `manage_device_payload` (
+  `id` int unsigned NOT NULL AUTO_INCREMENT,
+  `payload_sn` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '',
+  `payload_name` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT 'undefined',
+  `payload_type` smallint NOT NULL,
+  `sub_type` smallint NOT NULL,
+  `version` smallint DEFAULT NULL,
+  `payload_index` smallint NOT NULL,
+  `device_sn` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '',
+  `payload_desc` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
+  `create_time` bigint NOT NULL,
+  `update_time` bigint NOT NULL,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `payload_sn_UNIQUE` (`payload_sn`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
+
+
+
+# manage_user
+# ------------------------------------------------------------
+
+DROP TABLE IF EXISTS `manage_user`;
+
+CREATE TABLE `manage_user` (
+  `id` int unsigned NOT NULL AUTO_INCREMENT,
+  `user_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '',
+  `username` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '',
+  `password` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '',
+  `workspace_id` int NOT NULL,
+  `user_type` smallint NOT NULL,
+  `mqtt_username` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '',
+  `mqtt_password` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '',
+  `create_time` bigint NOT NULL,
+  `update_time` bigint NOT NULL,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `user_id_UNIQUE` (`user_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
+
+LOCK TABLES `manage_user` WRITE;
+/*!40000 ALTER TABLE `manage_user` DISABLE KEYS */;
+
+INSERT INTO `manage_user` (`id`, `user_id`, `username`, `password`, `workspace_id`, `user_type`, `mqtt_username`, `mqtt_password`, `create_time`, `update_time`)
+VALUES
+	(1,'a1559e7c-8dd8-4780-b952-100cc4797da2','adminPC','adminPC',1,1,'admin','admin',1634898410751,1634898410751),
+	(2,'be7c6c3d-afe9-4be4-b9eb-c55066c0914e','pilot','pilot123',1,2,'pilot','pilot123',1634898410751,1634898410751);
+
+/*!40000 ALTER TABLE `manage_user` ENABLE KEYS */;
+UNLOCK TABLES;
+
+
+# manage_workspace
+# ------------------------------------------------------------
+
+DROP TABLE IF EXISTS `manage_workspace`;
+
+CREATE TABLE `manage_workspace` (
+  `id` int unsigned NOT NULL AUTO_INCREMENT,
+  `workspace_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '',
+  `workspace_name` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '',
+  `workspace_desc` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '',
+  `platform_name` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '',
+  `create_time` bigint NOT NULL,
+  `update_time` bigint NOT NULL,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `workspace_id_UNIQUE` (`workspace_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
+
+LOCK TABLES `manage_workspace` WRITE;
+/*!40000 ALTER TABLE `manage_workspace` DISABLE KEYS */;
+
+INSERT INTO `manage_workspace` (`id`, `workspace_id`, `workspace_name`, `workspace_desc`, `platform_name`, `create_time`, `update_time`)
+VALUES
+	(1,'e3dea0f5-37f2-4d79-ae58-490af3228069','Test Group One','Cloud Sample Test Platform','Cloud Api Platform',1634898410751,1634898410751);
+
+/*!40000 ALTER TABLE `manage_workspace` ENABLE KEYS */;
+UNLOCK TABLES;
+
+
+# map_element_coordinate
+# ------------------------------------------------------------
+
+DROP TABLE IF EXISTS `map_element_coordinate`;
+
+CREATE TABLE `map_element_coordinate` (
+  `id` int unsigned NOT NULL AUTO_INCREMENT,
+  `element_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '',
+  `longitude` decimal(18,14) NOT NULL,
+  `latitude` decimal(17,14) NOT NULL,
+  `altitude` decimal(17,14) DEFAULT NULL,
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
+
+
+
+# map_group
+# ------------------------------------------------------------
+
+DROP TABLE IF EXISTS `map_group`;
+
+CREATE TABLE `map_group` (
+  `id` int unsigned NOT NULL AUTO_INCREMENT,
+  `group_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
+  `group_name` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
+  `group_type` int NOT NULL,
+  `workspace_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '',
+  `is_distributed` tinyint(1) NOT NULL DEFAULT '1',
+  `is_lock` tinyint(1) NOT NULL DEFAULT '0',
+  `create_time` bigint NOT NULL,
+  `update_time` bigint NOT NULL,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `group_id_UNIQUE` (`group_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8_bin;
+
+LOCK TABLES `map_group` WRITE;
+/*!40000 ALTER TABLE `map_group` DISABLE KEYS */;
+
+INSERT INTO `map_group` (`id`, `group_id`, `group_name`, `group_type`, `workspace_id`, `is_distributed`, `is_lock`, `create_time`, `update_time`)
+VALUES
+	(1,'e3dea0f5-37f2-4d79-ae58-490af3228060','Pilot Share Layer',2,'e3dea0f5-37f2-4d79-ae58-490af3228069',1,0,1638330077356,1638330077356),
+	(2,'e3dea0f5-37f2-4d79-ae58-490af3228011','Default Layer',1,'e3dea0f5-37f2-4d79-ae58-490af3228069',1,0,1638330077356,1638330077356);
+
+/*!40000 ALTER TABLE `map_group` ENABLE KEYS */;
+UNLOCK TABLES;
+
+
+# map_group_element
+# ------------------------------------------------------------
+
+DROP TABLE IF EXISTS `map_group_element`;
+
+CREATE TABLE `map_group_element` (
+  `id` int unsigned NOT NULL AUTO_INCREMENT,
+  `element_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
+  `element_name` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
+  `display` smallint NOT NULL DEFAULT '1',
+  `group_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '',
+  `element_type` smallint NOT NULL,
+  `username` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '',
+  `color` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '',
+  `clamp_to_ground` tinyint(1) NOT NULL DEFAULT '0',
+  `create_time` bigint NOT NULL,
+  `update_time` bigint NOT NULL,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `element_id_UNIQUE` (`element_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8_bin;
+
+
+
+# media_file
+# ------------------------------------------------------------
+
+DROP TABLE IF EXISTS `media_file`;
+
+CREATE TABLE `media_file` (
+  `id` int unsigned NOT NULL AUTO_INCREMENT,
+  `file_name` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '',
+  `file_path` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '',
+  `workspace_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '',
+  `fingerprint` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '',
+  `tinny_fingerprint` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '',
+  `object_key` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '',
+  `sub_file_type` int NOT NULL,
+  `is_original` tinyint(1) NOT NULL,
+  `drone` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT 'undefined',
+  `payload` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT 'undefined',
+  `create_time` bigint NOT NULL,
+  `update_time` bigint NOT NULL,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `fingerprint_UNIQUE` (`fingerprint`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
+
+
+
+# wayline_file
+# ------------------------------------------------------------
+
+DROP TABLE IF EXISTS `wayline_file`;
+
+CREATE TABLE `wayline_file` (
+  `id` int unsigned NOT NULL AUTO_INCREMENT,
+  `name` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
+  `wayline_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
+  `drone_model_key` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
+  `payload_model_keys` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
+  `workspace_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '',
+  `favorited` tinyint(1) NOT NULL DEFAULT '0',
+  `template_types` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
+  `object_key` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '',
+  `user_name` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
+  `create_time` bigint NOT NULL,
+  `update_time` bigint NOT NULL COMMENT 'required, can not modify.',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `wayline_id_UNIQUE` (`wayline_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
+
+
+
+
+/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
+/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
+/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
+/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
+/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
+/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
+
diff --git a/src/main/java/com/dji/sample/CloudApiSampleApplication.java b/src/main/java/com/dji/sample/CloudApiSampleApplication.java
new file mode 100644
index 0000000..14cca32
--- /dev/null
+++ b/src/main/java/com/dji/sample/CloudApiSampleApplication.java
@@ -0,0 +1,17 @@
+package com.dji.sample;
+
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.scheduling.annotation.EnableScheduling;
+
+@MapperScan("com.dji.sample.*.dao")
+@SpringBootApplication
+@EnableScheduling
+public class CloudApiSampleApplication {
+
+	public static void main(String[] args) {
+		SpringApplication.run(CloudApiSampleApplication.class, args);
+	}
+
+}
diff --git a/src/main/java/com/dji/sample/common/error/CommonErrorEnum.java b/src/main/java/com/dji/sample/common/error/CommonErrorEnum.java
new file mode 100644
index 0000000..1dab9d1
--- /dev/null
+++ b/src/main/java/com/dji/sample/common/error/CommonErrorEnum.java
@@ -0,0 +1,40 @@
+package com.dji.sample.common.error;
+
+/**
+ * @author sean.zhou
+ * @version 0.1
+ * @date 2021/11/25
+ */
+public enum CommonErrorEnum implements IErrorInfo {
+
+    SYSTEM_ERROR(600500, "system error"),
+
+    SECRET_INVALID(600100, "secret invalid"),
+
+    NO_TOKEN(600101, "accss_token is null"),
+
+    TOKEN_EXPIRED(600102, "token is expired"),
+
+    TOKEN_INVALID(600103, "token invalid"),
+
+    SIGN_INVALID(600104, "sign invalid");
+
+    private String msg;
+
+    private int code;
+
+    CommonErrorEnum(int code, String msg) {
+        this.code = code;
+        this.msg = msg;
+    }
+
+    @Override
+    public String getErrorMsg() {
+        return this.msg;
+    }
+
+    @Override
+    public Integer getErrorCode() {
+        return this.code;
+    }
+}
diff --git a/src/main/java/com/dji/sample/common/error/IErrorInfo.java b/src/main/java/com/dji/sample/common/error/IErrorInfo.java
new file mode 100644
index 0000000..3332dd3
--- /dev/null
+++ b/src/main/java/com/dji/sample/common/error/IErrorInfo.java
@@ -0,0 +1,22 @@
+package com.dji.sample.common.error;
+
+/**
+ * @author sean.zhou
+ * @version 0.1
+ * @date 2021/11/25
+ */
+public interface IErrorInfo {
+
+    /**
+     * Get error message.
+     * @return error message
+     */
+    String getErrorMsg();
+
+    /**
+     * Get error code.
+     * @return error code
+     */
+    Integer getErrorCode();
+
+}
diff --git a/src/main/java/com/dji/sample/common/error/LiveErrorEnum.java b/src/main/java/com/dji/sample/common/error/LiveErrorEnum.java
new file mode 100644
index 0000000..0a65eed
--- /dev/null
+++ b/src/main/java/com/dji/sample/common/error/LiveErrorEnum.java
@@ -0,0 +1,78 @@
+package com.dji.sample.common.error;
+
+/**
+ * Live streaming related error codes. When on-demand via mqtt,
+ * it can be matched with the error code information replied by the pilot.
+ * @author sean.zhou
+ * @version 0.1
+ * @date 2021/11/25
+ */
+public enum LiveErrorEnum implements IErrorInfo {
+
+    NO_AIRCRAFT(613001, "No aircraft."),
+
+    NO_CAMERA(613002, "No camera."),
+
+    LIVE_STREAM_ALREADY_STARTED(613003, "The camera has started live streaming."),
+    
+    FUNCTION_NOT_SUPPORT(613004, "The function is not supported."),
+    
+    STRATEGY_NOT_SUPPORT(613005, "The strategy is not supported."),
+    
+    NOT_IN_CAMERA_INTERFACE(613006, "The current app is not in the camera interface."),
+    
+    NO_FLIGHT_CONTROL(613007, "The remote control has no flight control rights and cannot respond to control commands"),
+
+    NO_STREAM_DATA(613008, "The current app has no stream data."),
+    
+    TOO_FREQUENT(613009, "The operation is too frequent."),
+    
+    ENABLE_FAILED(613010, "Please check whether the live stream service is normal."),
+    
+    NO_LIVE_STREAM(613011, "There are no live stream currently."),
+    
+    SWITCH_NOT_SUPPORT(613012, "There is already another camera in the live stream. It's not support to switch the stream directly."),
+
+    URL_TYPE_NOT_SUPPORTED(613013, "This url type is not supported."),
+
+    ERROR_PARAMETERS(613014, "The live stream parameters are abnormal or incomplete."),
+
+    NO_REPLY(613098, "No live reply received."),
+
+    UNKNOWN(613099, "UNKNOWN");
+
+
+    private String msg;
+
+    private int code;
+
+    LiveErrorEnum(int code, String msg) {
+        this.code = code;
+        this.msg = msg;
+    }
+
+    @Override
+    public String getErrorMsg() {
+        return this.msg;
+    }
+
+    @Override
+    public Integer getErrorCode() {
+        return this.code;
+    }
+
+    /**
+     * Get the corresponding enumeration object based on the error code.
+     * @param code error code
+     * @return enumeration object
+     */
+    public static LiveErrorEnum find(int code) {
+
+        for (LiveErrorEnum errorEnum : LiveErrorEnum.class.getEnumConstants()) {
+            if (errorEnum.code == code) {
+                return errorEnum;
+            }
+        }
+        return UNKNOWN;
+    }
+}
diff --git a/src/main/java/com/dji/sample/common/model/CustomClaim.java b/src/main/java/com/dji/sample/common/model/CustomClaim.java
new file mode 100644
index 0000000..85c16a9
--- /dev/null
+++ b/src/main/java/com/dji/sample/common/model/CustomClaim.java
@@ -0,0 +1,88 @@
+package com.dji.sample.common.model;
+
+import com.auth0.jwt.interfaces.Claim;
+import com.fasterxml.jackson.annotation.JsonAlias;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+import java.lang.reflect.Field;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * A custom claim for storing custom information in the token.
+ * @author sean.zhou
+ * @date 2021/11/16
+ * @version 0.1
+ */
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+@Slf4j
+public class CustomClaim {
+
+    /**
+     * The id of the account.
+     */
+    private String id;
+
+    private String username;
+
+    @JsonAlias("user_type")
+    private Integer userType;
+
+    @JsonAlias("workspace_id")
+    private String workspaceId;
+
+    /**
+     * Convert the custom claim data type to the Map type.
+     * @return map
+     */
+    public ConcurrentHashMap<String, String> convertToMap() {
+        ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>(4);
+        try {
+            Field[] declaredFields = this.getClass().getDeclaredFields();
+            for (Field field : declaredFields) {
+                JsonAlias annotation = field.getAnnotation(JsonAlias.class);
+                field.setAccessible(true);
+                // The value of key is named underscore.
+                map.put(annotation != null ? annotation.value()[0] : field.getName(),
+                        field.get(this).toString());
+            }
+        } catch (IllegalAccessException e) {
+            log.info("CustomClaim converts failed. {}", this.toString());
+            e.printStackTrace();
+        }
+        return map;
+    }
+
+    /**
+     * Convert the data in Map into a custom claim object.
+     * @param claimMap
+     */
+    public CustomClaim (Map<String, Claim> claimMap) {
+        Field[] declaredFields = this.getClass().getDeclaredFields();
+        for (Field field : declaredFields) {
+            field.setAccessible(true);
+            JsonAlias annotation = field.getAnnotation(JsonAlias.class);
+
+            Claim value = claimMap.get(annotation == null ? field.getName() : annotation.value()[0]);
+            try {
+                Class<?> type = field.getType();
+                if (Integer.class.equals(type)) {
+                    field.set(this, Integer.valueOf(value.asString()));
+                    continue;
+                }
+                if (String.class.equals(type)) {
+                    field.set(this, value.asString());
+                    continue;
+                }
+            } catch (IllegalAccessException e) {
+                log.info("Claim parses failed. {}", claimMap.toString());
+                e.printStackTrace();
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/common/model/Pagination.java b/src/main/java/com/dji/sample/common/model/Pagination.java
new file mode 100644
index 0000000..74d1e8a
--- /dev/null
+++ b/src/main/java/com/dji/sample/common/model/Pagination.java
@@ -0,0 +1,36 @@
+package com.dji.sample.common.model;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import lombok.Data;
+
+/**
+ * Used for paging display in the wayline. These field names cannot be changed.
+ * Because they need to be the same as the pilot.
+ * @author sean
+ * @version 0.3
+ * @date 2021/12/22
+ */
+@Data
+public class Pagination {
+
+    /**
+     * The current page number.
+     */
+    private long page;
+
+    /**
+     * The amount of data displayed per page.
+     */
+    private long pageSize;
+
+    /**
+     * The total amount of all data.
+     */
+    private long total;
+
+    public Pagination(Page page) {
+        this.page = page.getCurrent();
+        this.pageSize = page.getSize();
+        this.total = page.getTotal();
+    }
+}
diff --git a/src/main/java/com/dji/sample/common/model/PaginationData.java b/src/main/java/com/dji/sample/common/model/PaginationData.java
new file mode 100644
index 0000000..95e2442
--- /dev/null
+++ b/src/main/java/com/dji/sample/common/model/PaginationData.java
@@ -0,0 +1,27 @@
+package com.dji.sample.common.model;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * The format of the data response when a paginated display is required.
+ * @author sean
+ * @version 0.3
+ * @date 2021/12/22
+ */
+@Data
+public class PaginationData<T> {
+
+    /**
+     * The collection in which the data list is stored.
+     */
+    private List<T> list;
+
+    private Pagination pagination;
+
+    public PaginationData(List<T> list, Pagination pagination) {
+        this.list = list;
+        this.pagination = pagination;
+    }
+}
diff --git a/src/main/java/com/dji/sample/common/model/ResponseResult.java b/src/main/java/com/dji/sample/common/model/ResponseResult.java
new file mode 100644
index 0000000..8ad8e1b
--- /dev/null
+++ b/src/main/java/com/dji/sample/common/model/ResponseResult.java
@@ -0,0 +1,69 @@
+package com.dji.sample.common.model;
+
+import com.dji.sample.common.error.IErrorInfo;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.springframework.http.HttpStatus;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder
+@JsonInclude
+public class ResponseResult<T> {
+
+    public static final int CODE_SUCCESS = 0;
+    public static final String MESSAGE_SUCCESS = "success";
+
+    private int code;
+
+    private String message;
+
+    private T data;
+
+    public static <T> ResponseResult<T> success(T data) {
+        return ResponseResult.<T>builder()
+                .code(CODE_SUCCESS)
+                .message(MESSAGE_SUCCESS)
+                .data(data)
+                .build();
+    }
+
+    public static ResponseResult success() {
+        return ResponseResult.builder()
+                .code(0)
+                .message(MESSAGE_SUCCESS)
+                .build();
+    }
+
+    public static ResponseResult error() {
+        return ResponseResult.builder()
+                .code(HttpStatus.INTERNAL_SERVER_ERROR.value())
+                .message(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase())
+                .build();
+    }
+
+    public static ResponseResult error(String message) {
+        return ResponseResult.builder()
+                .code(HttpStatus.INTERNAL_SERVER_ERROR.value())
+                .message(message)
+                .build();
+    }
+
+    public static ResponseResult error(int code, String message) {
+        return ResponseResult.builder()
+                .code(code)
+                .message(message)
+                .build();
+    }
+
+    public static ResponseResult error(IErrorInfo errorInfo) {
+        return ResponseResult.builder()
+                .code(errorInfo.getErrorCode())
+                .message(errorInfo.getErrorMsg())
+                .build();
+    }
+}
diff --git a/src/main/java/com/dji/sample/common/util/JwtUtil.java b/src/main/java/com/dji/sample/common/util/JwtUtil.java
new file mode 100644
index 0000000..9c2cb9a
--- /dev/null
+++ b/src/main/java/com/dji/sample/common/util/JwtUtil.java
@@ -0,0 +1,103 @@
+package com.dji.sample.common.util;
+
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.JWTCreator;
+import com.auth0.jwt.JWTVerifier;
+import com.auth0.jwt.algorithms.Algorithm;
+import com.auth0.jwt.exceptions.TokenExpiredException;
+import com.auth0.jwt.interfaces.DecodedJWT;
+import com.dji.sample.common.model.CustomClaim;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import java.util.Date;
+import java.util.Map;
+import java.util.Optional;
+
+@Slf4j
+@Component
+public class JwtUtil {
+
+    private static String issuer;
+
+    private static String subject;
+
+    private static long age;
+
+    private static String secret;
+
+    private static Algorithm algorithm;
+
+    @Value("${jwt.issuer: DJI}")
+    private void setIssuer(String issuer) {
+        JwtUtil.issuer = issuer;
+    }
+
+    @Value("${jwt.subject: CloudApiSample}")
+    private void setSubject(String subject) {
+        JwtUtil.subject = subject;
+    }
+
+    @Value("${jwt.age: 86400}")
+    private void setAge(long age) {
+        JwtUtil.age = age * 1000;
+    }
+
+    @Value("${jwt.secret: CloudApiSample}")
+    private void setSecret(String secret) {
+        JwtUtil.secret = secret;
+        setAlgorithm();
+    }
+
+    private void setAlgorithm() {
+        JwtUtil.algorithm = Algorithm.HMAC256(secret);
+    }
+
+    /**
+     * Create a token based on custom information.
+     * @param claims custom information
+     * @return token
+     */
+    public static String createToken(Map<String, String> claims) {
+        Date now = new Date();
+        JWTCreator.Builder builder = JWT.create();
+        // Add custom information to the token's payload segment.
+        claims.forEach(builder::withClaim);
+        String token = builder.withIssuer(issuer)
+                .withSubject(subject)
+                .withIssuedAt(now)
+                .withExpiresAt(new Date(now.getTime() + age))
+                .withNotBefore(now)
+                .sign(algorithm);
+        log.debug("token created. " + token);
+        return token;
+    }
+
+    /**
+     * Verify that the token is valid.
+     * @param token
+     * @return
+     * @throws TokenExpiredException
+     */
+    public static DecodedJWT verifyToken(String token) {
+        try {
+            JWTVerifier verifier = JWT.require(algorithm).build();
+            return verifier.verify(token);
+        } catch (Exception e) {
+            log.error(e.getMessage());
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+    /**
+     * Parses the custom information in the token into a CustomClaim object.
+     * @param token
+     * @return custom claim
+     */
+    public static Optional<CustomClaim> parseToken(String token) {
+        DecodedJWT jwt = verifyToken(token);
+        return jwt == null ? Optional.empty() : Optional.of(new CustomClaim(jwt.getClaims()));
+    }
+}
diff --git a/src/main/java/com/dji/sample/component/ApplicationBootInitial.java b/src/main/java/com/dji/sample/component/ApplicationBootInitial.java
new file mode 100644
index 0000000..226fe1a
--- /dev/null
+++ b/src/main/java/com/dji/sample/component/ApplicationBootInitial.java
@@ -0,0 +1,40 @@
+package com.dji.sample.component;
+
+import com.dji.sample.manage.model.DeviceStatusManager;
+import com.dji.sample.manage.model.enums.DeviceDomainEnum;
+import com.dji.sample.manage.model.param.DeviceQueryParam;
+import com.dji.sample.manage.service.IDeviceService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+
+/**
+ * @author sean.zhou
+ * @date 2021/11/24
+ * @version 0.1
+ */
+@Component
+public class ApplicationBootInitial implements CommandLineRunner {
+
+    @Autowired
+    private IDeviceService deviceService;
+
+    /**
+     * Subscribe to the devices that exist in the database when the program starts,
+     * to prevent the data from being different from the pilot side due to program interruptions.
+     * @param args
+     * @throws Exception
+     */
+    @Override
+    public void run(String... args) throws Exception {
+        deviceService.getDevicesByParams(DeviceQueryParam.builder().build())
+                .forEach(device -> {
+                    deviceService.subscribeTopicOnline(device.getDeviceSn());
+                    DeviceStatusManager.STATUS_MANAGER.put(
+                            DeviceDomainEnum.getVal(device.getDomain()) + "/"
+                                    + device.getDeviceSn(), LocalDateTime.now());
+                });
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/component/AuthInterceptor.java b/src/main/java/com/dji/sample/component/AuthInterceptor.java
new file mode 100644
index 0000000..0d3b8f7
--- /dev/null
+++ b/src/main/java/com/dji/sample/component/AuthInterceptor.java
@@ -0,0 +1,60 @@
+package com.dji.sample.component;
+
+import com.dji.sample.common.error.CommonErrorEnum;
+import com.dji.sample.common.model.CustomClaim;
+import com.dji.sample.common.util.JwtUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+import org.springframework.web.servlet.HandlerInterceptor;
+import org.springframework.web.servlet.ModelAndView;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.util.Optional;
+
+@Slf4j
+@Component
+public class AuthInterceptor implements HandlerInterceptor {
+
+    public static final String PARAM_TOKEN = "x-auth-token";
+
+    public static final String TOKEN_CLAIM = "customClaim";
+
+    @Override
+    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+        String uri = request.getRequestURI();
+        log.debug("request uri: {}", uri);
+        // The options method is passed directly.
+        if (HttpMethod.OPTIONS.matches(request.getMethod())) {
+            response.setStatus(HttpStatus.OK.value());
+            return false;
+        }
+        String token = request.getHeader(PARAM_TOKEN);
+        // Check if the token exists.
+        if (!StringUtils.hasText(token)) {
+            response.setStatus(HttpStatus.UNAUTHORIZED.value());
+            log.error(CommonErrorEnum.NO_TOKEN.getErrorMsg());
+            return false;
+        }
+
+        // Check if the current token is valid.
+        Optional<CustomClaim> customClaimOpt = JwtUtil.parseToken(token);
+        if (customClaimOpt.isEmpty()) {
+            response.setStatus(HttpStatus.UNAUTHORIZED.value());
+            return false;
+        }
+
+        // Put the custom data from the token into the request.
+        request.setAttribute(TOKEN_CLAIM, customClaimOpt.get());
+        return true;
+    }
+
+    @Override
+    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
+        // Delete the custom data in the request after the request ends.
+        request.removeAttribute(TOKEN_CLAIM);
+    }
+}
diff --git a/src/main/java/com/dji/sample/component/CorsFilter.java b/src/main/java/com/dji/sample/component/CorsFilter.java
new file mode 100644
index 0000000..a4c245f
--- /dev/null
+++ b/src/main/java/com/dji/sample/component/CorsFilter.java
@@ -0,0 +1,35 @@
+package com.dji.sample.component;
+
+import org.springframework.stereotype.Component;
+
+import javax.servlet.*;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+import static com.dji.sample.component.AuthInterceptor.PARAM_TOKEN;
+
+/**
+ * @author sean.zhou
+ * @version 0.1
+ * @date 2021/11/22
+ */
+@Component
+public class CorsFilter implements Filter {
+
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
+        HttpServletResponse res = (HttpServletResponse) response;
+        res.addHeader("Access-Control-Allow-Credentials", "true");
+        res.addHeader("Access-Control-Allow-Origin", "*");
+        res.addHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT");
+        res.addHeader("Access-Control-Allow-Headers", "Access-Control-Allow-Headers," +
+                "Authorization, Content-Length, X-CSRF-Token, Token,session,X_Requested_With,Accept, "+
+                        "Origin, Host, Connection, Accept-Encoding, Accept-Language,DNT, X-CustomHeader, Keep-Alive," +
+                        " User-Agent, X-Requested-With, If-Modified-Since, Cache-Control, Content-Type, Pragma," + PARAM_TOKEN);
+        if (((HttpServletRequest) request).getMethod().equals("OPTIONS")) {
+            return;
+        }
+        filterChain.doFilter(request, response);
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/component/GlobalExceptionHandler.java b/src/main/java/com/dji/sample/component/GlobalExceptionHandler.java
new file mode 100644
index 0000000..1d5eb14
--- /dev/null
+++ b/src/main/java/com/dji/sample/component/GlobalExceptionHandler.java
@@ -0,0 +1,33 @@
+package com.dji.sample.component;
+
+import com.dji.sample.common.model.ResponseResult;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/12/1
+ */
+@ControllerAdvice
+@ResponseBody
+public class GlobalExceptionHandler {
+
+    /**
+     * Please do not return directly like this, there is a risk.
+     * @param e
+     * @return
+     */
+    @ExceptionHandler(Exception.class)
+    public ResponseResult exceptionHandler(Exception e) {
+        e.printStackTrace();
+        return ResponseResult.error(e.getLocalizedMessage());
+    }
+
+    @ExceptionHandler(NullPointerException.class)
+    public ResponseResult nullPointerExceptionHandler(NullPointerException e) {
+        e.printStackTrace();
+        return ResponseResult.error("A null object appeared.");
+    }
+}
diff --git a/src/main/java/com/dji/sample/component/GlobalScheduleService.java b/src/main/java/com/dji/sample/component/GlobalScheduleService.java
new file mode 100644
index 0000000..ea44a62
--- /dev/null
+++ b/src/main/java/com/dji/sample/component/GlobalScheduleService.java
@@ -0,0 +1,58 @@
+package com.dji.sample.component;
+
+import com.dji.sample.manage.model.enums.DeviceDomainEnum;
+import com.dji.sample.manage.service.IDeviceService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import static com.dji.sample.manage.model.DeviceStatusManager.DEFAULT_ALIVE_SECOND;
+import static com.dji.sample.manage.model.DeviceStatusManager.STATUS_MANAGER;
+
+/**
+ * @author sean.zhou
+ * @date 2021/11/24
+ * @version 0.1
+ */
+@Component
+@Slf4j
+public class GlobalScheduleService {
+
+    @Autowired
+    private IDeviceService deviceService;
+
+    /**
+     * Check the status of the devices every 30 seconds. It is recommended to use cache.
+     */
+    @Scheduled(fixedRate = 30, timeUnit = TimeUnit.SECONDS)
+    private void deviceStatusListen() {
+        for (Map.Entry<String, LocalDateTime> entry : STATUS_MANAGER.entrySet()) {
+            if (entry.getValue().isAfter(
+                    LocalDateTime.now().minusSeconds(DEFAULT_ALIVE_SECOND))) {
+                continue;
+            }
+
+            String device = entry.getKey();
+            int index = device.indexOf("/");
+
+            STATUS_MANAGER.remove(device);
+
+            int type = Integer.parseInt(device.substring(0, index));
+            String sn = device.substring(index + 1);
+            // Determine whether it is a gateway device.
+            if (DeviceDomainEnum.GATEWAY.getVal() == type) {
+                deviceService.deviceOffline(sn);
+                deviceService.unsubscribeTopicOffline(sn);
+                continue;
+            }
+
+            deviceService.subDeviceOffline(sn);
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/component/mqtt/config/InboundMessageRouter.java b/src/main/java/com/dji/sample/component/mqtt/config/InboundMessageRouter.java
new file mode 100644
index 0000000..1b2bdf2
--- /dev/null
+++ b/src/main/java/com/dji/sample/component/mqtt/config/InboundMessageRouter.java
@@ -0,0 +1,95 @@
+package com.dji.sample.component.mqtt.config;
+
+import com.dji.sample.component.mqtt.model.ChannelName;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.integration.annotation.Router;
+import org.springframework.integration.mqtt.support.MqttHeaders;
+import org.springframework.integration.router.AbstractMessageRouter;
+import org.springframework.messaging.Message;
+import org.springframework.messaging.MessageChannel;
+import org.springframework.messaging.MessageHeaders;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.regex.Pattern;
+
+import static com.dji.sample.component.mqtt.model.TopicConst.*;
+
+/**
+ *
+ * @author sean.zhou
+ * @date 2021/11/10
+ * @version 0.1
+ */
+@Component
+@Slf4j
+public class InboundMessageRouter extends AbstractMessageRouter {
+
+    @Resource(name = ChannelName.INBOUND)
+    private MessageChannel inboundChannel;
+
+    @Resource(name = ChannelName.INBOUND_STATUS)
+    private MessageChannel statusChannel;
+
+    @Resource(name = ChannelName.INBOUND_STATE)
+    private MessageChannel stateChannel;
+
+    @Resource(name = ChannelName.DEFAULT)
+    private MessageChannel defaultChannel;
+
+    @Resource(name = ChannelName.INBOUND_SERVICE_REPLY)
+    private MessageChannel serviceReplyChannel;
+
+    @Resource(name = ChannelName.INBOUND_OSD)
+    private MessageChannel osdChannel;
+
+    private static final Pattern PATTERN_TOPIC_STATUS =
+            Pattern.compile("^" + BASIC_PRE + PRODUCT + REGEX_SN + STATUS_SUF + "$");
+
+    private static final Pattern PATTERN_TOPIC_STATE =
+            Pattern.compile("^" + THING_MODEL_PRE + PRODUCT + REGEX_SN + STATE_SUF + "$");
+
+    private static final Pattern PATTERN_TOPIC_SERVICE_REPLY =
+            Pattern.compile("^" + THING_MODEL_PRE + PRODUCT + REGEX_SN + SERVICES_SUF + _REPLY_SUF + "$");
+
+    private static final Pattern PATTERN_TOPIC_OSD =
+            Pattern.compile("^" + THING_MODEL_PRE + PRODUCT + REGEX_SN + OSD_SUF);
+
+    /**
+     * All mqtt broker messages will arrive here before distributing them to different channels.
+     * @param message message from mqtt broker
+     * @return channel
+     */
+    @Override
+    @Router(inputChannel = ChannelName.INBOUND)
+    protected Collection<MessageChannel> determineTargetChannels(Message<?> message) {
+        MessageHeaders headers = message.getHeaders();
+        String topic = headers.get(MqttHeaders.RECEIVED_TOPIC).toString();
+        byte[] payload = (byte[])message.getPayload();
+
+        // osd
+        if (PATTERN_TOPIC_OSD.matcher(topic).matches()) {
+            return Collections.singleton(osdChannel);
+        }
+
+        log.debug("received topic :{} \t payload :{}", topic, new String(payload));
+
+        // status
+        if (PATTERN_TOPIC_STATUS.matcher(topic).matches()) {
+            return Collections.singleton(statusChannel);
+        }
+
+        // state
+        if (PATTERN_TOPIC_STATE.matcher(topic).matches()) {
+            return Collections.singleton(stateChannel);
+        }
+
+        // services_reply
+        if (PATTERN_TOPIC_SERVICE_REPLY.matcher(topic).matches()) {
+            return Collections.singleton(serviceReplyChannel);
+        }
+        return Collections.singleton(defaultChannel);
+    }
+}
diff --git a/src/main/java/com/dji/sample/component/mqtt/config/MqttConfiguration.java b/src/main/java/com/dji/sample/component/mqtt/config/MqttConfiguration.java
new file mode 100644
index 0000000..750ba58
--- /dev/null
+++ b/src/main/java/com/dji/sample/component/mqtt/config/MqttConfiguration.java
@@ -0,0 +1,63 @@
+package com.dji.sample.component.mqtt.config;
+
+import lombok.Data;
+import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.integration.mqtt.core.DefaultMqttPahoClientFactory;
+import org.springframework.integration.mqtt.core.MqttPahoClientFactory;
+
+/**
+ *
+ * @author sean.zhou
+ * @date 2021/11/10
+ * @version 0.1
+ */
+@Configuration
+@Data
+@ConfigurationProperties(prefix = "mqtt")
+public class MqttConfiguration {
+
+    private String protocol;
+
+    private String host;
+
+    private Integer port;
+
+    private String username;
+
+    private String password;
+
+    private String clientId;
+
+    /**
+     * The topic to subscribe to immediately when client connects.
+     */
+    private String inboundTopic;
+
+    @Bean
+    public MqttConnectOptions mqttConnectOptions() {
+        MqttConnectOptions mqttConnectOptions = new MqttConnectOptions();
+        mqttConnectOptions.setServerURIs(new String[]{
+                new StringBuilder()
+                        .append(protocol.trim())
+                        .append("://")
+                        .append(host.trim())
+                        .append(":")
+                        .append(port)
+                        .toString()});
+        mqttConnectOptions.setUserName(username);
+        mqttConnectOptions.setPassword(password.toCharArray());
+        mqttConnectOptions.setAutomaticReconnect(true);
+        mqttConnectOptions.setKeepAliveInterval(10);
+        return mqttConnectOptions;
+    }
+
+    @Bean
+    public MqttPahoClientFactory mqttClientFactory() {
+        DefaultMqttPahoClientFactory factory = new DefaultMqttPahoClientFactory();
+        factory.setConnectionOptions(mqttConnectOptions());
+        return factory;
+    }
+}
diff --git a/src/main/java/com/dji/sample/component/mqtt/config/MqttInboundConfiguration.java b/src/main/java/com/dji/sample/component/mqtt/config/MqttInboundConfiguration.java
new file mode 100644
index 0000000..df6f04f
--- /dev/null
+++ b/src/main/java/com/dji/sample/component/mqtt/config/MqttInboundConfiguration.java
@@ -0,0 +1,69 @@
+package com.dji.sample.component.mqtt.config;
+
+import com.dji.sample.component.mqtt.model.ChannelName;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.integration.annotation.IntegrationComponentScan;
+import org.springframework.integration.annotation.ServiceActivator;
+import org.springframework.integration.endpoint.MessageProducerSupport;
+import org.springframework.integration.mqtt.core.MqttPahoClientFactory;
+import org.springframework.integration.mqtt.inbound.MqttPahoMessageDrivenChannelAdapter;
+import org.springframework.integration.mqtt.support.DefaultPahoMessageConverter;
+import org.springframework.messaging.MessageChannel;
+import org.springframework.messaging.MessageHandler;
+
+import javax.annotation.Resource;
+
+/**
+ * Client configuration for inbound messages.
+ * @author sean.zhou
+ * @date 2021/11/10
+ * @version 0.1
+ */
+@Slf4j
+@Configuration
+@IntegrationComponentScan
+public class MqttInboundConfiguration {
+
+    @Autowired
+    private MqttConfiguration mqttConfiguration;
+
+    @Autowired
+    private MqttPahoClientFactory mqttClientFactory;
+
+    @Resource(name = ChannelName.INBOUND)
+    private MessageChannel inboundChannel;
+
+    /**
+     * Clients of inbound message channels.
+     * @return
+     */
+    @Bean(name = "adapter")
+    public MessageProducerSupport mqttInbound() {
+        MqttPahoMessageDrivenChannelAdapter adapter = new MqttPahoMessageDrivenChannelAdapter(
+                mqttConfiguration.getClientId() + "_consumer_" + System.currentTimeMillis(),
+                mqttClientFactory, mqttConfiguration.getInboundTopic().split(","));
+        DefaultPahoMessageConverter converter = new DefaultPahoMessageConverter();
+        // use byte types uniformly
+        converter.setPayloadAsBytes(true);
+        adapter.setConverter(converter);
+        adapter.setQos(1);
+        adapter.setOutputChannel(inboundChannel);
+        return adapter;
+    }
+
+    /**
+     * Define a default channel to handle messages that have no effect.
+     * @return
+     */
+    @Bean
+    @ServiceActivator(inputChannel = ChannelName.DEFAULT)
+    public MessageHandler defaultInboundHandler() {
+        return message -> {
+            log.info("The default channel does not handle messages.");
+        };
+    }
+
+}
diff --git a/src/main/java/com/dji/sample/component/mqtt/config/MqttMessageChannel.java b/src/main/java/com/dji/sample/component/mqtt/config/MqttMessageChannel.java
new file mode 100644
index 0000000..2f06938
--- /dev/null
+++ b/src/main/java/com/dji/sample/component/mqtt/config/MqttMessageChannel.java
@@ -0,0 +1,90 @@
+package com.dji.sample.component.mqtt.config;
+
+import com.dji.sample.component.mqtt.model.ChannelName;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.integration.channel.DirectChannel;
+import org.springframework.integration.channel.ExecutorChannel;
+import org.springframework.messaging.MessageChannel;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Definition classes for all channels
+ * @author sean.zhou
+ * @date 2021/11/10
+ * @version 0.1
+ */
+@Configuration
+public class MqttMessageChannel {
+
+    @Autowired
+    private Executor threadPool;
+
+    @Bean(name = ChannelName.INBOUND)
+    public MessageChannel inboundChannel() {
+        return new ExecutorChannel(threadPool);
+    }
+
+    @Bean(name = ChannelName.INBOUND_STATUS)
+    public MessageChannel statusChannel() {
+        return new DirectChannel();
+    }
+
+    @Bean(name = ChannelName.INBOUND_STATUS_ONLINE)
+    public MessageChannel statusOnlineChannel() {
+        return new DirectChannel();
+    }
+
+    @Bean(name = ChannelName.INBOUND_STATUS_OFFLINE)
+    public MessageChannel statusOffChannel() {
+        return new DirectChannel();
+    }
+
+    @Bean(name = ChannelName.INBOUND_STATE)
+    public MessageChannel stateChannel() {
+        return new DirectChannel();
+    }
+
+    @Bean(name = ChannelName.INBOUND_STATE_BASIC)
+    public MessageChannel stateBasicChannel() {
+        return new DirectChannel();
+    }
+
+    @Bean(name = ChannelName.INBOUND_STATE_PAYLOAD)
+    public MessageChannel statePayloadChannel() {
+        return new DirectChannel();
+    }
+
+    @Bean(name = ChannelName.INBOUND_SERVICE_REPLY)
+    public MessageChannel serviceReplyChannel() {
+        return new DirectChannel();
+    }
+
+    @Bean(name = ChannelName.INBOUND_STATE_CAPACITY)
+    public MessageChannel stateCapacityChannel() {
+        return new DirectChannel();
+    }
+
+    @Bean(name = ChannelName.INBOUND_STATE_PAYLOAD_UPDATE)
+    public MessageChannel statePayloadUpdateChannel() {
+        return new DirectChannel();
+    }
+
+    @Bean(name = ChannelName.INBOUND_OSD)
+    public MessageChannel osdChannel() {
+        return new DirectChannel();
+    }
+
+    @Bean(name = ChannelName.DEFAULT)
+    public MessageChannel defaultChannel() {
+        return new DirectChannel();
+    }
+
+    @Bean(name = ChannelName.OUTBOUND)
+    public MessageChannel outboundChannel() {
+        return new DirectChannel();
+    }
+
+}
diff --git a/src/main/java/com/dji/sample/component/mqtt/config/MqttOutboundConfiguration.java b/src/main/java/com/dji/sample/component/mqtt/config/MqttOutboundConfiguration.java
new file mode 100644
index 0000000..69b919b
--- /dev/null
+++ b/src/main/java/com/dji/sample/component/mqtt/config/MqttOutboundConfiguration.java
@@ -0,0 +1,47 @@
+package com.dji.sample.component.mqtt.config;
+
+import com.dji.sample.component.mqtt.model.ChannelName;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.integration.annotation.ServiceActivator;
+import org.springframework.integration.mqtt.core.MqttPahoClientFactory;
+import org.springframework.integration.mqtt.outbound.MqttPahoMessageHandler;
+import org.springframework.integration.mqtt.support.DefaultPahoMessageConverter;
+import org.springframework.messaging.MessageHandler;
+
+/**
+ * Client configuration for outbound messages.
+ * @author sean.zhou
+ * @date 2021/11/10
+ * @version 0.1
+ */
+@Configuration
+public class MqttOutboundConfiguration {
+
+    @Autowired
+    private MqttConfiguration mqttConfiguration;
+
+    @Autowired
+    private MqttPahoClientFactory mqttClientFactory;
+
+    /**
+     * Clients of outbound message channels.
+     * @return
+     */
+    @Bean
+    @ServiceActivator(inputChannel = ChannelName.OUTBOUND)
+    public MessageHandler mqttOutbound() {
+        MqttPahoMessageHandler messageHandler = new MqttPahoMessageHandler(
+                mqttConfiguration.getClientId() + "_producer_" + System.currentTimeMillis(),
+                mqttClientFactory);
+        DefaultPahoMessageConverter converter = new DefaultPahoMessageConverter();
+        // use byte types uniformly
+        converter.setPayloadAsBytes(true);
+
+        messageHandler.setAsync(true);
+        messageHandler.setDefaultQos(0);
+        messageHandler.setConverter(converter);
+        return messageHandler;
+    }
+}
diff --git a/src/main/java/com/dji/sample/component/mqtt/model/ChannelName.java b/src/main/java/com/dji/sample/component/mqtt/model/ChannelName.java
new file mode 100644
index 0000000..c8f5f01
--- /dev/null
+++ b/src/main/java/com/dji/sample/component/mqtt/model/ChannelName.java
@@ -0,0 +1,46 @@
+package com.dji.sample.component.mqtt.model;
+
+/**
+ * The name of all channels.
+ *
+ * @author sean.zhou
+ * @date 2021/11/10
+ * @version 0.1
+ */
+public class ChannelName {
+
+    public static final String INBOUND = "inbound";
+
+    public static final String INBOUND_STATUS = "inboundStatus";
+
+    public static final String INBOUND_STATUS_ROUTER = "inboundStatusRouter";
+
+    public static final String INBOUND_STATUS_ONLINE = "inboundStatusOnline";
+
+    public static final String INBOUND_STATUS_OFFLINE = "inboundStatusOffline";
+
+    public static final String INBOUND_STATE = "inboundState";
+
+    public static final String INBOUND_STATE_SPLITTER = "inboundStateSplitter";
+
+    public static final String INBOUND_STATE_ROUTER = "inboundStateRouter";
+
+    public static final String INBOUND_STATE_BASIC = "inboundStateBasic";
+
+    public static final String INBOUND_STATE_PAYLOAD = "inboundStatePayload";
+
+    public static final String INBOUND_STATE_PAYLOAD_UPDATE = "inboundStatePayloadUpdate";
+
+    public static final String INBOUND_STATE_CAPACITY = "inboundStateCapacity";
+
+    public static final String INBOUND_STATE_LIST = "inboundStateList";
+
+    public static final String INBOUND_SERVICE_REPLY = "inboundStateServiceReply";
+
+    public static final String INBOUND_OSD = "inboundOsd";
+
+    public static final String DEFAULT = "default";
+
+    public static final String OUTBOUND = "outbound";
+
+}
diff --git a/src/main/java/com/dji/sample/component/mqtt/model/CommonTopicReceiver.java b/src/main/java/com/dji/sample/component/mqtt/model/CommonTopicReceiver.java
new file mode 100644
index 0000000..5a6fe45
--- /dev/null
+++ b/src/main/java/com/dji/sample/component/mqtt/model/CommonTopicReceiver.java
@@ -0,0 +1,29 @@
+package com.dji.sample.component.mqtt.model;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import lombok.Data;
+
+/**
+ * Unified topic receiving format.
+ * @author sean.zhou
+ * @date 2021/11/10
+ * @version 0.1
+ */
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class CommonTopicReceiver<T> {
+
+    /**
+     * The command is sent and the response is matched by the tid and bid fields in the message,
+     * and the reply should keep the tid and bid the same.
+     */
+    private String tid;
+
+    private String bid;
+
+    private String method;
+
+    private Long timestamp;
+
+    private T data;
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/component/mqtt/model/CommonTopicResponse.java b/src/main/java/com/dji/sample/component/mqtt/model/CommonTopicResponse.java
new file mode 100644
index 0000000..effd5c1
--- /dev/null
+++ b/src/main/java/com/dji/sample/component/mqtt/model/CommonTopicResponse.java
@@ -0,0 +1,33 @@
+package com.dji.sample.component.mqtt.model;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * Unified Topic response format
+ * @author sean.zhou
+ * @date 2021/11/15
+ * @version 0.1
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class CommonTopicResponse<T> {
+
+    /**
+     * The command is sent and the response is matched by the tid and bid fields in the message,
+     * and the reply should keep the tid and bid the same.
+     */
+    private String tid;
+
+    private String bid;
+
+    private String method;
+
+    private T data;
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/component/mqtt/model/TopicConst.java b/src/main/java/com/dji/sample/component/mqtt/model/TopicConst.java
new file mode 100644
index 0000000..667c969
--- /dev/null
+++ b/src/main/java/com/dji/sample/component/mqtt/model/TopicConst.java
@@ -0,0 +1,29 @@
+package com.dji.sample.component.mqtt.model;
+
+/**
+ * All the topics that need to be used in the project.
+ * @author sean.zhou
+ * @date 2021/11/10
+ * @version 0.1
+ */
+public class TopicConst {
+
+    public static final String BASIC_PRE = "sys/";
+
+    public static final String THING_MODEL_PRE = "thing/";
+
+    public static final String PRODUCT = "product/";
+
+    public static final String STATUS_SUF = "/status";
+
+    public static final String _REPLY_SUF = "_reply";
+
+    public static final String STATE_SUF = "/state";
+
+    public static final String SERVICES_SUF = "/services";
+
+    public static final String OSD_SUF = "/osd";
+
+    public static final String REGEX_SN = "[A-Za-z0-9]+";
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/component/mqtt/model/TopicStateReceiver.java b/src/main/java/com/dji/sample/component/mqtt/model/TopicStateReceiver.java
new file mode 100644
index 0000000..522a4f8
--- /dev/null
+++ b/src/main/java/com/dji/sample/component/mqtt/model/TopicStateReceiver.java
@@ -0,0 +1,29 @@
+package com.dji.sample.component.mqtt.model;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import lombok.Data;
+
+/**
+ * The data format of the state topic.
+ * @author sean.zhou
+ * @date 2021/11/17
+ * @version 0.1
+ */
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class TopicStateReceiver<T> {
+
+    private String tid;
+
+    private String bid;
+
+    private Long timestamp;
+
+    /**
+     * The sn of the gateway device.
+     */
+    private String gateway;
+
+    private T data;
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/component/mqtt/service/IMessageSenderService.java b/src/main/java/com/dji/sample/component/mqtt/service/IMessageSenderService.java
new file mode 100644
index 0000000..7930d41
--- /dev/null
+++ b/src/main/java/com/dji/sample/component/mqtt/service/IMessageSenderService.java
@@ -0,0 +1,27 @@
+package com.dji.sample.component.mqtt.service;
+
+import com.dji.sample.component.mqtt.model.CommonTopicResponse;
+
+/**
+ * @author sean.zhou
+ * @version 0.1
+ * @date 2021/11/25
+ */
+public interface IMessageSenderService {
+
+    /**
+     * Publish a message to a specific topic.
+     * @param topic target
+     * @param response message
+     */
+    void publish(String topic, CommonTopicResponse response);
+
+    /**
+     * Use a specific qos to push messages to a specific topic.
+     * @param topic target
+     * @param qos   qos
+     * @param response  message
+     */
+    void publish(String topic, int qos, CommonTopicResponse response);
+
+}
diff --git a/src/main/java/com/dji/sample/component/mqtt/service/IMqttMessageGateway.java b/src/main/java/com/dji/sample/component/mqtt/service/IMqttMessageGateway.java
new file mode 100644
index 0000000..cb50b12
--- /dev/null
+++ b/src/main/java/com/dji/sample/component/mqtt/service/IMqttMessageGateway.java
@@ -0,0 +1,33 @@
+package com.dji.sample.component.mqtt.service;
+
+import com.dji.sample.component.mqtt.model.ChannelName;
+import org.springframework.integration.annotation.MessagingGateway;
+import org.springframework.integration.mqtt.support.MqttHeaders;
+import org.springframework.messaging.handler.annotation.Header;
+import org.springframework.stereotype.Component;
+
+/**
+ *
+ * @author sean.zhou
+ * @date 2021/11/10
+ * @version 0.1
+ */
+@Component
+@MessagingGateway(defaultRequestChannel = ChannelName.OUTBOUND)
+public interface IMqttMessageGateway {
+
+    /**
+     * Publish a message to a specific topic.
+     * @param topic target
+     * @param payload   message
+     */
+    void publish(@Header(MqttHeaders.TOPIC) String topic, byte[] payload);
+
+    /**
+     * Use a specific qos to push messages to a specific topic.
+     * @param topic     target
+     * @param payload   message
+     * @param qos   qos
+     */
+    void publish(@Header(MqttHeaders.TOPIC) String topic, byte[] payload, @Header(MqttHeaders.QOS) int qos);
+}
diff --git a/src/main/java/com/dji/sample/component/mqtt/service/IMqttTopicService.java b/src/main/java/com/dji/sample/component/mqtt/service/IMqttTopicService.java
new file mode 100644
index 0000000..6983109
--- /dev/null
+++ b/src/main/java/com/dji/sample/component/mqtt/service/IMqttTopicService.java
@@ -0,0 +1,38 @@
+package com.dji.sample.component.mqtt.service;
+
+import org.springframework.integration.mqtt.support.MqttHeaders;
+import org.springframework.messaging.handler.annotation.Header;
+
+/**
+ *
+ * @author sean.zhou
+ * @date 2021/11/10
+ * @version 0.1
+ */
+public interface IMqttTopicService {
+
+    /**
+     * Subscribe to a specific topic.
+     * @param topic target
+     */
+    void subscribe(@Header(MqttHeaders.TOPIC) String topic);
+
+    /**
+     * Subscribe to a specific topic using a specific qos.
+     * @param topic target
+     * @param qos   qos
+     */
+    void subscribe(@Header(MqttHeaders.TOPIC) String topic, int qos);
+
+    /**
+     * Unsubscribe from a specific topic.
+     * @param topic target
+     */
+    void unsubscribe(@Header(MqttHeaders.TOPIC) String topic);
+
+    /**
+     * Get all the subscribed topics.
+     * @return topics
+     */
+    String[] getSubscribedTopic();
+}
diff --git a/src/main/java/com/dji/sample/component/mqtt/service/impl/MessageSenderServiceImpl.java b/src/main/java/com/dji/sample/component/mqtt/service/impl/MessageSenderServiceImpl.java
new file mode 100644
index 0000000..2510ead
--- /dev/null
+++ b/src/main/java/com/dji/sample/component/mqtt/service/impl/MessageSenderServiceImpl.java
@@ -0,0 +1,50 @@
+package com.dji.sample.component.mqtt.service.impl;
+
+import com.dji.sample.component.mqtt.model.CommonTopicResponse;
+import com.dji.sample.component.mqtt.service.IMessageSenderService;
+import com.dji.sample.component.mqtt.service.IMqttMessageGateway;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+/**
+ * @author sean.zhou
+ * @date 2021/11/16
+ * @version 0.1
+ */
+@Service
+@Slf4j
+public class MessageSenderServiceImpl implements IMessageSenderService {
+
+    @Autowired
+    private IMqttMessageGateway messageGateway;
+
+    public void publish(String topic, CommonTopicResponse response) {
+        try {
+            ObjectMapper mapper = new ObjectMapper();
+            // Only parameters whose value is not null will be serialised.
+            mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
+
+            messageGateway.publish(topic, mapper.writeValueAsBytes(response));
+        } catch (JsonProcessingException e) {
+            log.info("Failed to publish the message. {}", response.toString());
+            e.printStackTrace();
+        }
+    }
+
+    public void publish(String topic, int qos, CommonTopicResponse response) {
+        try {
+            ObjectMapper mapper = new ObjectMapper();
+            // Only parameters whose value is not null will be serialised.
+            mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
+
+            messageGateway.publish(topic, mapper.writeValueAsBytes(response), qos);
+        } catch (JsonProcessingException e) {
+            log.info("Failed to publish the message. {}", response.toString());
+            e.printStackTrace();
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/component/mqtt/service/impl/MqttTopicServiceImpl.java b/src/main/java/com/dji/sample/component/mqtt/service/impl/MqttTopicServiceImpl.java
new file mode 100644
index 0000000..6880ecf
--- /dev/null
+++ b/src/main/java/com/dji/sample/component/mqtt/service/impl/MqttTopicServiceImpl.java
@@ -0,0 +1,44 @@
+package com.dji.sample.component.mqtt.service.impl;
+
+import com.dji.sample.component.mqtt.service.IMqttTopicService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.integration.mqtt.inbound.MqttPahoMessageDrivenChannelAdapter;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+
+/**
+ *
+ * @author sean.zhou
+ * @date 2021/11/10
+ * @version 0.1
+ */
+@Component
+@Slf4j
+public class MqttTopicServiceImpl implements IMqttTopicService {
+
+    @Resource
+    private MqttPahoMessageDrivenChannelAdapter adapter;
+
+    @Override
+    public void subscribe(String topic) {
+        log.debug("subscribe topic: {}", topic);
+        adapter.addTopic(topic);
+    }
+
+    @Override
+    public void subscribe(String topic, int qos) {
+        log.debug("subscribe topic: {}", topic);
+        adapter.addTopic(topic, qos);
+    }
+
+    @Override
+    public void unsubscribe(String topic) {
+        log.debug("unsubscribe topic: {}", topic);
+        adapter.removeTopic(topic);
+    }
+
+    public String[] getSubscribedTopic() {
+        return adapter.getTopic();
+    }
+}
diff --git a/src/main/java/com/dji/sample/component/mybatis/MybatisPlusConfiguration.java b/src/main/java/com/dji/sample/component/mybatis/MybatisPlusConfiguration.java
new file mode 100644
index 0000000..2992641
--- /dev/null
+++ b/src/main/java/com/dji/sample/component/mybatis/MybatisPlusConfiguration.java
@@ -0,0 +1,24 @@
+package com.dji.sample.component.mybatis;
+
+import com.baomidou.mybatisplus.annotation.DbType;
+import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * @author sean
+ * @version 0.3
+ * @date 2021/12/22
+ */
+@Configuration
+public class MybatisPlusConfiguration {
+
+    @Bean
+    public MybatisPlusInterceptor mybatisPlusInterceptor() {
+        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
+        // select database
+        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
+        return interceptor;
+    }
+}
diff --git a/src/main/java/com/dji/sample/component/mybatis/MybatisPlusMetaObjectHandler.java b/src/main/java/com/dji/sample/component/mybatis/MybatisPlusMetaObjectHandler.java
new file mode 100644
index 0000000..88170ed
--- /dev/null
+++ b/src/main/java/com/dji/sample/component/mybatis/MybatisPlusMetaObjectHandler.java
@@ -0,0 +1,37 @@
+package com.dji.sample.component.mybatis;
+
+import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
+import org.apache.ibatis.reflection.MetaObject;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+
+/**
+ * Automatic filling for set values
+ */
+@Component
+public class MybatisPlusMetaObjectHandler implements MetaObjectHandler {
+
+    /**
+     * Automatic filling when inserting into the database.
+     * @param metaObject
+     */
+    @Override
+    public void insertFill(MetaObject metaObject) {
+        this.strictInsertFill(metaObject, "createTime", Long.class,
+                LocalDateTime.now().toInstant(ZoneOffset.ofHours(8)).toEpochMilli());
+        this.strictInsertFill(metaObject, "updateTime", Long.class,
+                LocalDateTime.now().toInstant(ZoneOffset.ofHours(8)).toEpochMilli());
+    }
+
+    /**
+     * Automatic filling when updating the data.
+     * @param metaObject
+     */
+    @Override
+    public void updateFill(MetaObject metaObject) {
+        this.strictUpdateFill(metaObject, "updateTime", Long.class,
+                LocalDateTime.now().toInstant(ZoneOffset.ofHours(8)).toEpochMilli());
+    }
+}
diff --git a/src/main/java/com/dji/sample/component/oss/model/AliyunOSSConfiguration.java b/src/main/java/com/dji/sample/component/oss/model/AliyunOSSConfiguration.java
new file mode 100644
index 0000000..32624ea
--- /dev/null
+++ b/src/main/java/com/dji/sample/component/oss/model/AliyunOSSConfiguration.java
@@ -0,0 +1,104 @@
+package com.dji.sample.component.oss.model;
+
+import com.aliyun.oss.OSS;
+import com.aliyun.oss.OSSClientBuilder;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Lazy;
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/12/9
+ */
+@Configuration
+public class AliyunOSSConfiguration {
+
+    /**
+     * default
+     */
+    public static final String PROVIDER = "ali";
+
+    /**
+     * Whether to use the current storage service.
+     */
+    public static boolean enable;
+
+    /**
+     * The protocol needs to be included at the beginning of the address.
+     */
+    public static String endpoint;
+
+    public static String accessKey;
+
+    public static String secretKey;
+
+    public static String region;
+
+    public static Long expire;
+
+    public static String roleSessionName;
+
+    public static String roleArn;
+
+    public static String bucket;
+
+    public static String objectDirPrefix;
+
+    @Value("${aliyun.oss.endpoint}")
+    private void setEndpoint(String endpoint) {
+        AliyunOSSConfiguration.endpoint = endpoint;
+    }
+
+    @Value("${aliyun.oss.access-key}")
+    private void setAccessKey(String accessKey) {
+        AliyunOSSConfiguration.accessKey = accessKey;
+    }
+
+    @Value("${aliyun.oss.secret-key}")
+    private void setSecretKey(String secretKey) {
+        AliyunOSSConfiguration.secretKey = secretKey;
+    }
+
+    @Value("${aliyun.oss.region}")
+    private void setRegion(String region) {
+        AliyunOSSConfiguration.region = region;
+    }
+
+    @Value("${aliyun.oss.expire: 3600}")
+    private void setExpire(Long expire) {
+        AliyunOSSConfiguration.expire = expire;
+    }
+
+    @Value("${aliyun.oss.enable: false}")
+    private void setEnable(boolean enable) {
+        AliyunOSSConfiguration.enable = enable;
+    }
+
+    @Value("${aliyun.oss.role-session-name}")
+    private void setRoleSessionName(String roleSessionName) {
+        AliyunOSSConfiguration.roleSessionName = roleSessionName;
+    }
+
+    @Value("${aliyun.oss.role-arn}")
+    private void setRoleArn(String roleArn) {
+        AliyunOSSConfiguration.roleArn = roleArn;
+    }
+
+    @Value("${aliyun.oss.bucket}")
+    private void setBucket(String bucket) {
+        AliyunOSSConfiguration.bucket = bucket;
+    }
+
+    @Value("${aliyun.oss.object-dir-prefix: wayline}")
+    private void setObjectDir(String objectDirPrefix) {
+        AliyunOSSConfiguration.objectDirPrefix = objectDirPrefix;
+    }
+
+    @Bean
+    @Lazy
+    public OSS ossClient() {
+        return new OSSClientBuilder().build(endpoint, accessKey, secretKey);
+    }
+}
diff --git a/src/main/java/com/dji/sample/component/oss/model/MinIOConfiguration.java b/src/main/java/com/dji/sample/component/oss/model/MinIOConfiguration.java
new file mode 100644
index 0000000..5768023
--- /dev/null
+++ b/src/main/java/com/dji/sample/component/oss/model/MinIOConfiguration.java
@@ -0,0 +1,93 @@
+package com.dji.sample.component.oss.model;
+
+import io.minio.MinioClient;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.util.StringUtils;
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/12/7
+ */
+@Configuration
+public class MinIOConfiguration {
+
+    /**
+     * default
+     */
+    public static final String PROVIDER = "aws";
+
+    /**
+     * Whether to use the current storage service.
+     */
+    public static boolean enable;
+
+    public static String endpoint;
+
+    public static String accessKey;
+
+    public static String secretKey;
+
+    public static String region;
+
+    public static String bucket;
+
+    public static Integer expire;
+
+    public static String objectDirPrefix;
+
+    @Value("${minio.endpoint: http://localhost:9000/}")
+    private void setEndpoint(String endpoint) {
+        MinIOConfiguration.endpoint = endpoint;
+    }
+
+    @Value("${minio.access-key: minioadmin}")
+    private void setAccessKey(String accessKey) {
+        MinIOConfiguration.accessKey = accessKey;
+    }
+
+    @Value("${minio.secret-key: minioadmin}")
+    private void setSecretKey(String secretKey) {
+        MinIOConfiguration.secretKey = secretKey;
+    }
+
+    @Value("${minio.region: }")
+    private void setRegion(String region) {
+        MinIOConfiguration.region = region;
+    }
+
+    @Value("${minio.bucket: test}")
+    private void setBucket(String bucket) {
+        MinIOConfiguration.bucket = bucket;
+    }
+
+    @Value("${minio.expire: 3600}")
+    private void setExpire(Integer expire) {
+        MinIOConfiguration.expire = expire;
+    }
+
+    @Value("${minio.enable: false}")
+    private void setEnable(boolean enable) {
+        MinIOConfiguration.enable = enable;
+    }
+
+    @Value("${minio.object-dir-prefix: wayline}")
+    private void setObjectDir(String objectDirPrefix) {
+        MinIOConfiguration.objectDirPrefix = objectDirPrefix;
+    }
+
+    @Bean
+    @Lazy
+    public MinioClient minioClient() {
+        MinioClient.Builder builder = MinioClient.builder()
+                .endpoint(endpoint)
+                .credentials(accessKey, secretKey);
+        if (StringUtils.hasText(region)) {
+            builder.region(MinIOConfiguration.region);
+        }
+        return builder.build();
+    }
+}
diff --git a/src/main/java/com/dji/sample/component/oss/service/IOssService.java b/src/main/java/com/dji/sample/component/oss/service/IOssService.java
new file mode 100644
index 0000000..beb9db6
--- /dev/null
+++ b/src/main/java/com/dji/sample/component/oss/service/IOssService.java
@@ -0,0 +1,27 @@
+package com.dji.sample.component.oss.service;
+
+import com.dji.sample.media.model.CredentialsDTO;
+
+import java.net.URL;
+
+/**
+ * @author sean
+ * @version 0.3
+ * @date 2021/12/23
+ */
+public interface IOssService {
+
+    /**
+     * Get temporary credentials.
+     * @return
+     */
+    CredentialsDTO getCredentials();
+
+    /**
+     * Get the address of the object based on the bucket name and the object name.
+     * @param bucket    bucket name
+     * @param objectKey object name
+     * @return download link
+     */
+    URL getObjectUrl(String bucket, String objectKey);
+}
diff --git a/src/main/java/com/dji/sample/component/oss/service/impl/AliyunOssServiceImpl.java b/src/main/java/com/dji/sample/component/oss/service/impl/AliyunOssServiceImpl.java
new file mode 100644
index 0000000..f9f92d3
--- /dev/null
+++ b/src/main/java/com/dji/sample/component/oss/service/impl/AliyunOssServiceImpl.java
@@ -0,0 +1,68 @@
+package com.dji.sample.component.oss.service.impl;
+
+import com.aliyun.oss.OSS;
+import com.aliyuncs.DefaultAcsClient;
+import com.aliyuncs.IAcsClient;
+import com.aliyuncs.exceptions.ClientException;
+import com.aliyuncs.profile.DefaultProfile;
+import com.aliyuncs.sts.model.v20150401.AssumeRoleRequest;
+import com.aliyuncs.sts.model.v20150401.AssumeRoleResponse;
+import com.dji.sample.component.oss.model.AliyunOSSConfiguration;
+import com.dji.sample.component.oss.service.IOssService;
+import com.dji.sample.media.model.CredentialsDTO;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+
+import java.net.URL;
+import java.util.Date;
+
+/**
+ * @author sean
+ * @version 0.3
+ * @date 2021/12/23
+ */
+@Service
+@Slf4j
+public class AliyunOssServiceImpl implements IOssService {
+
+    @Autowired
+    private OSS ossClient;
+
+    @Override
+    public CredentialsDTO getCredentials() {
+
+        try {
+            DefaultProfile profile = DefaultProfile.getProfile(
+                    AliyunOSSConfiguration.region, AliyunOSSConfiguration.accessKey, AliyunOSSConfiguration.secretKey);
+            IAcsClient client = new DefaultAcsClient(profile);
+
+            AssumeRoleRequest request = new AssumeRoleRequest();
+            request.setDurationSeconds(AliyunOSSConfiguration.expire);
+            request.setRoleArn(AliyunOSSConfiguration.roleArn);
+            request.setRoleSessionName(AliyunOSSConfiguration.roleSessionName);
+
+            AssumeRoleResponse response = client.getAcsResponse(request);
+            return new CredentialsDTO(response.getCredentials(), AliyunOSSConfiguration.expire);
+
+        } catch (ClientException e) {
+            log.debug("Failed to obtain sts.");
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    @Override
+    public URL getObjectUrl(String bucket, String objectKey) {
+        if (!StringUtils.hasText(bucket) || !StringUtils.hasText(objectKey)) {
+            return null;
+        }
+        // First check if the object can be fetched.
+        ossClient.getObject(bucket, objectKey);
+
+        return ossClient.generatePresignedUrl(bucket, objectKey,
+                new Date(System.currentTimeMillis() + AliyunOSSConfiguration.expire * 1000));
+    }
+
+}
diff --git a/src/main/java/com/dji/sample/component/oss/service/impl/MinIOServiceImpl.java b/src/main/java/com/dji/sample/component/oss/service/impl/MinIOServiceImpl.java
new file mode 100644
index 0000000..1b5e74e
--- /dev/null
+++ b/src/main/java/com/dji/sample/component/oss/service/impl/MinIOServiceImpl.java
@@ -0,0 +1,66 @@
+package com.dji.sample.component.oss.service.impl;
+
+import com.dji.sample.component.oss.model.MinIOConfiguration;
+import com.dji.sample.component.oss.service.IOssService;
+import com.dji.sample.media.model.CredentialsDTO;
+import io.minio.GetPresignedObjectUrlArgs;
+import io.minio.MinioClient;
+import io.minio.credentials.AssumeRoleProvider;
+import io.minio.errors.*;
+import io.minio.http.Method;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.io.IOException;
+import java.net.URL;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * @author sean
+ * @version 0.3
+ * @date 2021/12/23
+ */
+@Service
+@Slf4j
+public class MinIOServiceImpl implements IOssService {
+
+    @Autowired
+    private MinioClient client;
+
+    @Override
+    public CredentialsDTO getCredentials() {
+        try {
+            AssumeRoleProvider provider = new AssumeRoleProvider(MinIOConfiguration.endpoint, MinIOConfiguration.accessKey,
+                    MinIOConfiguration.secretKey, MinIOConfiguration.expire,
+                    null, null, null, null, null, null);
+            return new CredentialsDTO(provider.fetch(), MinIOConfiguration.expire);
+        } catch (NoSuchAlgorithmException e) {
+            log.debug("Failed to obtain sts.");
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    @Override
+    public URL getObjectUrl(String bucket, String objectKey) {
+        try {
+            return new URL(
+                    client.getPresignedObjectUrl(
+                            GetPresignedObjectUrlArgs.builder()
+                                    .method(Method.GET)
+                                    .bucket(bucket)
+                                    .object(objectKey)
+                                    .expiry(MinIOConfiguration.expire)
+                                    .build()));
+        } catch (ErrorResponseException | InsufficientDataException | InternalException |
+                InvalidKeyException | InvalidResponseException | IOException |
+                NoSuchAlgorithmException | XmlParserException | ServerException e) {
+            log.error("The file does not exist on the oss.");
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+}
diff --git a/src/main/java/com/dji/sample/component/websocket/config/AuthPrincipalHandler.java b/src/main/java/com/dji/sample/component/websocket/config/AuthPrincipalHandler.java
new file mode 100644
index 0000000..68f070e
--- /dev/null
+++ b/src/main/java/com/dji/sample/component/websocket/config/AuthPrincipalHandler.java
@@ -0,0 +1,70 @@
+package com.dji.sample.component.websocket.config;
+
+import com.dji.sample.common.model.CustomClaim;
+import com.dji.sample.common.util.JwtUtil;
+import com.dji.sample.component.AuthInterceptor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.server.ServerHttpRequest;
+import org.springframework.http.server.ServletServerHttpRequest;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+import org.springframework.web.socket.WebSocketHandler;
+import org.springframework.web.socket.server.support.DefaultHandshakeHandler;
+
+import javax.servlet.http.HttpServletRequest;
+import java.security.Principal;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * @author sean.zhou
+ * @date 2021/11/16
+ * @version 0.1
+ */
+@Slf4j
+@Component
+public class AuthPrincipalHandler extends DefaultHandshakeHandler {
+
+    @Override
+    protected boolean isValidOrigin(ServerHttpRequest request) {
+
+        if (request instanceof ServletServerHttpRequest) {
+            HttpServletRequest servletRequest = ((ServletServerHttpRequest) request).getServletRequest();
+            String token = servletRequest.getParameter(AuthInterceptor.PARAM_TOKEN);
+
+            if (!StringUtils.hasText(token)) {
+                return false;
+            }
+
+            Optional<CustomClaim> customClaim = JwtUtil.parseToken(token);
+            if (customClaim.isEmpty()) {
+                return false;
+            }
+
+            servletRequest.setAttribute(AuthInterceptor.TOKEN_CLAIM, customClaim.get());
+            return true;
+        }
+        return false;
+
+    }
+
+    /**
+     * The principal's name: {workspaceId}/{userType}/{userId}
+     * @param request
+     * @param wsHandler
+     * @param attributes
+     * @return
+     */
+    @Override
+    protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler, Map<String, Object> attributes) {
+        if (request instanceof ServletServerHttpRequest) {
+
+            // get the custom claim
+            CustomClaim claim = (CustomClaim) ((ServletServerHttpRequest) request).getServletRequest()
+                    .getAttribute(AuthInterceptor.TOKEN_CLAIM);
+
+            return () -> claim.getWorkspaceId() + "/" + claim.getUserType() + "/" + claim.getId();
+        }
+        return () -> null;
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/component/websocket/config/ConcurrentWebSocketSession.java b/src/main/java/com/dji/sample/component/websocket/config/ConcurrentWebSocketSession.java
new file mode 100644
index 0000000..cd13365
--- /dev/null
+++ b/src/main/java/com/dji/sample/component/websocket/config/ConcurrentWebSocketSession.java
@@ -0,0 +1,25 @@
+package com.dji.sample.component.websocket.config;
+
+import org.springframework.web.socket.WebSocketSession;
+import org.springframework.web.socket.handler.ConcurrentWebSocketSessionDecorator;
+
+/**
+ * @author sean.zhou
+ * @version 0.1
+ * @date 2021/11/24
+ */
+public class ConcurrentWebSocketSession extends ConcurrentWebSocketSessionDecorator {
+
+    private static final int SEND_BUFFER_SIZE_LIMIT = 1024 * 1024;
+
+    private static final int SEND_TIME_LIMIT = 1000;
+
+    private ConcurrentWebSocketSession(WebSocketSession delegate, int sendTimeLimit, int bufferSizeLimit) {
+        super(delegate, sendTimeLimit, bufferSizeLimit);
+    }
+
+    ConcurrentWebSocketSession(WebSocketSession delegate) {
+        this(delegate, SEND_TIME_LIMIT, SEND_BUFFER_SIZE_LIMIT);
+    }
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/component/websocket/config/WebSocketDefaultFactory.java b/src/main/java/com/dji/sample/component/websocket/config/WebSocketDefaultFactory.java
new file mode 100644
index 0000000..4fdd774
--- /dev/null
+++ b/src/main/java/com/dji/sample/component/websocket/config/WebSocketDefaultFactory.java
@@ -0,0 +1,20 @@
+package com.dji.sample.component.websocket.config;
+
+import org.springframework.stereotype.Component;
+import org.springframework.web.socket.WebSocketHandler;
+import org.springframework.web.socket.handler.WebSocketHandlerDecoratorFactory;
+
+/**
+ *
+ * @author sean.zhou
+ * @date 2021/11/16
+ * @version 0.1
+ */
+@Component
+public class WebSocketDefaultFactory implements WebSocketHandlerDecoratorFactory {
+
+    @Override
+    public WebSocketHandler decorate(WebSocketHandler handler) {
+        return new WebSocketDefaultHandler(handler);
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/component/websocket/config/WebSocketDefaultHandler.java b/src/main/java/com/dji/sample/component/websocket/config/WebSocketDefaultHandler.java
new file mode 100644
index 0000000..d4c7e7a
--- /dev/null
+++ b/src/main/java/com/dji/sample/component/websocket/config/WebSocketDefaultHandler.java
@@ -0,0 +1,60 @@
+package com.dji.sample.component.websocket.config;
+
+import com.dji.sample.component.websocket.model.WebSocketManager;
+import com.dji.sample.component.websocket.service.ISendMessageService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.util.StringUtils;
+import org.springframework.web.socket.CloseStatus;
+import org.springframework.web.socket.WebSocketHandler;
+import org.springframework.web.socket.WebSocketMessage;
+import org.springframework.web.socket.WebSocketSession;
+import org.springframework.web.socket.handler.WebSocketHandlerDecorator;
+
+import java.security.Principal;
+
+/**
+ *
+ * @author sean.zhou
+ * @date 2021/11/16
+ * @version 0.1
+ */
+@Slf4j
+public class WebSocketDefaultHandler extends WebSocketHandlerDecorator {
+
+    @Autowired
+    private ISendMessageService sendMessageService;
+
+    WebSocketDefaultHandler(WebSocketHandler delegate) {
+        super(delegate);
+    }
+
+    @Override
+    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
+        Principal principal = session.getPrincipal();
+        if (StringUtils.hasText(principal.getName())) {
+            WebSocketManager.put(principal.getName(), new ConcurrentWebSocketSession(session));
+            log.debug("{} is connected. ID: {}. WebSocketSession[current count: {}]",
+                    principal.getName(), session.getId(), WebSocketManager.getConnectedCount());
+            return;
+        }
+        session.close();
+    }
+
+    @Override
+    public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
+        Principal principal = session.getPrincipal();
+        if (StringUtils.hasText(principal.getName())) {
+            WebSocketManager.remove(principal.getName(), session.getId());
+            log.debug("{} is disconnected. ID: {}. WebSocketSession[current count: {}]",
+                    principal.getName(), session.getId(), WebSocketManager.getConnectedCount());
+        }
+
+    }
+
+    @Override
+    public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
+        log.debug("received message: {}", message.getPayload());
+    }
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/component/websocket/config/WebSocketMessageConfiguration.java b/src/main/java/com/dji/sample/component/websocket/config/WebSocketMessageConfiguration.java
new file mode 100644
index 0000000..85a696c
--- /dev/null
+++ b/src/main/java/com/dji/sample/component/websocket/config/WebSocketMessageConfiguration.java
@@ -0,0 +1,39 @@
+package com.dji.sample.component.websocket.config;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
+import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
+import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
+import org.springframework.web.socket.config.annotation.WebSocketTransportRegistration;
+
+/**
+ *
+ * @author sean.zhou
+ * @date 2021/11/17
+ * @version 0.1
+ */
+@EnableWebSocketMessageBroker
+@Configuration
+public class WebSocketMessageConfiguration implements WebSocketMessageBrokerConfigurer {
+
+    @Autowired
+    private AuthPrincipalHandler authPrincipalHandler;
+
+    @Autowired
+    private WebSocketDefaultFactory webSocketDefaultFactory;
+
+    @Override
+    public void registerStompEndpoints(StompEndpointRegistry registry) {
+        // Set the WebSocket connection address
+        registry.addEndpoint("/api/v1/ws").setAllowedOriginPatterns("*")
+                .setHandshakeHandler(authPrincipalHandler);
+    }
+
+    @Override
+    public void configureWebSocketTransport(WebSocketTransportRegistration registry) {
+        registry.addDecoratorFactory(webSocketDefaultFactory);
+        registry.setTimeToFirstMessage(60000 * 60 * 24 * 10);
+    }
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/component/websocket/model/BizCodeEnum.java b/src/main/java/com/dji/sample/component/websocket/model/BizCodeEnum.java
new file mode 100644
index 0000000..6ac35c2
--- /dev/null
+++ b/src/main/java/com/dji/sample/component/websocket/model/BizCodeEnum.java
@@ -0,0 +1,37 @@
+package com.dji.sample.component.websocket.model;
+
+/**
+ * @author sean
+ * @version 0.1
+ * @date 2021/11/26
+ */
+public enum BizCodeEnum {
+
+    DEVICE_ONLINE("device_online"),
+
+    DEVICE_OFFLINE("device_offline"),
+
+    DEVICE_UPDATE_TOPO("device_update_topo"),
+
+    DEVICE_OSD("device_osd"),
+
+    GATEWAY_OSD("gateway_osd"),
+
+    MAP_ELEMENT_CREATE("map_element_create"),
+
+    MAP_ELEMENT_UPDATE("map_element_update"),
+
+    MAP_ELEMENT_DELETE("map_element_delete"),
+
+    MAP_GROUP_REFRESH("map_group_refresh");
+
+    private String code;
+
+    BizCodeEnum(String code) {
+        this.code = code;
+    }
+
+    public String getCode() {
+        return code;
+    }
+}
diff --git a/src/main/java/com/dji/sample/component/websocket/model/CustomWebSocketMessage.java b/src/main/java/com/dji/sample/component/websocket/model/CustomWebSocketMessage.java
new file mode 100644
index 0000000..c1d162d
--- /dev/null
+++ b/src/main/java/com/dji/sample/component/websocket/model/CustomWebSocketMessage.java
@@ -0,0 +1,30 @@
+package com.dji.sample.component.websocket.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Builder;
+import lombok.Data;
+
+/**
+ * The format of WebSocket messages that the pilot can receive.
+ * @author sean.zhou
+ * @date 2021/11/17
+ * @version 0.1
+ */
+@Data
+@Builder
+public class CustomWebSocketMessage<T> {
+
+    /**
+     * @see BizCodeEnum
+     * specific value
+     */
+    @JsonProperty("biz_code")
+    private String bizCode;
+
+    @Builder.Default
+    private String version = "1.0";
+
+    private Long timestamp;
+
+    private T data;
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/component/websocket/model/WebSocketManager.java b/src/main/java/com/dji/sample/component/websocket/model/WebSocketManager.java
new file mode 100644
index 0000000..da48ae1
--- /dev/null
+++ b/src/main/java/com/dji/sample/component/websocket/model/WebSocketManager.java
@@ -0,0 +1,112 @@
+package com.dji.sample.component.websocket.model;
+
+import com.dji.sample.component.websocket.config.ConcurrentWebSocketSession;
+import com.dji.sample.manage.model.enums.UserTypeEnum;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Manage all WebSocket connection objects.
+ * @author sean.zhou
+ * @date 2021/11/16
+ * @version 0.1
+ */
+@Slf4j
+public class WebSocketManager {
+
+    private static final ConcurrentHashMap<String,
+            ConcurrentHashMap<String,
+                    ConcurrentHashMap<String, ConcurrentWebSocketSession>>> MANAGER = new ConcurrentHashMap<>(16);
+
+    /**
+     * WebSocket connection from the pilot.
+     */
+    private static final Set<ConcurrentWebSocketSession> PILOT_SESSION = ConcurrentHashMap.newKeySet(16);
+
+    /**
+     * WebSocket connection from the web.
+     */
+    private static final Set<ConcurrentWebSocketSession> WEB_SESSION = ConcurrentHashMap.newKeySet(16);
+
+    public static void put(String key, ConcurrentWebSocketSession val) {
+
+        String[] name = key.split("/");
+        if (name.length != 3) {
+            log.debug("The key is out of format. [{workspaceId}/{userType}/{userId}]");
+            return;
+        }
+
+        ConcurrentHashMap<String, ConcurrentHashMap<String, ConcurrentWebSocketSession>> workspaceSessions =
+                MANAGER.getOrDefault(name[0], new ConcurrentHashMap<>(16));
+
+        ConcurrentHashMap<String, ConcurrentWebSocketSession> userSessions = workspaceSessions.getOrDefault(
+                name[2], new ConcurrentHashMap<>(16));
+        userSessions.put(val.getId(), val);
+        workspaceSessions.put(name[2], userSessions);
+        MANAGER.put(name[0], workspaceSessions);
+
+        getSetByUserType(Integer.valueOf(name[1])).add(val);
+
+    }
+
+    public static void remove(String key, String sessionId) {
+        String[] name = key.split("/");
+        if (name.length != 3) {
+            log.debug("The key is out of format. [{workspaceId}/{userType}/{userId}]");
+            return;
+        }
+        ConcurrentHashMap<String, ConcurrentWebSocketSession> userSession = MANAGER.get(name[0]).get(name[2]);
+
+        Set<ConcurrentWebSocketSession> typeSession = getSetByUserType(Integer.valueOf(name[1]));
+
+        ConcurrentWebSocketSession session = userSession.get(sessionId);
+        typeSession.remove(session);
+        userSession.remove(sessionId);
+    }
+
+    public static int getConnectedCount() {
+        return PILOT_SESSION.size() + WEB_SESSION.size();
+    }
+
+    public static Collection<ConcurrentWebSocketSession> getValueWithWorkspace(String workspaceId) {
+        Set<ConcurrentWebSocketSession> sessions = ConcurrentHashMap.newKeySet();
+
+        MANAGER.get(workspaceId)
+                .forEach((userId, userSessions) -> {
+                    sessions.addAll(userSessions.values());
+                });
+        return sessions;
+    }
+
+    public static Collection<ConcurrentWebSocketSession> getValueWithWorkspaceAndUserType(String workspaceId, Integer userType) {
+        Set<ConcurrentWebSocketSession> sessions = ConcurrentHashMap.newKeySet();
+        Set<ConcurrentWebSocketSession> typeSessions = getSetByUserType(userType);
+
+        MANAGER.getOrDefault(workspaceId, new ConcurrentHashMap<>())
+                .forEach((userId, userSessions) -> {
+                    Collection<ConcurrentWebSocketSession> sessionList = userSessions.values();
+                    if (!sessionList.isEmpty()) {
+                        ConcurrentWebSocketSession session = sessionList.iterator().next();
+                        if (typeSessions.contains(session)) {
+                            sessions.addAll(sessionList);
+                        }
+                    }
+                });
+        return sessions;
+    }
+
+    private static Set<ConcurrentWebSocketSession> getSetByUserType(Integer userType) {
+        if (UserTypeEnum.PILOT.getVal() == userType) {
+            return PILOT_SESSION;
+        }
+
+        if (UserTypeEnum.WEB.getVal() == userType) {
+            return WEB_SESSION;
+        }
+        return new HashSet<>();
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/component/websocket/service/ISendMessageService.java b/src/main/java/com/dji/sample/component/websocket/service/ISendMessageService.java
new file mode 100644
index 0000000..e1d67e7
--- /dev/null
+++ b/src/main/java/com/dji/sample/component/websocket/service/ISendMessageService.java
@@ -0,0 +1,28 @@
+package com.dji.sample.component.websocket.service;
+
+import com.dji.sample.component.websocket.config.ConcurrentWebSocketSession;
+import com.dji.sample.component.websocket.model.CustomWebSocketMessage;
+
+import java.util.Collection;
+
+/**
+ * @author sean.zhou
+ * @date 2021/11/24
+ * @version 0.1
+ */
+public interface ISendMessageService {
+
+    /**
+     * Send a message to the specific connection.
+     * @param session   A WebSocket connection object
+     * @param message   message
+     */
+    void sendMessage(ConcurrentWebSocketSession session, CustomWebSocketMessage message);
+
+    /**
+     * Send the same message to specific connection.
+     * @param sessions  A collection of WebSocket connection objects.
+     * @param message   message
+     */
+    void sendBatch(Collection<ConcurrentWebSocketSession> sessions, CustomWebSocketMessage message);
+}
diff --git a/src/main/java/com/dji/sample/component/websocket/service/impl/SendMessageServiceImpl.java b/src/main/java/com/dji/sample/component/websocket/service/impl/SendMessageServiceImpl.java
new file mode 100644
index 0000000..8ee6131
--- /dev/null
+++ b/src/main/java/com/dji/sample/component/websocket/service/impl/SendMessageServiceImpl.java
@@ -0,0 +1,72 @@
+package com.dji.sample.component.websocket.service.impl;
+
+import com.dji.sample.component.websocket.config.ConcurrentWebSocketSession;
+import com.dji.sample.component.websocket.model.CustomWebSocketMessage;
+import com.dji.sample.component.websocket.service.ISendMessageService;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.web.socket.TextMessage;
+
+import java.io.IOException;
+import java.util.Collection;
+
+/**
+ * @author sean.zhou
+ * @version 0.1
+ * @date 2021/11/24
+ */
+@Service
+@Slf4j
+public class SendMessageServiceImpl implements ISendMessageService {
+
+    @Override
+    public void sendMessage(ConcurrentWebSocketSession session, CustomWebSocketMessage message) {
+        if (session == null) {
+            return;
+        }
+
+        try {
+            if (!session.isOpen()) {
+                session.close();
+                log.debug("This session is closed.");
+                return;
+            }
+
+            ObjectMapper mapper = new ObjectMapper();
+
+            session.sendMessage(new TextMessage(mapper.writeValueAsBytes(message)));
+        } catch (IOException e) {
+            log.info("Failed to publish the message. {}", message.toString());
+            e.printStackTrace();
+        }
+    }
+
+    @Override
+    public void sendBatch(Collection<ConcurrentWebSocketSession> sessions, CustomWebSocketMessage message) {
+        if (sessions.isEmpty()) {
+            return;
+        }
+
+        try {
+
+            ObjectMapper mapper = new ObjectMapper();
+            TextMessage data = new TextMessage(mapper.writeValueAsBytes(message));
+
+            for (ConcurrentWebSocketSession session : sessions) {
+                if (!session.isOpen()) {
+                    session.close();
+                    log.debug("This session is closed.");
+                    return;
+                }
+                session.sendMessage(data);
+
+            }
+
+        } catch (IOException e) {
+            log.info("Failed to publish the message. {}", message.toString());
+
+            e.printStackTrace();
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/configuration/GlobalMVCConfigurer.java b/src/main/java/com/dji/sample/configuration/GlobalMVCConfigurer.java
new file mode 100644
index 0000000..0dfc05c
--- /dev/null
+++ b/src/main/java/com/dji/sample/configuration/GlobalMVCConfigurer.java
@@ -0,0 +1,35 @@
+package com.dji.sample.configuration;
+
+import com.dji.sample.component.AuthInterceptor;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Configuration
+public class GlobalMVCConfigurer implements WebMvcConfigurer {
+
+    @Autowired
+    private AuthInterceptor authInterceptor;
+
+    private static List<String> excludePaths = new ArrayList<>();
+
+    @Value("${url.manage.prefix}")
+    private String managePrefix;
+
+    @Value("${url.manage.version}")
+    private String manageVersion;
+
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        // Exclude the login interface.
+        excludePaths.add(managePrefix + manageVersion + "/login");
+        excludePaths.add(managePrefix + manageVersion + "/token/refresh");
+        // Intercept for all request interfaces.
+        registry.addInterceptor(authInterceptor).addPathPatterns("/**").excludePathPatterns(excludePaths);
+    }
+}
diff --git a/src/main/java/com/dji/sample/configuration/GlobalThreadPoolConfiguration.java b/src/main/java/com/dji/sample/configuration/GlobalThreadPoolConfiguration.java
new file mode 100644
index 0000000..af7a92d
--- /dev/null
+++ b/src/main/java/com/dji/sample/configuration/GlobalThreadPoolConfiguration.java
@@ -0,0 +1,42 @@
+package com.dji.sample.configuration;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.concurrent.*;
+
+/**
+ *
+ * @author sean.zhou
+ * @date 2021/11/10
+ * @version 0.1
+ */
+@Configuration
+public class GlobalThreadPoolConfiguration {
+
+    @Value("${thread.pool.core-pool-size: 10}")
+    private int corePoolSize;
+
+    @Value("${thread.pool.maximum-pool-size: 20}")
+    private int maximumPoolSize;
+
+    @Value("${thread.pool.keep-alive-time: 60}")
+    private long keepAliveTime;
+
+    @Value("${thread.pool.queue.capacity: 1000}")
+    private int capacity;
+
+    /**
+     * A custom thread pool.
+     * @return
+     */
+    @Bean
+    public Executor threadPool() {
+        return new ThreadPoolExecutor(corePoolSize,
+                maximumPoolSize, keepAliveTime,
+                TimeUnit.SECONDS, new LinkedBlockingQueue<>(capacity),
+                Executors.defaultThreadFactory(), new ThreadPoolExecutor.DiscardOldestPolicy());
+    }
+
+}
diff --git a/src/main/java/com/dji/sample/manage/controller/DeviceController.java b/src/main/java/com/dji/sample/manage/controller/DeviceController.java
new file mode 100644
index 0000000..08a245d
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/controller/DeviceController.java
@@ -0,0 +1,138 @@
+package com.dji.sample.manage.controller;
+
+import com.dji.sample.common.model.CustomClaim;
+import com.dji.sample.common.model.ResponseResult;
+import com.dji.sample.component.AuthInterceptor;
+import com.dji.sample.component.mqtt.model.ChannelName;
+import com.dji.sample.component.mqtt.model.CommonTopicReceiver;
+import com.dji.sample.component.mqtt.model.CommonTopicResponse;
+import com.dji.sample.component.websocket.model.BizCodeEnum;
+import com.dji.sample.component.websocket.model.CustomWebSocketMessage;
+import com.dji.sample.component.websocket.model.WebSocketManager;
+import com.dji.sample.component.websocket.service.ISendMessageService;
+import com.dji.sample.manage.model.dto.DeviceDTO;
+import com.dji.sample.manage.model.dto.WorkspaceDTO;
+import com.dji.sample.manage.model.enums.UserTypeEnum;
+import com.dji.sample.manage.model.param.DeviceQueryParam;
+import com.dji.sample.manage.model.receiver.StatusGatewayReceiver;
+import com.dji.sample.manage.service.IDeviceService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.integration.annotation.ServiceActivator;
+import org.springframework.integration.mqtt.support.MqttHeaders;
+import org.springframework.messaging.Message;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.List;
+
+/**
+ * @author sean.zhou
+ * @version 0.1
+ * @date 2021/11/15
+ */
+@RestController
+@Slf4j
+@RequestMapping("${url.manage.prefix}${url.manage.version}/devices")
+public class DeviceController {
+
+    @Autowired
+    private IDeviceService deviceService;
+
+    @Autowired
+    private ISendMessageService sendMessageService;
+
+    /**
+     * Handles the message that the drone goes online.
+     * @param receiver  The drone information is not empty.
+     */
+    @ServiceActivator(inputChannel = ChannelName.INBOUND_STATUS_ONLINE, outputChannel = ChannelName.OUTBOUND)
+    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())
+                            .build());
+
+            // Publish the latest device topology information in the current workspace to the pilot.
+            deviceService.pushDeviceOnlineTopo(WorkspaceDTO.DEFAULT_WORKSPACE_ID,
+                    receiver.getData().getSn(), receiver.getData().getSubDevices().get(0).getSn());
+        }
+    }
+
+    /**
+     * Handles the message that the drone goes offline.
+     * @param receiver  The drone information is empty.
+     */
+    @ServiceActivator(inputChannel = ChannelName.INBOUND_STATUS_OFFLINE, outputChannel = ChannelName.OUTBOUND)
+    public void deviceOffline(CommonTopicReceiver<StatusGatewayReceiver> receiver) {
+
+        boolean offline = deviceService.deviceOffline(receiver.getData().getSn());
+        if (offline) {
+            // Notify pilot that the device is offline successfully.
+            deviceService.publishStatusReply(receiver.getData().getSn(),
+                    CommonTopicResponse.builder()
+                            .tid(receiver.getTid())
+                            .bid(receiver.getBid())
+                            .build());
+
+            // Publish the latest device topology information in the current workspace to the pilot.
+            deviceService.pushDeviceOfflineTopo(WorkspaceDTO.DEFAULT_WORKSPACE_ID, receiver.getData().getSn());
+        }
+    }
+
+    /**
+     * Get the topology list of all devices in the current user workspace.
+     * @param request
+     * @return
+     */
+    @GetMapping("/devices")
+    public ResponseResult<List<DeviceDTO>> getDevices(HttpServletRequest request) {
+        // Get information about the current user.
+        CustomClaim claim = (CustomClaim)request.getAttribute(AuthInterceptor.TOKEN_CLAIM);
+        String workspaceId = claim.getWorkspaceId();
+        // Get information about the devices in the current user's workspace.
+        List<DeviceDTO> devicesList = deviceService.getDevicesTopoForWeb(workspaceId);
+
+        return ResponseResult.success(devicesList);
+    }
+
+    @ServiceActivator(inputChannel = ChannelName.INBOUND_OSD)
+    public void osdRealTime(Message<?> message) {
+        String topic = message.getHeaders().get(MqttHeaders.RECEIVED_TOPIC).toString();
+        byte[] payload = (byte[])message.getPayload();
+        deviceService.handleOSD(topic, payload);
+    }
+
+    /**
+     * Handles the payloads data of the drone.
+     * @param deviceSn  drone's sn
+     */
+    @ServiceActivator(inputChannel = ChannelName.INBOUND_STATE_PAYLOAD_UPDATE)
+    public void pushWebSocketDevices(String deviceSn) {
+        List<DeviceDTO> devicesList = deviceService.getDevicesByParams(
+                DeviceQueryParam.builder()
+                        .deviceSn(deviceSn)
+                        .build());
+        // Get drone information based on the sn of the drone. The sn of the drone is unique.
+        DeviceDTO device = devicesList.get(0);
+        // Set the remote controller and payloads information of the drone.
+        deviceService.spliceDeviceTopo(device);
+
+        CustomWebSocketMessage wsMessage = CustomWebSocketMessage.builder()
+                .timestamp(System.currentTimeMillis())
+                .bizCode(BizCodeEnum.DEVICE_UPDATE_TOPO.getCode())
+                .data(device)
+                .build();
+        // Update the topology of the drone via WebSocket notifications to the web side.
+        sendMessageService.sendBatch(WebSocketManager
+                .getValueWithWorkspaceAndUserType(
+                        device.getWorkspaceId(), UserTypeEnum.WEB.getVal()),
+                wsMessage);
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/manage/controller/DevicePayloadController.java b/src/main/java/com/dji/sample/manage/controller/DevicePayloadController.java
new file mode 100644
index 0000000..0c0ba2c
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/controller/DevicePayloadController.java
@@ -0,0 +1,66 @@
+package com.dji.sample.manage.controller;
+
+import com.dji.sample.component.mqtt.model.ChannelName;
+import com.dji.sample.manage.model.receiver.DeviceBasicReceiver;
+import com.dji.sample.manage.model.receiver.DevicePayloadReceiver;
+import com.dji.sample.manage.service.IDevicePayloadService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.integration.annotation.ServiceActivator;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+/**
+ * @author sean.zhou
+ * @date 2021/11/19
+ * @version 0.1
+ */
+@RestController
+@Slf4j
+public class DevicePayloadController {
+
+    @Autowired
+    private IDevicePayloadService devicePayloadService;
+
+    /**
+     * Handles the data for the payload messages in the state topic.
+     * @param payloadsList  List of payload information.
+     * @return  drone's sn
+     */
+    @ServiceActivator(inputChannel = ChannelName.INBOUND_STATE_PAYLOAD,
+            outputChannel = ChannelName.INBOUND_STATE_PAYLOAD_UPDATE)
+    public String statePayload(List<DevicePayloadReceiver> payloadsList) {
+        // Delete all payload information for the drone based on the drone's sn.
+        devicePayloadService.deletePayloadsByDeviceSn(List.of(payloadsList.get(0).getDeviceSn()));
+
+        // Save the new payload information.
+        devicePayloadService.savePayloadDTOs(payloadsList);
+
+        log.debug("The result of saving the payload is successful.");
+
+        return payloadsList.get(0).getDeviceSn();
+    }
+
+    /**
+     * 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
+     * @return  drone's sn
+     */
+    @ServiceActivator(inputChannel = ChannelName.INBOUND_STATE_BASIC,
+            outputChannel = ChannelName.INBOUND_STATE_PAYLOAD_UPDATE)
+    public String stateBasic(DeviceBasicReceiver deviceBasic) {
+        // Delete all payload information for the drone based on the drone's sn.
+        devicePayloadService.deletePayloadsByDeviceSn(List.of(deviceBasic.getDeviceSn()));
+
+        // Save the new payload information.
+        boolean isSave = devicePayloadService.savePayloadDTOs(deviceBasic.getPayloads());
+
+        log.debug("The result of saving the payloads is {}.", isSave);
+
+        return deviceBasic.getDeviceSn();
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/manage/controller/LiveStreamController.java b/src/main/java/com/dji/sample/manage/controller/LiveStreamController.java
new file mode 100644
index 0000000..ef89f34
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/controller/LiveStreamController.java
@@ -0,0 +1,113 @@
+package com.dji.sample.manage.controller;
+
+import com.dji.sample.common.model.CustomClaim;
+import com.dji.sample.common.model.ResponseResult;
+import com.dji.sample.component.mqtt.model.ChannelName;
+import com.dji.sample.component.mqtt.model.CommonTopicReceiver;
+import com.dji.sample.manage.model.Chan;
+import com.dji.sample.manage.model.dto.CapacityDeviceDTO;
+import com.dji.sample.manage.model.dto.LiveTypeDTO;
+import com.dji.sample.manage.model.receiver.CapacityDeviceReceiver;
+import com.dji.sample.manage.model.receiver.ServiceReplyReceiver;
+import com.dji.sample.manage.service.ILiveStreamService;
+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.annotation.ServiceActivator;
+import org.springframework.messaging.Message;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletRequest;
+import java.io.IOException;
+import java.util.List;
+
+import static com.dji.sample.component.AuthInterceptor.TOKEN_CLAIM;
+
+/**
+ * @author sean.zhou
+ * @version 0.1
+ * @date 2021/11/19
+ */
+
+@RestController
+@Slf4j
+@RequestMapping("${url.manage.prefix}${url.manage.version}/live")
+public class LiveStreamController {
+
+    @Autowired
+    private ILiveStreamService liveStreamService;
+
+    /**
+     * Analyze the live streaming capabilities of drones.
+     * This data is necessary if drones are required for live streaming.
+     * @param device    the capacity of drone
+     */
+    @ServiceActivator(inputChannel = ChannelName.INBOUND_STATE_CAPACITY)
+    public void stateCapacity(CapacityDeviceReceiver device) {
+        boolean parseCapacity = liveStreamService.saveLiveCapacity(device);
+        log.debug("The result of parsing the live capacity is {}.", parseCapacity);
+    }
+
+    /**
+     * Get live capability data of all drones in the current user's workspace from the database.
+     * @param request
+     * @return  live capability
+     */
+    @GetMapping("/capacity")
+    public ResponseResult<List<CapacityDeviceDTO>> getLiveCapacity(HttpServletRequest request) {
+        // Get information about the current user.
+        CustomClaim customClaim = (CustomClaim)request.getAttribute(TOKEN_CLAIM);
+
+        List<CapacityDeviceDTO> liveCapacity = liveStreamService.getLiveCapacity(customClaim.getWorkspaceId());
+
+        return ResponseResult.success(liveCapacity);
+    }
+
+    /**
+     * Live streaming according to the parameters passed in from the web side.
+     * @param liveParam Live streaming parameters.
+     * @return
+     */
+    @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
+     */
+    @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
+     */
+    @PostMapping("/streams/update")
+    public ResponseResult liveSetQuality(@RequestBody LiveTypeDTO liveParam) {
+        return liveStreamService.liveSetQuality(liveParam);
+    }
+
+    /**
+     * Handle the reply message from the pilot side to the on-demand video.
+     * @param message   reply message
+     * @throws IOException
+     */
+    @ServiceActivator(inputChannel = ChannelName.INBOUND_SERVICE_REPLY)
+    public void serviceReply(Message<?> message) throws IOException {
+        byte[] payload = (byte[])message.getPayload();
+        ObjectMapper mapper = new ObjectMapper();
+        CommonTopicReceiver<ServiceReplyReceiver> receiver = mapper.readValue(payload,
+                new TypeReference<CommonTopicReceiver<ServiceReplyReceiver>>() {
+        });
+        Chan<CommonTopicReceiver> chan = Chan.getInstance();
+        // Put the message to the chan object.
+        chan.put(receiver);
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/manage/controller/LoginController.java b/src/main/java/com/dji/sample/manage/controller/LoginController.java
new file mode 100644
index 0000000..b0cacb4
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/controller/LoginController.java
@@ -0,0 +1,47 @@
+package com.dji.sample.manage.controller;
+
+import com.dji.sample.common.error.CommonErrorEnum;
+import com.dji.sample.common.model.ResponseResult;
+import com.dji.sample.manage.model.dto.UserDTO;
+import com.dji.sample.manage.model.dto.UserLoginDTO;
+import com.dji.sample.manage.service.IUserService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.util.Optional;
+
+import static com.dji.sample.component.AuthInterceptor.PARAM_TOKEN;
+
+@RestController
+@RequestMapping("${url.manage.prefix}${url.manage.version}")
+public class LoginController {
+
+    @Autowired
+    private IUserService userService;
+
+    @PostMapping("/login")
+    public ResponseResult login(@RequestBody UserLoginDTO loginDTO) {
+        String username = loginDTO.getUsername();
+        String password = loginDTO.getPassword();
+        return userService.userLogin(username, password);
+    }
+
+    @PostMapping("/token/refresh")
+    public ResponseResult refreshToken(HttpServletRequest request, HttpServletResponse response) {
+        String token = request.getHeader(PARAM_TOKEN);
+        Optional<UserDTO> user = userService.refreshToken(token);
+
+        if (user.isEmpty()) {
+            response.setStatus(HttpStatus.UNAUTHORIZED.value());
+            return ResponseResult.error(CommonErrorEnum.NO_TOKEN.getErrorMsg());
+        }
+
+        return ResponseResult.success(user);
+    }
+}
diff --git a/src/main/java/com/dji/sample/manage/controller/TopologyController.java b/src/main/java/com/dji/sample/manage/controller/TopologyController.java
new file mode 100644
index 0000000..3f2b80d
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/controller/TopologyController.java
@@ -0,0 +1,40 @@
+package com.dji.sample.manage.controller;
+
+import com.dji.sample.common.model.ResponseResult;
+import com.dji.sample.manage.model.dto.TopologyDTO;
+import com.dji.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;
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/12/8
+ */
+@RestController
+@RequestMapping("${url.manage.prefix}${url.manage.version}/workspaces")
+public class TopologyController {
+
+    @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/src/main/java/com/dji/sample/manage/controller/UserController.java b/src/main/java/com/dji/sample/manage/controller/UserController.java
new file mode 100644
index 0000000..06cfb4d
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/controller/UserController.java
@@ -0,0 +1,29 @@
+package com.dji.sample.manage.controller;
+
+import com.dji.sample.common.model.CustomClaim;
+import com.dji.sample.common.model.ResponseResult;
+import com.dji.sample.manage.service.IUserService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.servlet.http.HttpServletRequest;
+
+import static com.dji.sample.component.AuthInterceptor.TOKEN_CLAIM;
+
+
+@RestController
+@RequestMapping("${url.manage.prefix}${url.manage.version}/users")
+public class UserController {
+
+    @Autowired
+    private IUserService userService;
+
+    @GetMapping("/current")
+    public ResponseResult getCurrentUserInfo(HttpServletRequest request) {
+        CustomClaim customClaim = (CustomClaim)request.getAttribute(TOKEN_CLAIM);
+        return userService.getUserByUsername(customClaim.getUsername(), customClaim.getWorkspaceId());
+    }
+
+}
diff --git a/src/main/java/com/dji/sample/manage/controller/WorkspaceController.java b/src/main/java/com/dji/sample/manage/controller/WorkspaceController.java
new file mode 100644
index 0000000..e2f5f43
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/controller/WorkspaceController.java
@@ -0,0 +1,41 @@
+package com.dji.sample.manage.controller;
+
+import com.dji.sample.common.model.CustomClaim;
+import com.dji.sample.common.model.ResponseResult;
+import com.dji.sample.manage.model.dto.WorkspaceDTO;
+import com.dji.sample.manage.service.IWorkspaceService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.Optional;
+
+import static com.dji.sample.component.AuthInterceptor.TOKEN_CLAIM;
+
+/**
+ * @author sean.zhou
+ * @version 0.1
+ * @date 2021/11/23
+ */
+@RestController
+@RequestMapping("${url.manage.prefix}${url.manage.version}/workspaces")
+public class WorkspaceController {
+
+    @Autowired
+    private IWorkspaceService workspaceService;
+
+    /**
+     * Gets information about the workspace that the current user is in.
+     * @param request
+     * @return
+     */
+    @GetMapping("/current")
+    public ResponseResult getCurrentWorkspace(HttpServletRequest request) {
+        CustomClaim customClaim = (CustomClaim)request.getAttribute(TOKEN_CLAIM);
+        Optional<WorkspaceDTO> workspaceOpt = workspaceService.getWorkspaceByWorkspaceId(customClaim.getWorkspaceId());
+
+        return workspaceOpt.isEmpty() ? ResponseResult.error() : ResponseResult.success(workspaceOpt.get());
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/manage/dao/ICameraVideoMapper.java b/src/main/java/com/dji/sample/manage/dao/ICameraVideoMapper.java
new file mode 100644
index 0000000..0784954
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/dao/ICameraVideoMapper.java
@@ -0,0 +1,12 @@
+package com.dji.sample.manage.dao;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.dji.sample.manage.model.entity.CameraVideoEntity;
+
+/**
+ * @author sean.zhou
+ * @version 0.1
+ * @date 2021/11/19
+ */
+public interface ICameraVideoMapper extends BaseMapper<CameraVideoEntity> {
+}
diff --git a/src/main/java/com/dji/sample/manage/dao/ICapacityCameraMapper.java b/src/main/java/com/dji/sample/manage/dao/ICapacityCameraMapper.java
new file mode 100644
index 0000000..777d6d0
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/dao/ICapacityCameraMapper.java
@@ -0,0 +1,12 @@
+package com.dji.sample.manage.dao;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.dji.sample.manage.model.entity.CapacityCameraEntity;
+
+/**
+ * @author sean.zhou
+ * @date 2021/11/19
+ * @version 0.1
+ */
+public interface ICapacityCameraMapper extends BaseMapper<CapacityCameraEntity> {
+}
diff --git a/src/main/java/com/dji/sample/manage/dao/IDeviceDictionaryMapper.java b/src/main/java/com/dji/sample/manage/dao/IDeviceDictionaryMapper.java
new file mode 100644
index 0000000..cce8f7e
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/dao/IDeviceDictionaryMapper.java
@@ -0,0 +1,13 @@
+package com.dji.sample.manage.dao;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.dji.sample.manage.model.entity.DeviceDictionaryEntity;
+
+/**
+ *
+ * @author sean.zhou
+ * @date 2021/11/15
+ * @version 0.1
+ */
+public interface IDeviceDictionaryMapper extends BaseMapper<DeviceDictionaryEntity> {
+}
diff --git a/src/main/java/com/dji/sample/manage/dao/IDeviceMapper.java b/src/main/java/com/dji/sample/manage/dao/IDeviceMapper.java
new file mode 100644
index 0000000..758d8e5
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/dao/IDeviceMapper.java
@@ -0,0 +1,14 @@
+package com.dji.sample.manage.dao;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.dji.sample.manage.model.entity.DeviceEntity;
+
+/**
+ *
+ * @author sean.zhou
+ * @date 2021/11/10
+ * @version 0.1
+ */
+public interface IDeviceMapper extends BaseMapper<DeviceEntity> {
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/manage/dao/IDevicePayloadMapper.java b/src/main/java/com/dji/sample/manage/dao/IDevicePayloadMapper.java
new file mode 100644
index 0000000..f90231c
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/dao/IDevicePayloadMapper.java
@@ -0,0 +1,12 @@
+package com.dji.sample.manage.dao;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.dji.sample.manage.model.entity.DevicePayloadEntity;
+
+/**
+ * @author sean.zhou
+ * @date 2021/11/19
+ * @version 0.1
+ */
+public interface IDevicePayloadMapper extends BaseMapper<DevicePayloadEntity> {
+}
diff --git a/src/main/java/com/dji/sample/manage/dao/IUserMapper.java b/src/main/java/com/dji/sample/manage/dao/IUserMapper.java
new file mode 100644
index 0000000..a6179ae
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/dao/IUserMapper.java
@@ -0,0 +1,8 @@
+package com.dji.sample.manage.dao;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.dji.sample.manage.model.entity.UserEntity;
+
+public interface IUserMapper extends BaseMapper<UserEntity> {
+
+}
diff --git a/src/main/java/com/dji/sample/manage/dao/IWorkspaceMapper.java b/src/main/java/com/dji/sample/manage/dao/IWorkspaceMapper.java
new file mode 100644
index 0000000..79bc240
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/dao/IWorkspaceMapper.java
@@ -0,0 +1,8 @@
+package com.dji.sample.manage.dao;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.dji.sample.manage.model.entity.WorkspaceEntity;
+
+public interface IWorkspaceMapper extends BaseMapper<WorkspaceEntity> {
+
+}
diff --git a/src/main/java/com/dji/sample/manage/handler/AbstractStateTopicHandler.java b/src/main/java/com/dji/sample/manage/handler/AbstractStateTopicHandler.java
new file mode 100644
index 0000000..5c51c42
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/handler/AbstractStateTopicHandler.java
@@ -0,0 +1,35 @@
+package com.dji.sample.manage.handler;
+
+import com.dji.sample.component.mqtt.model.TopicStateReceiver;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+
+/**
+ * @author sean
+ * @version 0.3
+ * @date 2022/2/21
+ */
+public abstract class AbstractStateTopicHandler {
+
+    protected AbstractStateTopicHandler handler;
+    protected static ObjectMapper mapper = new ObjectMapper();;
+
+    protected AbstractStateTopicHandler(AbstractStateTopicHandler handler){
+        this.handler = handler;
+        mapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
+        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+    }
+
+    /**
+     * Passing dataNode data, using different processing methods depending on the data selection.
+     * @param dataNode
+     * @param stateReceiver
+     * @param sn
+     * @return
+     * @throws JsonProcessingException
+     */
+    public abstract TopicStateReceiver handleState(JsonNode dataNode, TopicStateReceiver stateReceiver, String sn) throws JsonProcessingException;
+}
diff --git a/src/main/java/com/dji/sample/manage/handler/StateDefaultHandler.java b/src/main/java/com/dji/sample/manage/handler/StateDefaultHandler.java
new file mode 100644
index 0000000..42140b6
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/handler/StateDefaultHandler.java
@@ -0,0 +1,25 @@
+package com.dji.sample.manage.handler;
+
+import com.dji.sample.component.mqtt.model.TopicStateReceiver;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import org.springframework.stereotype.Service;
+
+/**
+ * @author sean
+ * @version 0.3
+ * @date 2022/3/21
+ */
+@Service
+public class StateDefaultHandler extends AbstractStateTopicHandler {
+
+    protected StateDefaultHandler() {
+        super(null);
+    }
+
+    @Override
+    public TopicStateReceiver handleState(JsonNode dataNode, TopicStateReceiver stateReceiver, String sn) throws JsonProcessingException {
+        // If no suitable handler is found for the data, it is not processed.
+        return stateReceiver;
+    }
+}
diff --git a/src/main/java/com/dji/sample/manage/handler/StateDeviceBasicHandler.java b/src/main/java/com/dji/sample/manage/handler/StateDeviceBasicHandler.java
new file mode 100644
index 0000000..cb773eb
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/handler/StateDeviceBasicHandler.java
@@ -0,0 +1,34 @@
+package com.dji.sample.manage.handler;
+
+import com.dji.sample.component.mqtt.model.TopicStateReceiver;
+import com.dji.sample.manage.model.receiver.DeviceBasicReceiver;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.stereotype.Service;
+
+/**
+ * @author sean
+ * @version 0.3
+ * @date 2022/2/21
+ */
+@Service
+public class StateDeviceBasicHandler extends AbstractStateTopicHandler {
+
+    public StateDeviceBasicHandler(@Autowired @Qualifier("statePayloadHandler") AbstractStateTopicHandler handler) {
+        super(handler);
+    }
+
+    @Override
+    public TopicStateReceiver handleState(JsonNode dataNode, TopicStateReceiver stateReceiver, String sn) throws JsonProcessingException {
+        // handle device basic data
+        if (dataNode.size() != 1) {
+            DeviceBasicReceiver data = mapper.treeToValue(dataNode, DeviceBasicReceiver.class);
+            data.setDeviceSn(sn);
+            stateReceiver.setData(data);
+            return stateReceiver;
+        }
+        return handler.handleState(dataNode, stateReceiver, sn);
+    }
+}
diff --git a/src/main/java/com/dji/sample/manage/handler/StateLiveCapacityHandler.java b/src/main/java/com/dji/sample/manage/handler/StateLiveCapacityHandler.java
new file mode 100644
index 0000000..9e79d6d
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/handler/StateLiveCapacityHandler.java
@@ -0,0 +1,39 @@
+package com.dji.sample.manage.handler;
+
+import com.dji.sample.component.mqtt.model.TopicStateReceiver;
+import com.dji.sample.manage.model.enums.StateDataEnum;
+import com.dji.sample.manage.model.receiver.CapacityDeviceReceiver;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.stereotype.Service;
+
+/**
+ * @author sean
+ * @version 0.3
+ * @date 2022/2/21
+ */
+@Service
+public class StateLiveCapacityHandler extends AbstractStateTopicHandler {
+
+    private static final String DEVICE_LIST = "device_list";
+
+    protected StateLiveCapacityHandler(@Autowired @Qualifier("stateDefaultHandler") AbstractStateTopicHandler handler) {
+        super(handler);
+    }
+
+    @Override
+    public TopicStateReceiver handleState(JsonNode dataNode, TopicStateReceiver stateReceiver, String sn) throws JsonProcessingException {
+        String name = dataNode.fieldNames().next();
+        JsonNode childNode = dataNode.findPath(name);
+        // Determine if it is live capacity data based on name.
+        if (name.equals(StateDataEnum.LIVE_CAPACITY.getDesc())) {
+            JsonNode deviceNode = childNode.findPath(DEVICE_LIST);
+            stateReceiver.setData(
+                    mapper.treeToValue(deviceNode.get(0), CapacityDeviceReceiver.class));
+            return stateReceiver;
+        }
+        return handler.handleState(dataNode, stateReceiver, sn);
+    }
+}
diff --git a/src/main/java/com/dji/sample/manage/handler/StatePayloadHandler.java b/src/main/java/com/dji/sample/manage/handler/StatePayloadHandler.java
new file mode 100644
index 0000000..a14e0d0
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/handler/StatePayloadHandler.java
@@ -0,0 +1,55 @@
+package com.dji.sample.manage.handler;
+
+import com.dji.sample.component.mqtt.model.TopicStateReceiver;
+import com.dji.sample.manage.model.enums.StateDataEnum;
+import com.dji.sample.manage.model.receiver.DevicePayloadReceiver;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * @author sean
+ * @version 0.3
+ * @date 2022/2/21
+ */
+@Service
+public class StatePayloadHandler extends AbstractStateTopicHandler {
+
+    protected StatePayloadHandler(@Autowired @Qualifier("stateLiveCapacityHandler") AbstractStateTopicHandler handler) {
+        super(handler);
+    }
+
+    @Override
+    public TopicStateReceiver handleState(JsonNode dataNode, TopicStateReceiver stateReceiver, String sn) throws JsonProcessingException {
+        String name = dataNode.fieldNames().next();
+        JsonNode childNode = dataNode.findPath(name);
+        // Determine if it is payload data based on name.
+        if (name.equals(StateDataEnum.PAYLOADS.getDesc())) {
+            List<DevicePayloadReceiver> payloadsList = new ArrayList<>();
+
+            Iterator<JsonNode> payloadsNode = childNode.elements();
+            while (payloadsNode.hasNext()) {
+                DevicePayloadReceiver payloadReceiver = mapper.treeToValue(
+                        payloadsNode.next(), DevicePayloadReceiver.class);
+                payloadReceiver.setDeviceSn(sn);
+                payloadsList.add(payloadReceiver);
+            }
+
+            if (payloadsList.isEmpty()) {
+                DevicePayloadReceiver payloadReceiver = new DevicePayloadReceiver();
+                payloadReceiver.setDeviceSn(sn);
+                payloadsList.add(payloadReceiver);
+            }
+
+            stateReceiver.setData(payloadsList);
+            return stateReceiver;
+        }
+        return handler.handleState(dataNode, stateReceiver, sn);
+    }
+}
diff --git a/src/main/java/com/dji/sample/manage/handler/StateRouter.java b/src/main/java/com/dji/sample/manage/handler/StateRouter.java
new file mode 100644
index 0000000..3fe3404
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/handler/StateRouter.java
@@ -0,0 +1,92 @@
+package com.dji.sample.manage.handler;
+
+import com.dji.sample.component.mqtt.model.ChannelName;
+import com.dji.sample.component.mqtt.model.TopicStateReceiver;
+import com.dji.sample.manage.model.receiver.CapacityDeviceReceiver;
+import com.dji.sample.manage.model.receiver.DeviceBasicReceiver;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.integration.annotation.MessageEndpoint;
+import org.springframework.integration.annotation.Router;
+import org.springframework.integration.annotation.ServiceActivator;
+import org.springframework.integration.mqtt.support.MqttHeaders;
+import org.springframework.integration.router.MessageRouter;
+import org.springframework.integration.router.PayloadTypeRouter;
+import org.springframework.messaging.Message;
+
+import javax.annotation.Resource;
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+import static com.dji.sample.component.mqtt.model.TopicConst.*;
+
+/**
+ *
+ * @author sean.zhou
+ * @date 2021/11/17
+ * @version 0.1
+ */
+@MessageEndpoint
+@Slf4j
+@Configuration
+public class StateRouter {
+
+    @Resource(name = "stateDeviceBasicHandler")
+    private AbstractStateTopicHandler handler;
+
+    /**
+     * Handles the routing of state topic messages. Depending on the data, it is assigned to different channels for handling.
+     * @param message
+     * @return
+     * @throws IOException
+     */
+    @ServiceActivator(inputChannel = ChannelName.INBOUND_STATE, outputChannel = ChannelName.INBOUND_STATE_SPLITTER)
+    public TopicStateReceiver<?> resolveStateData(Message<?> message) throws IOException {
+        byte[] payload = (byte[])message.getPayload();
+        String topic = message.getHeaders().get(MqttHeaders.RECEIVED_TOPIC).toString();
+
+        ObjectMapper mapper = new ObjectMapper();
+        mapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
+        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+
+        TopicStateReceiver stateReceiver = mapper.readValue(payload, TopicStateReceiver.class);
+        // Get the sn of the topic source.
+        String from = topic.substring((THING_MODEL_PRE + PRODUCT).length(),
+                topic.indexOf(STATE_SUF));
+
+        try {
+            JsonNode dataNode = mapper.readTree(payload).findPath("data");
+            return handler.handleState(dataNode, stateReceiver, from);
+
+        } catch (UnrecognizedPropertyException e) {
+            log.info("The {} data is not processed.", e.getPropertyName());
+        }
+        return stateReceiver;
+    }
+
+    @Bean
+    @Router(inputChannel = ChannelName.INBOUND_STATE_ROUTER)
+    public MessageRouter resolveStateRouter() {
+        PayloadTypeRouter router = new PayloadTypeRouter();
+        // // Channel mapping for basic data.
+        router.setChannelMapping(DeviceBasicReceiver.class.getName(),
+                ChannelName.INBOUND_STATE_BASIC);
+        // Channel mapping for live streaming capabilities.
+        router.setChannelMapping(CapacityDeviceReceiver.class.getName(),
+                ChannelName.INBOUND_STATE_CAPACITY);
+        // Channel mapping for payload data.
+        router.setChannelMapping(List.class.getName(),
+                ChannelName.INBOUND_STATE_PAYLOAD);
+        router.setChannelMapping(Map.class.getName(),
+                ChannelName.DEFAULT);
+        return router;
+    }
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/manage/handler/StateSplitter.java b/src/main/java/com/dji/sample/manage/handler/StateSplitter.java
new file mode 100644
index 0000000..e47fd53
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/handler/StateSplitter.java
@@ -0,0 +1,59 @@
+package com.dji.sample.manage.handler;
+
+import com.dji.sample.component.mqtt.model.ChannelName;
+import com.dji.sample.component.mqtt.model.TopicStateReceiver;
+import com.dji.sample.manage.model.receiver.DevicePayloadReceiver;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.integration.annotation.MessageEndpoint;
+import org.springframework.integration.annotation.Splitter;
+import org.springframework.integration.dsl.IntegrationFlow;
+import org.springframework.integration.dsl.IntegrationFlows;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+/**
+ *
+ * @author sean.zhou
+ * @date 2021/11/17
+ * @version 0.1
+ */
+@MessageEndpoint
+@Configuration
+public class StateSplitter {
+
+    /**
+     * Split the state message data to different channels for handling according to their different types.
+     * @param receiver state message
+     * @return
+     */
+    @Splitter(inputChannel = ChannelName.INBOUND_STATE_SPLITTER, outputChannel = ChannelName.INBOUND_STATE_ROUTER)
+    public Collection<Object> splitState(TopicStateReceiver receiver) {
+        ArrayList<Object> type = new ArrayList<>();
+        type.add(receiver.getData());
+        return type;
+    }
+
+    /**
+     * Split according to the different types in the list.
+     * @return
+     */
+    @Bean
+    public IntegrationFlow splitList() {
+        return IntegrationFlows
+                .from(ChannelName.INBOUND_STATE_LIST)
+                .split()
+                .<Object, String> route(dataType -> {
+                    Class<?> clazz = dataType.getClass();
+                    if (DevicePayloadReceiver.class.isAssignableFrom(clazz)) {
+                        return ChannelName.INBOUND_STATE_PAYLOAD;
+                    }
+                    return null;
+                }, mapping -> {
+                    mapping.channelMapping(ChannelName.INBOUND_STATE_PAYLOAD,
+                            ChannelName.INBOUND_STATE_PAYLOAD);
+                })
+                .get();
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/manage/handler/StatusRouter.java b/src/main/java/com/dji/sample/manage/handler/StatusRouter.java
new file mode 100644
index 0000000..3fa6bc8
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/handler/StatusRouter.java
@@ -0,0 +1,70 @@
+package com.dji.sample.manage.handler;
+
+import com.dji.sample.component.mqtt.model.ChannelName;
+import com.dji.sample.component.mqtt.model.CommonTopicReceiver;
+import com.dji.sample.manage.model.enums.DeviceDomainEnum;
+import com.dji.sample.manage.model.receiver.StatusGatewayReceiver;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+import org.springframework.integration.annotation.MessageEndpoint;
+import org.springframework.integration.annotation.Router;
+import org.springframework.integration.annotation.ServiceActivator;
+import org.springframework.integration.mqtt.support.MqttHeaders;
+import org.springframework.messaging.Message;
+import org.springframework.util.CollectionUtils;
+
+import static com.dji.sample.component.mqtt.model.TopicConst.*;
+
+/**
+ *
+ * @author sean.zhou
+ * @date 2021/11/12
+ * @version 0.1
+ */
+@MessageEndpoint
+public class StatusRouter {
+
+    /**
+     * Converts the status data sent by the gateway device into an object.
+     * @param message
+     * @return
+     */
+    @ServiceActivator(inputChannel = ChannelName.INBOUND_STATUS, outputChannel = ChannelName.INBOUND_STATUS_ROUTER)
+    public CommonTopicReceiver<StatusGatewayReceiver> resolveStatus(Message<?> message) {
+
+        CommonTopicReceiver<StatusGatewayReceiver> statusReceiver = new CommonTopicReceiver<>();
+        ObjectMapper mapper = new ObjectMapper();
+        mapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
+        try {
+            statusReceiver = mapper.readValue(
+                    (byte[])message.getPayload(),
+                    new TypeReference<CommonTopicReceiver<StatusGatewayReceiver>>() {});
+
+            String topic = message.getHeaders().get(MqttHeaders.RECEIVED_TOPIC).toString();
+
+            // set gateway's domain
+            statusReceiver.getData().setDomain(DeviceDomainEnum.GATEWAY.getVal());
+            // set gateway's sn
+            statusReceiver.getData().setSn(
+                    topic.substring((BASIC_PRE + PRODUCT).length(),
+                            topic.indexOf(STATUS_SUF)));
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return statusReceiver;
+    }
+
+    /**
+     * Handles the routing of status topic messages. Depending on the data, it is assigned to different channels for handling.
+     * @param receiver
+     * @return
+     */
+    @Router(inputChannel = ChannelName.INBOUND_STATUS_ROUTER)
+    public String resolveStatusRouter(CommonTopicReceiver<StatusGatewayReceiver> receiver) {
+        // Determine whether the drone is online or offline according to whether the data of the sub-device is empty.
+        return CollectionUtils.isEmpty(receiver.getData().getSubDevices()) ?
+                ChannelName.INBOUND_STATUS_OFFLINE : ChannelName.INBOUND_STATUS_ONLINE;
+    }
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/manage/model/Chan.java b/src/main/java/com/dji/sample/manage/model/Chan.java
new file mode 100644
index 0000000..5aa7961
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/model/Chan.java
@@ -0,0 +1,45 @@
+package com.dji.sample.manage.model;
+
+import java.util.concurrent.locks.LockSupport;
+
+/**
+ * The demo is only for functional closure, which is not recommended.
+ * @author sean.zhou
+ * @date 2021/11/22
+ * @version 0.1
+ */
+public class Chan<T> {
+
+    private static final long THREAD_WAIT_TIME = 1000_000 * 500;
+
+    private volatile T data;
+
+    private volatile Thread t;
+
+    private Chan () {
+
+    }
+
+    public static Chan getInstance() {
+        return ChanSingleton.INSTANCE;
+    }
+
+    public T get(Object blocker) {
+        this.t = Thread.currentThread();
+        LockSupport.parkNanos(blocker, THREAD_WAIT_TIME);
+        this.t = null;
+        return data;
+    }
+
+    public void put(T data) {
+        this.data = data;
+        if (t == null) {
+            return;
+        }
+        LockSupport.unpark(t);
+    }
+
+    private static class ChanSingleton {
+        private static final Chan<?> INSTANCE = new Chan<>();
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/manage/model/DeviceStatusManager.java b/src/main/java/com/dji/sample/manage/model/DeviceStatusManager.java
new file mode 100644
index 0000000..2bc6960
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/model/DeviceStatusManager.java
@@ -0,0 +1,20 @@
+package com.dji.sample.manage.model;
+
+import java.time.LocalDateTime;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * The demo is only for functional closure, which is not recommended,
+ * and it is recommended to use caching for handling.
+ * @author sean.zhou
+ * @version 0.1
+ * @date 2021/11/25
+ */
+public class DeviceStatusManager {
+
+    public static final ConcurrentHashMap<String, LocalDateTime> STATUS_MANAGER =
+            new ConcurrentHashMap<>(16);
+
+    public static final Integer DEFAULT_ALIVE_SECOND = 30;
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/manage/model/dto/CapacityCameraDTO.java b/src/main/java/com/dji/sample/manage/model/dto/CapacityCameraDTO.java
new file mode 100644
index 0000000..dbc6481
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/model/dto/CapacityCameraDTO.java
@@ -0,0 +1,34 @@
+package com.dji.sample.manage.model.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+/**
+ * @author sean.zhou
+ * @date 2021/11/22
+ * @version 0.1
+ */
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class CapacityCameraDTO {
+
+    private Integer id;
+
+    private String deviceSn;
+
+    private String name;
+
+    private String description;
+
+    private String index;
+
+    private String type;
+
+    private List<CapacityVideoDTO> videosList;
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/manage/model/dto/CapacityDeviceDTO.java b/src/main/java/com/dji/sample/manage/model/dto/CapacityDeviceDTO.java
new file mode 100644
index 0000000..efa0bcc
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/model/dto/CapacityDeviceDTO.java
@@ -0,0 +1,26 @@
+package com.dji.sample.manage.model.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+/**
+ * @author sean.zhou
+ * @date 2021/11/22
+ * @version 0.1
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class CapacityDeviceDTO {
+
+    private String sn;
+
+    private String name;
+
+    private List<CapacityCameraDTO> camerasList;
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/manage/model/dto/CapacityVideoDTO.java b/src/main/java/com/dji/sample/manage/model/dto/CapacityVideoDTO.java
new file mode 100644
index 0000000..1d9dc3a
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/model/dto/CapacityVideoDTO.java
@@ -0,0 +1,24 @@
+package com.dji.sample.manage.model.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * @author sean.zhou
+ * @date 2021/11/22
+ * @version 0.1
+ */
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class CapacityVideoDTO {
+
+    private Integer id;
+
+    private String index;
+
+    private String type;
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/manage/model/dto/DeviceDTO.java b/src/main/java/com/dji/sample/manage/model/dto/DeviceDTO.java
new file mode 100644
index 0000000..2571dff
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/model/dto/DeviceDTO.java
@@ -0,0 +1,47 @@
+package com.dji.sample.manage.model.dto;
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+/**
+ * @author sean.zhou
+ * @date 2021/11/19
+ * @version 0.1
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder
+@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
+public class DeviceDTO {
+
+    private String deviceSn;
+
+    private String deviceName;
+
+    private String workspaceId;
+
+    private String deviceIndex;
+
+    private String deviceDesc;
+
+    private String childDeviceSn;
+
+    private String domain;
+
+    private Integer type;
+
+    private Integer subType;
+
+    private List<DeviceDTO> gatewaysList;
+
+    private List<DevicePayloadDTO> payloadsList;
+
+    private IconUrlDTO iconUrl;
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/manage/model/dto/DeviceDictionaryDTO.java b/src/main/java/com/dji/sample/manage/model/dto/DeviceDictionaryDTO.java
new file mode 100644
index 0000000..9c0170d
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/model/dto/DeviceDictionaryDTO.java
@@ -0,0 +1,28 @@
+package com.dji.sample.manage.model.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * @author sean.zhou
+ * @version 0.1
+ * @date 2021/11/23
+ */
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@Data
+public class DeviceDictionaryDTO {
+
+    private Integer domain;
+
+    private Integer deviceType;
+
+    private Integer subType;
+
+    private String deviceName;
+
+    private String deviceDesc;
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/manage/model/dto/DeviceModelDTO.java b/src/main/java/com/dji/sample/manage/model/dto/DeviceModelDTO.java
new file mode 100644
index 0000000..d7559ff
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/model/dto/DeviceModelDTO.java
@@ -0,0 +1,27 @@
+package com.dji.sample.manage.model.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/12/8
+ */
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class DeviceModelDTO {
+
+    private String key;
+
+    private String domain;
+
+    private String type;
+
+    private String subType;
+
+}
diff --git a/src/main/java/com/dji/sample/manage/model/dto/DevicePayloadDTO.java b/src/main/java/com/dji/sample/manage/model/dto/DevicePayloadDTO.java
new file mode 100644
index 0000000..bb16641
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/model/dto/DevicePayloadDTO.java
@@ -0,0 +1,29 @@
+package com.dji.sample.manage.model.dto;
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * @author sean.zhou
+ * @date 2021/11/19
+ * @version 0.1
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
+public class DevicePayloadDTO {
+
+    private String payloadSn;
+
+    private String payloadName;
+
+    private Integer payloadIndex;
+
+    private String payloadDesc;
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/manage/model/dto/IconUrlDTO.java b/src/main/java/com/dji/sample/manage/model/dto/IconUrlDTO.java
new file mode 100644
index 0000000..effbf1c
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/model/dto/IconUrlDTO.java
@@ -0,0 +1,25 @@
+package com.dji.sample.manage.model.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * @author sean
+ * @version 0.3
+ * @date 2022/1/5
+ */
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class IconUrlDTO {
+
+    @JsonProperty("normal_icon_url")
+    private String normalUrl;
+
+    @JsonProperty("selected_icon_url")
+    private String selectUrl;
+}
diff --git a/src/main/java/com/dji/sample/manage/model/dto/LiveDTO.java b/src/main/java/com/dji/sample/manage/model/dto/LiveDTO.java
new file mode 100644
index 0000000..29841c2
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/model/dto/LiveDTO.java
@@ -0,0 +1,20 @@
+package com.dji.sample.manage.model.dto;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import lombok.Data;
+
+/**
+ * @author sean.zhou
+ * @version 0.1
+ * @date 2021/11/23
+ */
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class LiveDTO {
+
+    private String url;
+
+    private String username;
+
+    private String password;
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/manage/model/dto/LiveTypeDTO.java b/src/main/java/com/dji/sample/manage/model/dto/LiveTypeDTO.java
new file mode 100644
index 0000000..ffe341e
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/model/dto/LiveTypeDTO.java
@@ -0,0 +1,26 @@
+package com.dji.sample.manage.model.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+/**
+ * Receive live parameters.
+ * @author sean.zhou
+ * @version 0.1
+ * @date 2021/11/22
+ */
+@Data
+public class LiveTypeDTO {
+
+    @JsonProperty("url_type")
+    private Integer urlType;
+
+    private String url;
+
+    @JsonProperty("video_id")
+    private String videoId;
+
+    @JsonProperty("video_quality")
+    private Integer videoQuality;
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/manage/model/dto/LiveUrlAgoraDTO.java b/src/main/java/com/dji/sample/manage/model/dto/LiveUrlAgoraDTO.java
new file mode 100644
index 0000000..1f9f781
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/model/dto/LiveUrlAgoraDTO.java
@@ -0,0 +1,20 @@
+package com.dji.sample.manage.model.dto;
+
+import lombok.Data;
+
+/**
+ * @author sean.zhou
+ * @date 2021/11/23
+ * @version 0.1
+ */
+@Data
+public class LiveUrlAgoraDTO {
+
+    private String channel;
+
+    private String sn;
+
+    private String token;
+
+    private Integer uid;
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/manage/model/dto/LiveUrlGB28181DTO.java b/src/main/java/com/dji/sample/manage/model/dto/LiveUrlGB28181DTO.java
new file mode 100644
index 0000000..d57f06a
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/model/dto/LiveUrlGB28181DTO.java
@@ -0,0 +1,27 @@
+package com.dji.sample.manage.model.dto;
+
+import lombok.Data;
+
+/**
+ * @author sean.zhou
+ * @version 0.1
+ * @date 2021/11/23
+ */
+@Data
+public class LiveUrlGB28181DTO {
+
+    private String serverIP;
+
+    private Integer serverPort;
+
+    private String serverID;
+
+    private String agentID;
+
+    private String agentPassword;
+
+    private Integer localPort;
+
+    private String channel;
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/manage/model/dto/LiveUrlRTSPDTO.java b/src/main/java/com/dji/sample/manage/model/dto/LiveUrlRTSPDTO.java
new file mode 100644
index 0000000..8e57734
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/model/dto/LiveUrlRTSPDTO.java
@@ -0,0 +1,18 @@
+package com.dji.sample.manage.model.dto;
+
+import lombok.Data;
+
+/**
+ * @author sean.zhou
+ * @version 0.1
+ * @date 2021/11/23
+ */
+@Data
+public class LiveUrlRTSPDTO {
+
+    private String userName;
+
+    private String password;
+
+    private Integer port;
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/manage/model/dto/TelemetryDTO.java b/src/main/java/com/dji/sample/manage/model/dto/TelemetryDTO.java
new file mode 100644
index 0000000..8f7f9d5
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/model/dto/TelemetryDTO.java
@@ -0,0 +1,22 @@
+package com.dji.sample.manage.model.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/12/8
+ */
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class TelemetryDTO {
+
+    private TelemetryDeviceDTO host;
+
+    private String sn;
+}
diff --git a/src/main/java/com/dji/sample/manage/model/dto/TelemetryDeviceDTO.java b/src/main/java/com/dji/sample/manage/model/dto/TelemetryDeviceDTO.java
new file mode 100644
index 0000000..1905804
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/model/dto/TelemetryDeviceDTO.java
@@ -0,0 +1,32 @@
+package com.dji.sample.manage.model.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/12/8
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class TelemetryDeviceDTO {
+
+    private Double latitude;
+
+    private Double longitude;
+
+    private Double altitude;
+
+    private Float attitudeHead;
+
+    private Double elevation;
+
+    private Float horizontalSpeed;
+
+    private Float verticalSpeed;
+}
diff --git a/src/main/java/com/dji/sample/manage/model/dto/TopologyDTO.java b/src/main/java/com/dji/sample/manage/model/dto/TopologyDTO.java
new file mode 100644
index 0000000..d91cd3a
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/model/dto/TopologyDTO.java
@@ -0,0 +1,24 @@
+package com.dji.sample.manage.model.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/12/8
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class TopologyDTO {
+
+    private List<TopologyDeviceDTO> hosts;
+
+    private List<TopologyDeviceDTO> parents;
+}
diff --git a/src/main/java/com/dji/sample/manage/model/dto/TopologyDeviceDTO.java b/src/main/java/com/dji/sample/manage/model/dto/TopologyDeviceDTO.java
new file mode 100644
index 0000000..c36e88d
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/model/dto/TopologyDeviceDTO.java
@@ -0,0 +1,35 @@
+package com.dji.sample.manage.model.dto;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/12/8
+ */
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class TopologyDeviceDTO {
+
+    private String sn;
+
+    private DeviceModelDTO deviceModel;
+
+    @Builder.Default
+    private Boolean onlineStatus = true;
+
+    private String deviceCallsign;
+
+    private String userId;
+
+    private String userCallsign;
+
+    private IconUrlDTO iconUrls;
+}
diff --git a/src/main/java/com/dji/sample/manage/model/dto/UserDTO.java b/src/main/java/com/dji/sample/manage/model/dto/UserDTO.java
new file mode 100644
index 0000000..1e67c72
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/model/dto/UserDTO.java
@@ -0,0 +1,37 @@
+package com.dji.sample.manage.model.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class UserDTO {
+
+    @JsonProperty("user_id")
+    private String userId;
+
+    private String username;
+
+    @JsonProperty("workspace_id")
+    private String workspaceId;
+
+    @JsonProperty("user_type")
+    private Integer userType;
+
+    @JsonProperty("mqtt_username")
+    private String mqttUsername;
+
+    @JsonProperty("mqtt_password")
+    private String mqttPassword;
+
+    @JsonProperty("access_token")
+    private String accessToken;
+
+    @JsonProperty("mqtt_addr")
+    private String mqttAddr;
+}
diff --git a/src/main/java/com/dji/sample/manage/model/dto/UserLoginDTO.java b/src/main/java/com/dji/sample/manage/model/dto/UserLoginDTO.java
new file mode 100644
index 0000000..b82ca44
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/model/dto/UserLoginDTO.java
@@ -0,0 +1,14 @@
+package com.dji.sample.manage.model.dto;
+
+import lombok.Data;
+import lombok.NonNull;
+
+@Data
+public class UserLoginDTO {
+
+    @NonNull
+    private String username;
+
+    @NonNull
+    private String password;
+}
diff --git a/src/main/java/com/dji/sample/manage/model/dto/WorkspaceDTO.java b/src/main/java/com/dji/sample/manage/model/dto/WorkspaceDTO.java
new file mode 100644
index 0000000..0ce7d15
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/model/dto/WorkspaceDTO.java
@@ -0,0 +1,33 @@
+package com.dji.sample.manage.model.dto;
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * @author sean.zhou
+ * @date 2021/11/22
+ * @version 0.1
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder
+@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
+public class WorkspaceDTO {
+
+    public static final String DEFAULT_WORKSPACE_ID = "e3dea0f5-37f2-4d79-ae58-490af3228069";
+
+    private Integer id;
+
+    private String workspaceId;
+
+    private String workspaceName;
+
+    private String workspaceDesc;
+
+    private String platformName;
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/manage/model/entity/CameraVideoEntity.java b/src/main/java/com/dji/sample/manage/model/entity/CameraVideoEntity.java
new file mode 100644
index 0000000..a2e7269
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/model/entity/CameraVideoEntity.java
@@ -0,0 +1,37 @@
+package com.dji.sample.manage.model.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * @author sean.zhou
+ * @date 2021/11/19
+ * @version 0.1
+ */
+@TableName(value = "manage_camera_video")
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder
+public class CameraVideoEntity implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Integer id;
+
+    @TableField(value = "camera_id")
+    private Integer cameraId;
+
+    @TableField(value = "video_index")
+    private String videoIndex;
+
+    @TableField(value = "video_type")
+    private String videoType;
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/manage/model/entity/CapacityCameraEntity.java b/src/main/java/com/dji/sample/manage/model/entity/CapacityCameraEntity.java
new file mode 100644
index 0000000..c57f496
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/model/entity/CapacityCameraEntity.java
@@ -0,0 +1,46 @@
+package com.dji.sample.manage.model.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * @author sean.zhou
+ * @date 2021/11/19
+ * @version 0.1
+ */
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@TableName(value = "manage_capacity_camera")
+public class CapacityCameraEntity implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Integer id;
+
+    @TableField(value = "device_sn")
+    private String deviceSn;
+
+    @TableField(value = "name")
+    private String name;
+
+    @TableField(value = "description")
+    private String description;
+
+    @TableField(value = "camera_index")
+    private String cameraIndex;
+
+    @TableField(value = "coexist_video_number_max")
+    private Integer coexistVideoNumberMax;
+
+    @TableField(value = "available_video_number")
+    private Integer availableVideoNumber;
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/manage/model/entity/DeviceDictionaryEntity.java b/src/main/java/com/dji/sample/manage/model/entity/DeviceDictionaryEntity.java
new file mode 100644
index 0000000..c01ec6f
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/model/entity/DeviceDictionaryEntity.java
@@ -0,0 +1,45 @@
+package com.dji.sample.manage.model.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ *
+ * @author sean.zhou
+ * @date 2021/11/15
+ * @version 0.1
+ */
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@TableName("manage_device_dictionary")
+public class DeviceDictionaryEntity implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Integer id;
+
+    @TableField(value = "domain")
+    private Integer domain;
+
+    @TableField(value = "device_type")
+    private Integer deviceType;
+
+    @TableField(value = "sub_type")
+    private Integer subType;
+
+    @TableField(value = "device_name")
+    private String deviceName;
+
+    @TableField(value = "device_desc")
+    private String deviceDesc;
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/manage/model/entity/DeviceEntity.java b/src/main/java/com/dji/sample/manage/model/entity/DeviceEntity.java
new file mode 100644
index 0000000..f019a01
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/model/entity/DeviceEntity.java
@@ -0,0 +1,69 @@
+package com.dji.sample.manage.model.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * The entity class of the device
+ *
+ * @author sean.zhou
+ * @version 0.1
+ * @date 2021/11/10
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@TableName(value = "manage_device")
+public class DeviceEntity implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Integer id;
+
+    @TableField(value = "device_sn")
+    private String deviceSn;
+
+    @TableField(value = "device_name")
+    private String deviceName;
+
+    @TableField(value = "workspace_id")
+    private String workspaceId;
+
+    @TableField(value = "device_type")
+    private Integer deviceType;
+
+    @TableField(value = "sub_type")
+    private Integer subType;
+
+    @TableField(value = "domain")
+    private Integer domain;
+
+    @TableField(value = "version")
+    private Integer version;
+
+    @TableField(value = "device_index")
+    private String deviceIndex;
+
+    @TableField(value = "child_sn")
+    private String childSn;
+
+    @TableField(value = "create_time", fill = FieldFill.INSERT)
+    private Long createTime;
+
+    @TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
+    private Long updateTime;
+
+    @TableField(value = "device_desc")
+    private String deviceDesc;
+
+    @TableField(value = "url_normal")
+    private String urlNormal;
+
+    @TableField(value = "url_select")
+    private String urlSelect;
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/manage/model/entity/DevicePayloadEntity.java b/src/main/java/com/dji/sample/manage/model/entity/DevicePayloadEntity.java
new file mode 100644
index 0000000..184fb0f
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/model/entity/DevicePayloadEntity.java
@@ -0,0 +1,56 @@
+package com.dji.sample.manage.model.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * @author sean.zhou
+ * @date 2021/11/19
+ * @version 0.1
+ */
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@TableName(value = "manage_device_payload")
+public class DevicePayloadEntity implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Integer id;
+
+    @TableField(value = "payload_sn")
+    private String payloadSn;
+
+    @TableField(value = "payload_name")
+    private String payloadName;
+
+    @TableField(value = "payload_type")
+    private Integer payloadType;
+
+    @TableField(value = "sub_type")
+    private Integer subType;
+
+    @TableField(value = "version")
+    private Integer version;
+
+    @TableField(value = "payload_index")
+    private Integer payloadIndex;
+
+    @TableField(value = "device_sn")
+    private String deviceSn;
+
+    @TableField(value = "create_time", fill = FieldFill.INSERT)
+    private Long createTime;
+
+    @TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
+    private Long updateTime;
+
+    @TableField(value = "payload_desc")
+    private String payloadDesc;
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/manage/model/entity/UserEntity.java b/src/main/java/com/dji/sample/manage/model/entity/UserEntity.java
new file mode 100644
index 0000000..67edecf
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/model/entity/UserEntity.java
@@ -0,0 +1,41 @@
+package com.dji.sample.manage.model.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+
+import java.io.Serializable;
+
+@TableName(value = "manage_user")
+@Data
+public class UserEntity implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Integer id;
+
+    @TableField(value = "user_id")
+    private String userId;
+
+    @TableField(value = "username")
+    private String username;
+
+    @TableField(value = "password")
+    private String password;
+
+    @TableField(value = "workspace_id")
+    private Integer workspaceId;
+
+    @TableField(value = "user_type")
+    private Integer userType;
+
+    @TableField(value = "mqtt_username")
+    private String mqttUsername;
+
+    @TableField(value = "mqtt_password")
+    private String mqttPassword;
+
+    @TableField(value = "create_time", fill = FieldFill.INSERT)
+    private Long createTime;
+
+    @TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
+    private Long updateTime;
+}
diff --git a/src/main/java/com/dji/sample/manage/model/entity/WorkspaceEntity.java b/src/main/java/com/dji/sample/manage/model/entity/WorkspaceEntity.java
new file mode 100644
index 0000000..8a84ff0
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/model/entity/WorkspaceEntity.java
@@ -0,0 +1,33 @@
+package com.dji.sample.manage.model.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+
+import java.io.Serializable;
+
+@TableName(value = "manage_workspace")
+@Data
+public class WorkspaceEntity implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Integer id;
+
+    @TableField(value = "workspace_id")
+    private String workspaceId;
+
+    @TableField(value = "workspace_name")
+    private String workspaceName;
+
+    @TableField(value = "workspace_desc")
+    private String workspaceDesc;
+
+    @TableField(value = "platform_name")
+    private String platformName;
+
+    @TableField(value = "create_time", fill = FieldFill.INSERT)
+    private Long createTime;
+
+    @TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
+    private Long updateTime;
+
+}
diff --git a/src/main/java/com/dji/sample/manage/model/enums/DeviceDomainEnum.java b/src/main/java/com/dji/sample/manage/model/enums/DeviceDomainEnum.java
new file mode 100644
index 0000000..cd8fd39
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/model/enums/DeviceDomainEnum.java
@@ -0,0 +1,63 @@
+package com.dji.sample.manage.model.enums;
+
+/**
+ *
+ * @author sean.zhou
+ * @date 2021/11/15
+ * @version 0.1
+ */
+public enum DeviceDomainEnum {
+
+    SUB_DEVICE(0, "sub-device"),
+
+    GATEWAY(2, "gateway"),
+
+    PAYLOAD(1, "payload"),
+
+    UNKNOWN(-1, "unknown");
+
+    private int val;
+
+    private String desc;
+
+    DeviceDomainEnum(int val, String desc) {
+        this.val = val;
+        this.desc = desc;
+    }
+
+    public int getVal() {
+        return val;
+    }
+
+    public static String getDesc(int val) {
+        if (SUB_DEVICE.val == val) {
+            return SUB_DEVICE.desc;
+        }
+
+        if (GATEWAY.val == val) {
+            return GATEWAY.desc;
+        }
+
+        if (PAYLOAD.val == val) {
+            return PAYLOAD.desc;
+        }
+        return UNKNOWN.desc;
+    }
+
+    public static int getVal(String desc) {
+        if (SUB_DEVICE.desc.equals(desc)) {
+            return SUB_DEVICE.val;
+        }
+
+        if (GATEWAY.desc.equals(desc)) {
+            return GATEWAY.val;
+        }
+
+        if (PAYLOAD.desc.equals(desc)) {
+            return PAYLOAD.val;
+        }
+        return UNKNOWN.val;
+    }
+
+
+}
diff --git a/src/main/java/com/dji/sample/manage/model/enums/IconUrlEnum.java b/src/main/java/com/dji/sample/manage/model/enums/IconUrlEnum.java
new file mode 100644
index 0000000..3803d2f
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/model/enums/IconUrlEnum.java
@@ -0,0 +1,37 @@
+package com.dji.sample.manage.model.enums;
+
+/**
+ * The system icon that comes with the pilot.
+ * @author sean
+ * @version 0.3
+ * @date 2022/1/5
+ */
+public enum IconUrlEnum {
+
+    SELECT_CAR("resource://pilot/drawable/tsa_car_select"),
+
+    NORMAL_CAR("resource://pilot/drawable/tsa_car_normal"),
+
+    SELECT_PERSON("resource://pilot/drawable/tsa_person_select"),
+
+    NORMAL_PERSON("resource://pilot/drawable/tsa_person_normal"),
+
+    SELECT_EQUIPMENT("resource://pilot/drawable/tsa_equipment_select"),
+
+    NORMAL_EQUIPMENT("resource://pilot/drawable/tsa_equipment_normal");
+
+    /**
+     * You can use icons from the web, and the App internally downloads and caches these icons and
+     * loads them at a fixed size (28dp) to display on the map.
+     * Example: http://r56978dr7.hn-bkt.clouddn.com/tsa_equipment_normal.png
+     */
+    private String url;
+
+    IconUrlEnum(String url) {
+        this.url = url;
+    }
+
+    public String getUrl() {
+        return url;
+    }
+}
diff --git a/src/main/java/com/dji/sample/manage/model/enums/LiveMethodEnum.java b/src/main/java/com/dji/sample/manage/model/enums/LiveMethodEnum.java
new file mode 100644
index 0000000..eebe35c
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/model/enums/LiveMethodEnum.java
@@ -0,0 +1,27 @@
+package com.dji.sample.manage.model.enums;
+
+/**
+ * @author sean.zhou
+ * @date 2021/11/22
+ * @version 0.1
+ */
+public enum LiveMethodEnum {
+
+    LIVE_START_PUSH("live_start_push"),
+
+    LIVE_STOP_PUSH("live_stop_push"),
+
+    LIVE_SET_QUALITY("live_set_quality"),
+
+    UNKNOWN("unknown");
+
+    private String method;
+
+    LiveMethodEnum(String method) {
+        this.method = method;
+    }
+
+    public String getMethod() {
+        return method;
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/manage/model/enums/LiveUrlTypeEnum.java b/src/main/java/com/dji/sample/manage/model/enums/LiveUrlTypeEnum.java
new file mode 100644
index 0000000..163b520
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/model/enums/LiveUrlTypeEnum.java
@@ -0,0 +1,41 @@
+package com.dji.sample.manage.model.enums;
+
+/**
+ * @author sean.zhou
+ * @version 0.1
+ * @date 2021/11/22
+ */
+public enum LiveUrlTypeEnum {
+
+    AGORA(0),
+
+    RTMP(1),
+
+    RTSP(2),
+
+    GB28181(3),
+
+    UNKNOWN(-1);
+
+    private int val;
+
+    LiveUrlTypeEnum(int val) {
+        this.val = val;
+    }
+
+    public static LiveUrlTypeEnum find(Integer val) {
+        if (AGORA.val == val) {
+            return AGORA;
+        }
+        if (RTMP.val == val) {
+            return RTMP;
+        }
+        if (RTSP.val == val) {
+            return RTSP;
+        }
+        if (GB28181.val == val) {
+            return GB28181;
+        }
+        return UNKNOWN;
+    }
+}
diff --git a/src/main/java/com/dji/sample/manage/model/enums/LiveVideoQualityEnum.java b/src/main/java/com/dji/sample/manage/model/enums/LiveVideoQualityEnum.java
new file mode 100644
index 0000000..3ad42d3
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/model/enums/LiveVideoQualityEnum.java
@@ -0,0 +1,51 @@
+package com.dji.sample.manage.model.enums;
+
+/**
+ * @author sean
+ * @version 0.1
+ * @date 2021/11/26
+ */
+public enum LiveVideoQualityEnum {
+
+    AUTO (0),
+
+    SMOOTH(1),
+
+    STANDARD_DEFINITION(2),
+
+    HIGH_DEFINITION(3),
+
+    ULTRA_HD(4),
+
+    UNKNOWN(-1);
+
+    private int val;
+
+    LiveVideoQualityEnum(int val) {
+        this.val = val;
+    }
+
+    public static LiveVideoQualityEnum find(int val) {
+        if (AUTO.val == val) {
+            return AUTO;
+        }
+
+        if (SMOOTH.val == val) {
+            return SMOOTH;
+        }
+
+        if (STANDARD_DEFINITION.val == val) {
+            return STANDARD_DEFINITION;
+        }
+
+        if (HIGH_DEFINITION.val == val) {
+            return HIGH_DEFINITION;
+        }
+
+        if (ULTRA_HD.val == val) {
+            return ULTRA_HD;
+        }
+
+        return UNKNOWN;
+    }
+}
diff --git a/src/main/java/com/dji/sample/manage/model/enums/StateDataEnum.java b/src/main/java/com/dji/sample/manage/model/enums/StateDataEnum.java
new file mode 100644
index 0000000..5a93cf3
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/model/enums/StateDataEnum.java
@@ -0,0 +1,26 @@
+package com.dji.sample.manage.model.enums;
+
+/**
+ *
+ * @author sean.zhou
+ * @date 2021/11/18
+ * @version 0.1
+ */
+public enum StateDataEnum {
+
+    BATTERIES("batteries"),
+
+    LIVE_CAPACITY("live_capacity"),
+
+    PAYLOADS("payloads");
+
+    private String desc;
+
+    StateDataEnum(String desc) {
+        this.desc = desc;
+    }
+
+    public String getDesc() {
+        return this.desc;
+    }
+}
diff --git a/src/main/java/com/dji/sample/manage/model/enums/UserTypeEnum.java b/src/main/java/com/dji/sample/manage/model/enums/UserTypeEnum.java
new file mode 100644
index 0000000..311669e
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/model/enums/UserTypeEnum.java
@@ -0,0 +1,24 @@
+package com.dji.sample.manage.model.enums;
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/12/2
+ */
+public enum UserTypeEnum {
+
+    WEB(1),
+
+    PILOT(2);
+
+    private int val;
+
+
+    UserTypeEnum(int val) {
+        this.val = val;
+    }
+
+    public int getVal() {
+        return val;
+    }
+}
diff --git a/src/main/java/com/dji/sample/manage/model/param/DeviceQueryParam.java b/src/main/java/com/dji/sample/manage/model/param/DeviceQueryParam.java
new file mode 100644
index 0000000..9233121
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/model/param/DeviceQueryParam.java
@@ -0,0 +1,32 @@
+package com.dji.sample.manage.model.param;
+
+import lombok.Builder;
+import lombok.Data;
+
+/**
+ * The object of the device query field.
+ *
+ * @author sean.zhou
+ * @date 2021/11/16
+ * @version 0.1
+ */
+@Data
+@Builder
+public class DeviceQueryParam {
+
+    private String deviceSn;
+
+    private String workspaceId;
+
+    private Integer deviceType;
+
+    private Integer subType;
+
+    private Integer domain;
+
+    private String childSn;
+
+    private boolean orderBy;
+
+    private boolean isAsc;
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/manage/model/receiver/BatteryReceiver.java b/src/main/java/com/dji/sample/manage/model/receiver/BatteryReceiver.java
new file mode 100644
index 0000000..dc0162f
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/model/receiver/BatteryReceiver.java
@@ -0,0 +1,27 @@
+package com.dji.sample.manage.model.receiver;
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * @author sean
+ * @version 0.3
+ * @date 2022/1/27
+ */
+@Data
+@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
+public class BatteryReceiver {
+
+    private List<BatteryStateReceiver> batteries;
+
+    private Integer capacityPercent;
+
+    private Integer landingPower;
+
+    private Integer remainFlightTime;
+
+    private Integer returnHomePower;
+}
diff --git a/src/main/java/com/dji/sample/manage/model/receiver/BatteryStateReceiver.java b/src/main/java/com/dji/sample/manage/model/receiver/BatteryStateReceiver.java
new file mode 100644
index 0000000..275adc1
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/model/receiver/BatteryStateReceiver.java
@@ -0,0 +1,34 @@
+package com.dji.sample.manage.model.receiver;
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import lombok.Data;
+
+/**
+ * @author sean.zhou
+ * @version 0.1
+ * @date 2021/11/24
+ */
+@Data
+@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
+public class BatteryStateReceiver {
+
+    private String firmwareVersion;
+
+    private Integer index;
+
+    private Integer loopTimes;
+
+    private Integer capacityPercent;
+
+    private String sn;
+
+    private Integer subType;
+
+    private Float temperature;
+
+    private Integer type;
+
+    private Integer voltage;
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/manage/model/receiver/CapacityCameraReceiver.java b/src/main/java/com/dji/sample/manage/model/receiver/CapacityCameraReceiver.java
new file mode 100644
index 0000000..e5321fb
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/model/receiver/CapacityCameraReceiver.java
@@ -0,0 +1,28 @@
+package com.dji.sample.manage.model.receiver;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * @author sean.zhou
+ * @date 2021/11/18
+ * @version 0.1
+ */
+@Data
+@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
+public class CapacityCameraReceiver {
+
+    private Integer availableVideoNumber;
+
+    private Integer coexistVideoNumberMax;
+
+    private String cameraIndex;
+
+    @JsonProperty(value = "video_list")
+    private List<CapacityVideoReceiver> videosList;
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/manage/model/receiver/CapacityDeviceReceiver.java b/src/main/java/com/dji/sample/manage/model/receiver/CapacityDeviceReceiver.java
new file mode 100644
index 0000000..750db59
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/model/receiver/CapacityDeviceReceiver.java
@@ -0,0 +1,25 @@
+package com.dji.sample.manage.model.receiver;
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * @author sean.zhou
+ * @date 2021/11/18
+ * @version 0.1
+ */
+@Data
+@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
+public class CapacityDeviceReceiver {
+
+    private String sn;
+
+    private Integer availableVideoNumber;
+
+    private Integer coexistVideoNumberMax;
+
+    private List<CapacityCameraReceiver> cameraList;
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/manage/model/receiver/CapacityVideoReceiver.java b/src/main/java/com/dji/sample/manage/model/receiver/CapacityVideoReceiver.java
new file mode 100644
index 0000000..616b5a9
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/model/receiver/CapacityVideoReceiver.java
@@ -0,0 +1,19 @@
+package com.dji.sample.manage.model.receiver;
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import lombok.Data;
+
+/**
+ * @author sean.zhou
+ * @date 2021/11/18
+ * @version 0.1
+ */
+@Data
+@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
+public class CapacityVideoReceiver {
+
+    private String videoIndex;
+
+    private String videoType;
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/manage/model/receiver/DeviceBasicReceiver.java b/src/main/java/com/dji/sample/manage/model/receiver/DeviceBasicReceiver.java
new file mode 100644
index 0000000..1ae46d8
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/model/receiver/DeviceBasicReceiver.java
@@ -0,0 +1,39 @@
+package com.dji.sample.manage.model.receiver;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * @author sean.zhou
+ * @date 2021/11/18
+ * @version 0.1
+ */
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
+public class DeviceBasicReceiver {
+
+    private String controlSource;
+
+    private String deviceSn;
+
+    private Integer flightMode;
+
+    private Integer flightStatus;
+
+    private Double homeLatitude;
+
+    private Double homeLongitude;
+
+    private Integer lowBatteryWarningThreshold;
+
+    private Integer positionMode;
+
+    private Integer seriousLowBatteryWarningThreshold;
+
+    private List<DevicePayloadReceiver> payloads;
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/manage/model/receiver/DevicePayloadReceiver.java b/src/main/java/com/dji/sample/manage/model/receiver/DevicePayloadReceiver.java
new file mode 100644
index 0000000..2b3f998
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/model/receiver/DevicePayloadReceiver.java
@@ -0,0 +1,29 @@
+package com.dji.sample.manage.model.receiver;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import lombok.Data;
+
+/**
+ * @author sean.zhou
+ * @date 2021/11/18
+ * @version 0.1
+ */
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
+public class DevicePayloadReceiver {
+
+    private String deviceSn;
+
+    private String controlSource;
+
+    private String payloadIndex;
+
+    private String sn;
+
+    private Integer version;
+
+    private Integer workMode;
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/manage/model/receiver/LiveStatusReceiver.java b/src/main/java/com/dji/sample/manage/model/receiver/LiveStatusReceiver.java
new file mode 100644
index 0000000..463cbf8
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/model/receiver/LiveStatusReceiver.java
@@ -0,0 +1,23 @@
+package com.dji.sample.manage.model.receiver;
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import lombok.Data;
+
+/**
+ * @author sean.zhou
+ * @version 0.1
+ * @date 2021/11/23
+ */
+@Data
+@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
+public class LiveStatusReceiver {
+
+    private Long liveTime;
+
+    private Integer liveTrendline;
+
+    private String videoId;
+
+    private Integer videoQuality;
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/manage/model/receiver/OsdGatewayReceiver.java b/src/main/java/com/dji/sample/manage/model/receiver/OsdGatewayReceiver.java
new file mode 100644
index 0000000..4e0a299
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/model/receiver/OsdGatewayReceiver.java
@@ -0,0 +1,31 @@
+package com.dji.sample.manage.model.receiver;
+
+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;
+
+/**
+ * @author sean.zhou
+ * @version 0.1
+ * @date 2021/11/23
+ */
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
+public class OsdGatewayReceiver {
+
+    private Double latitude;
+
+    private Double longitude;
+
+    @JsonProperty("capacity_percent")
+    private Integer remainPower;
+
+    private Integer transmissionSignalQuality;
+
+    private LiveStatusReceiver liveStatus;
+
+    private WirelessLinkStateReceiver wirelessLinkState;
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/manage/model/receiver/OsdSubDeviceReceiver.java b/src/main/java/com/dji/sample/manage/model/receiver/OsdSubDeviceReceiver.java
new file mode 100644
index 0000000..24d101e
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/model/receiver/OsdSubDeviceReceiver.java
@@ -0,0 +1,55 @@
+package com.dji.sample.manage.model.receiver;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import lombok.Data;
+
+/**
+ * @author sean.zhou
+ * @version 0.1
+ * @date 2021/11/23
+ */
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
+public class OsdSubDeviceReceiver {
+
+    private Float attitudeHead;
+
+    private Double attitudePitch;
+
+    private Double attitudeRoll;
+
+    private Double elevation;
+
+    private BatteryReceiver battery;
+
+    private String firmwareVersion;
+
+    private Integer gear;
+
+    private Double height;
+
+    private Double homeDistance;
+
+    private Float horizontalSpeed;
+
+    private Double latitude;
+
+    private Double longitude;
+
+    private Integer modeCode;
+
+    private Double totalFlightDistance;
+
+    private Double totalFlightTime;
+
+    private Float verticalSpeed;
+
+    private Double windDirection;
+
+    private Double windSpeed;
+
+    private PositionStateReceiver positionState;
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/manage/model/receiver/PositionStateReceiver.java b/src/main/java/com/dji/sample/manage/model/receiver/PositionStateReceiver.java
new file mode 100644
index 0000000..5c6787f
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/model/receiver/PositionStateReceiver.java
@@ -0,0 +1,23 @@
+package com.dji.sample.manage.model.receiver;
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import lombok.Data;
+
+/**
+ * @author sean
+ * @version 0.3
+ * @date 2022/1/27
+ */
+@Data
+@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
+public class PositionStateReceiver {
+
+    private Integer gpsNumber;
+
+    private Integer isFixed;
+
+    private Integer quality;
+
+    private Integer rtkNumber;
+}
diff --git a/src/main/java/com/dji/sample/manage/model/receiver/RTKStateReceiver.java b/src/main/java/com/dji/sample/manage/model/receiver/RTKStateReceiver.java
new file mode 100644
index 0000000..c877c0c
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/model/receiver/RTKStateReceiver.java
@@ -0,0 +1,28 @@
+package com.dji.sample.manage.model.receiver;
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import lombok.Data;
+
+/**
+ * @author sean.zhou
+ * @version 0.1
+ * @date 2021/11/24
+ */
+@Data
+@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
+public class RTKStateReceiver {
+
+    private Integer bdsNumber;
+
+    private Integer galNumber;
+
+    private Integer gloNumber;
+
+    private Integer gpsNumber;
+
+    private Boolean isFixed;
+
+    private Integer quality;
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/manage/model/receiver/ServiceReplyReceiver.java b/src/main/java/com/dji/sample/manage/model/receiver/ServiceReplyReceiver.java
new file mode 100644
index 0000000..ece269f
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/model/receiver/ServiceReplyReceiver.java
@@ -0,0 +1,18 @@
+package com.dji.sample.manage.model.receiver;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import lombok.Data;
+
+/**
+ * @author sean.zhou
+ * @version 0.1
+ * @date 2021/11/22
+ */
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class ServiceReplyReceiver<T> {
+
+    private Integer result;
+
+    private String info;
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/manage/model/receiver/StatusGatewayReceiver.java b/src/main/java/com/dji/sample/manage/model/receiver/StatusGatewayReceiver.java
new file mode 100644
index 0000000..0cee5ed
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/model/receiver/StatusGatewayReceiver.java
@@ -0,0 +1,43 @@
+package com.dji.sample.manage.model.receiver;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+/**
+ *
+ * @author sean.zhou
+ * @version 0.1
+ * @date 2021/11/12
+ */
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class StatusGatewayReceiver {
+
+    private String sn;
+
+    private Integer domain;
+
+    private Integer type;
+
+    @JsonProperty(value = "sub_type")
+    private Integer subType;
+
+    @JsonProperty(value = "device_secret")
+    private String deviceSecret;
+
+    private String nonce;
+
+    private Integer version;
+
+    @JsonProperty(value = "sub_devices")
+    private List<StatusSubDeviceReceiver> subDevices;
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/manage/model/receiver/StatusSubDeviceReceiver.java b/src/main/java/com/dji/sample/manage/model/receiver/StatusSubDeviceReceiver.java
new file mode 100644
index 0000000..4f0d47a
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/model/receiver/StatusSubDeviceReceiver.java
@@ -0,0 +1,32 @@
+package com.dji.sample.manage.model.receiver;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+/**
+ *
+ * @author sean.zhou
+ * @version 0.1
+ * @date 2021/11/12
+ */
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class StatusSubDeviceReceiver {
+
+    private String sn;
+
+    private Integer type;
+
+    @JsonProperty(value = "sub_type")
+    private Integer subType;
+
+    private String index;
+
+    @JsonProperty(value = "device_secret")
+    private String deviceSecret;
+
+    private String nonce;
+
+    private Integer version;
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/manage/model/receiver/WirelessLinkStateReceiver.java b/src/main/java/com/dji/sample/manage/model/receiver/WirelessLinkStateReceiver.java
new file mode 100644
index 0000000..5dc8bd5
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/model/receiver/WirelessLinkStateReceiver.java
@@ -0,0 +1,22 @@
+package com.dji.sample.manage.model.receiver;
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import lombok.Data;
+
+/**
+ * @author sean.zhou
+ * @version 0.1
+ * @date 2021/11/23
+ */
+@Data
+@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
+public class WirelessLinkStateReceiver {
+
+    private Integer downloadQuality;
+
+    private Integer frequencyBand;
+
+    private Integer upwardQuality;
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/manage/service/ICameraVideoService.java b/src/main/java/com/dji/sample/manage/service/ICameraVideoService.java
new file mode 100644
index 0000000..808d6dd
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/service/ICameraVideoService.java
@@ -0,0 +1,37 @@
+package com.dji.sample.manage.service;
+
+import com.dji.sample.manage.model.dto.CapacityVideoDTO;
+import com.dji.sample.manage.model.receiver.CapacityVideoReceiver;
+
+import java.util.List;
+
+/**
+ * @author sean.zhou
+ * @date 2021/11/19
+ * @version 0.1
+ */
+public interface ICameraVideoService {
+
+    /**
+     * Queries all lens data contained in the camera based on camera id.
+     * @param cameraId
+     * @return
+     */
+    List<CapacityVideoDTO> getCameraVideosByCameraId(Integer cameraId);
+
+    /**
+     * Deletes all the data of this lens, according to the lens id.
+     * @param ids   A collection of lens ids.
+     * @return true
+     */
+    Boolean deleteCameraVideosById(List<Integer> ids);
+
+    /**
+     * Save the live capability of all lenses of this camera.
+     * @param capacityVideoReceivers live capability of lens
+     * @param cameraId
+     * @return
+     */
+    Boolean saveCameraVideoDTOList(List<CapacityVideoReceiver> capacityVideoReceivers, Integer cameraId);
+
+}
diff --git a/src/main/java/com/dji/sample/manage/service/ICapacityCameraService.java b/src/main/java/com/dji/sample/manage/service/ICapacityCameraService.java
new file mode 100644
index 0000000..02ab494
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/service/ICapacityCameraService.java
@@ -0,0 +1,45 @@
+package com.dji.sample.manage.service;
+
+import com.dji.sample.manage.model.dto.CapacityCameraDTO;
+import com.dji.sample.manage.model.receiver.CapacityCameraReceiver;
+
+import java.util.List;
+
+/**
+ * @author sean.zhou
+ * @date 2021/11/19
+ * @version 0.1
+ */
+public interface ICapacityCameraService {
+
+    /**
+     * Query all camera data that can be live streamed from this device based on the device sn.
+     * @param deviceSn
+     * @return
+     */
+    List<CapacityCameraDTO> getCapacityCameraByDeviceSn(String deviceSn);
+
+    /**
+     * Query whether this camera data has been saved based on the device sn and camera location.
+     * @param deviceSn
+     * @param cameraIndex
+     * @return
+     */
+    Boolean checkExist(String deviceSn, String cameraIndex);
+
+    /**
+     * Delete all live capability data for this device based on the device sn.
+     * @param deviceSn
+     * @return
+     */
+    Boolean deleteCapacityCameraByDeviceSn(String deviceSn);
+
+    /**
+     * Save the live capability data of the device.
+     * @param capacityCameraReceivers
+     * @param deviceSn
+     * @return
+     */
+    Boolean saveCapacityCameraReceiverList(List<CapacityCameraReceiver> capacityCameraReceivers, String deviceSn);
+
+}
diff --git a/src/main/java/com/dji/sample/manage/service/IDeviceDictionaryService.java b/src/main/java/com/dji/sample/manage/service/IDeviceDictionaryService.java
new file mode 100644
index 0000000..65324ac
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/service/IDeviceDictionaryService.java
@@ -0,0 +1,24 @@
+package com.dji.sample.manage.service;
+
+import com.dji.sample.manage.model.dto.DeviceDictionaryDTO;
+
+import java.util.Optional;
+
+/**
+ * @author sean.zhou
+ * @version 0.1
+ * @date 2021/11/15
+ */
+public interface IDeviceDictionaryService {
+
+    /**
+     * Query the type data of the device based on domain, device type and sub type.
+     * @param domain
+     * @param deviceType
+     * @param subType
+     * @return
+     */
+    Optional<DeviceDictionaryDTO> getOneDictionaryInfoByDomainTypeSubType(
+            Integer domain, Integer deviceType, Integer subType);
+
+}
diff --git a/src/main/java/com/dji/sample/manage/service/IDevicePayloadService.java b/src/main/java/com/dji/sample/manage/service/IDevicePayloadService.java
new file mode 100644
index 0000000..2eb5c0e
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/service/IDevicePayloadService.java
@@ -0,0 +1,48 @@
+package com.dji.sample.manage.service;
+
+import com.dji.sample.manage.model.dto.DevicePayloadDTO;
+import com.dji.sample.manage.model.receiver.DevicePayloadReceiver;
+
+import java.util.List;
+
+/**
+ * @author sean.zhou
+ * @date 2021/11/19
+ * @version 0.1
+ */
+public interface IDevicePayloadService {
+
+    /**
+     * Query if the payload has been saved based on the sn of the payload.
+     * @param payloadSn
+     * @return
+     */
+    Integer checkPayloadExist(String payloadSn);
+
+    /**
+     * Save all payload data.
+     * @param payloadReceiverList
+     * @return
+     */
+    Boolean savePayloadDTOs(List<DevicePayloadReceiver> payloadReceiverList);
+
+    /**
+     * Save a payload data.
+     * @param payloadReceiver
+     * @return
+     */
+    Integer saveOnePayloadDTO(DevicePayloadReceiver payloadReceiver);
+
+    /**
+     * Query all payload data on this device based on the device sn.
+     * @param deviceSn
+     * @return
+     */
+    List<DevicePayloadDTO> getDevicePayloadEntitiesByDeviceSn(String deviceSn);
+
+    /**
+     * Delete all payload data on these devices based on the collection of device sns.
+     * @param deviceSns
+     */
+    void deletePayloadsByDeviceSn(List<String> deviceSns);
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/manage/service/IDeviceService.java b/src/main/java/com/dji/sample/manage/service/IDeviceService.java
new file mode 100644
index 0000000..444d49d
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/service/IDeviceService.java
@@ -0,0 +1,135 @@
+package com.dji.sample.manage.service;
+
+import com.dji.sample.component.mqtt.model.CommonTopicResponse;
+import com.dji.sample.component.websocket.config.ConcurrentWebSocketSession;
+import com.dji.sample.manage.model.dto.DeviceDTO;
+import com.dji.sample.manage.model.dto.TopologyDeviceDTO;
+import com.dji.sample.manage.model.param.DeviceQueryParam;
+import com.dji.sample.manage.model.receiver.StatusGatewayReceiver;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * @author sean.zhou
+ * @date 2021/11/10
+ * @version 0.1
+ */
+public interface IDeviceService {
+
+    /**
+     * The device goes online.
+     * @param deviceGateway gateway
+     * @return Whether the online is successful.
+     */
+    Boolean deviceOnline(StatusGatewayReceiver deviceGateway);
+
+    /**
+     * The device goes offline.
+     * @param gatewaySn
+     * @return Whether the offline is successful.
+     */
+    Boolean deviceOffline(String gatewaySn);
+
+    /**
+     * The aircraft goes offline.
+     * @param deviceSn aircraft's SN
+     * @return Whether the offline is successful.
+     */
+    Boolean subDeviceOffline(String deviceSn);
+
+    /**
+     * When the device goes online, it needs to subscribe to topics.
+     * @param sn device's SN
+     */
+    void subscribeTopicOnline(String sn);
+
+    /**
+     * When the device goes offine, it needs to cancel the subscribed topics.
+     * @param sn device's SN
+     */
+    void unsubscribeTopicOffline(String sn);
+
+    /**
+     * Delete all device data according to the SN of the device.
+     * @param ids device's SN
+     * @return
+     */
+    Boolean delDeviceByDeviceSns(List<String> ids);
+
+    /**
+     * Obtain device data according to different query conditions.
+     * @param param query parameters
+     * @return
+     */
+    List<DeviceDTO> getDevicesByParams(DeviceQueryParam param);
+
+    /**
+     * When you receive a status topic message, you need to reply to it.
+     * @param sn   the target of sn
+     * @param response
+     */
+    void publishStatusReply(String sn, CommonTopicResponse<Object> response);
+
+    /**
+     * The business interface on the web side. Get all information about all devices in this workspace.
+     * @param workspaceId
+     * @return
+     */
+    List<DeviceDTO> getDevicesTopoForWeb(String workspaceId);
+
+    /**
+     * Set the remote controller and payloads information of the drone.
+     * @param device
+     */
+    void spliceDeviceTopo(DeviceDTO device);
+
+    /**
+     * Push the topology information to the pilot after one device is online.
+     * @param sessions  The collection of connection objects on the pilot side.
+     * @param sn
+     */
+    void pushDeviceOnlineTopo(Collection<ConcurrentWebSocketSession> sessions, String sn);
+
+    /**
+     * Query the information of the device according to the sn of the device.
+     * @param sn device sn
+     * @return
+     */
+    Optional<TopologyDeviceDTO> getDeviceTopoForPilot(String sn);
+
+    /**
+     * Convert individual device information into topology objects.
+     * @param device
+     * @return
+     */
+    TopologyDeviceDTO deviceConvertToTopologyDTO(DeviceDTO device);
+
+    /**
+     * When the server receives the request of any device online, offline and topology update in the same workspace,
+     * it also broadcasts a push of device online, offline and topology update to PILOT via websocket,
+     * and PILOT will get the device topology list again after receiving the push.
+     * @param workspaceId
+     * @param gatewaySn
+     */
+    void pushDeviceOfflineTopo(String workspaceId, String gatewaySn);
+
+    /**
+     * When the server receives the request of any device online, offline and topology update in the same workspace,
+     * it also broadcasts a push of device online, offline and topology update to PILOT via websocket,
+     * and PILOT will get the device topology list again after receiving the push.
+     * @param workspaceId
+     * @param deviceSn
+     * @param gatewaySn
+     */
+    void pushDeviceOnlineTopo(String workspaceId, String deviceSn, String gatewaySn);
+
+    /**
+     * Handle messages from the osd topic.
+     * @param topic     osd
+     * @param payload
+     */
+    void handleOSD(String topic, byte[] payload);
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/manage/service/ILiveStreamService.java b/src/main/java/com/dji/sample/manage/service/ILiveStreamService.java
new file mode 100644
index 0000000..09cba9e
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/service/ILiveStreamService.java
@@ -0,0 +1,51 @@
+package com.dji.sample.manage.service;
+
+import com.dji.sample.common.model.ResponseResult;
+import com.dji.sample.manage.model.dto.CapacityDeviceDTO;
+import com.dji.sample.manage.model.dto.LiveTypeDTO;
+import com.dji.sample.manage.model.receiver.CapacityDeviceReceiver;
+
+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.
+     * @param workspaceId
+     * @return
+     */
+    List<CapacityDeviceDTO> getLiveCapacity(String workspaceId);
+
+    /**
+     * Save live capability data from drone.
+     * @param capacityDeviceReceiver
+     * @return
+     */
+    Boolean saveLiveCapacity(CapacityDeviceReceiver capacityDeviceReceiver);
+
+    /**
+     * 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);
+}
diff --git a/src/main/java/com/dji/sample/manage/service/ITSAService.java b/src/main/java/com/dji/sample/manage/service/ITSAService.java
new file mode 100644
index 0000000..a444f92
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/service/ITSAService.java
@@ -0,0 +1,17 @@
+package com.dji.sample.manage.service;
+
+/**
+ * @author sean
+ * @version 0.3
+ * @date 2022/2/21
+ */
+public interface ITSAService {
+
+    /**
+     * Real-time push telemetry data.
+     * @param workspaceId
+     * @param osdData
+     * @param sn
+     */
+    void pushTelemetryData(String workspaceId, Object osdData, String sn);
+}
diff --git a/src/main/java/com/dji/sample/manage/service/ITopologyService.java b/src/main/java/com/dji/sample/manage/service/ITopologyService.java
new file mode 100644
index 0000000..e75a857
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/service/ITopologyService.java
@@ -0,0 +1,20 @@
+package com.dji.sample.manage.service;
+
+import com.dji.sample.manage.model.dto.TopologyDTO;
+
+import java.util.List;
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/12/8
+ */
+public interface ITopologyService {
+
+    /**
+     * Get the topology list of all devices in the workspace for pilot display.
+     * @param workspaceId
+     * @return
+     */
+    List<TopologyDTO> getDeviceTopology(String workspaceId);
+}
diff --git a/src/main/java/com/dji/sample/manage/service/IUserService.java b/src/main/java/com/dji/sample/manage/service/IUserService.java
new file mode 100644
index 0000000..2ceff96
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/service/IUserService.java
@@ -0,0 +1,32 @@
+package com.dji.sample.manage.service;
+
+import com.dji.sample.common.model.ResponseResult;
+import com.dji.sample.manage.model.dto.UserDTO;
+
+import java.util.Optional;
+
+public interface IUserService {
+
+    /**
+     * Query user's details based on username.
+     * @param username
+     * @param workspaceId
+     * @return
+     */
+    ResponseResult getUserByUsername(String username, String workspaceId);
+
+    /**
+     * Verify the username and password to log in.
+     * @param username
+     * @param password
+     * @return
+     */
+    ResponseResult userLogin(String username, String password);
+
+    /**
+     * Create a user object containing a new token.
+     * @param token
+     * @return
+     */
+    Optional<UserDTO> refreshToken(String token);
+}
diff --git a/src/main/java/com/dji/sample/manage/service/IWorkspaceService.java b/src/main/java/com/dji/sample/manage/service/IWorkspaceService.java
new file mode 100644
index 0000000..5defa10
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/service/IWorkspaceService.java
@@ -0,0 +1,23 @@
+package com.dji.sample.manage.service;
+
+
+import com.dji.sample.manage.model.dto.WorkspaceDTO;
+
+import java.util.Optional;
+
+public interface IWorkspaceService {
+
+    /**
+     * Query the workspace information based on the primary key id of the database.
+     * @param id primary key id
+     * @return
+     */
+    Optional<WorkspaceDTO> getWorkspaceById(int id);
+
+    /**
+     * Query the information of a workspace based on its workspace id.
+     * @param workspaceId
+     * @return
+     */
+    Optional<WorkspaceDTO> getWorkspaceByWorkspaceId(String workspaceId);
+}
diff --git a/src/main/java/com/dji/sample/manage/service/impl/AbstractTSAService.java b/src/main/java/com/dji/sample/manage/service/impl/AbstractTSAService.java
new file mode 100644
index 0000000..35597fa
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/service/impl/AbstractTSAService.java
@@ -0,0 +1,59 @@
+package com.dji.sample.manage.service.impl;
+
+import com.dji.sample.component.mqtt.model.TopicStateReceiver;
+import com.dji.sample.component.websocket.config.ConcurrentWebSocketSession;
+import com.dji.sample.component.websocket.model.BizCodeEnum;
+import com.dji.sample.component.websocket.model.CustomWebSocketMessage;
+import com.dji.sample.component.websocket.model.WebSocketManager;
+import com.dji.sample.component.websocket.service.ISendMessageService;
+import com.dji.sample.manage.model.dto.TelemetryDTO;
+import com.dji.sample.manage.model.enums.UserTypeEnum;
+import com.dji.sample.manage.service.ITSAService;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import java.util.Collection;
+
+/**
+ * @author sean
+ * @version 0.3
+ * @date 2022/2/21
+ */
+public abstract class AbstractTSAService implements ITSAService {
+
+    protected AbstractTSAService tsaService;
+
+    public AbstractTSAService(AbstractTSAService tsaService) {
+        this.tsaService = tsaService;
+    }
+
+    @Autowired
+    protected ISendMessageService sendMessageService;
+
+    @Override
+    public void pushTelemetryData(String workspaceId, Object osdData, String sn) {
+        // All connected accounts on the pilot side of this workspace.
+        Collection<ConcurrentWebSocketSession> pilotSessions = WebSocketManager
+                .getValueWithWorkspaceAndUserType(
+                        workspaceId, UserTypeEnum.PILOT.getVal());
+
+        TelemetryDTO telemetry = TelemetryDTO.builder()
+                .sn(sn)
+                .build();
+        CustomWebSocketMessage<TelemetryDTO> pilotMessage = CustomWebSocketMessage.<TelemetryDTO>builder()
+                .timestamp(System.currentTimeMillis())
+                .bizCode(BizCodeEnum.DEVICE_OSD.getCode())
+                .data(telemetry)
+                .build();
+
+        this.pushTelemetryData(pilotSessions, pilotMessage, osdData);
+    }
+
+    public abstract void pushTelemetryData(Collection<ConcurrentWebSocketSession> sessions,
+                                           CustomWebSocketMessage<TelemetryDTO> message, Object Object);
+
+    protected abstract void handleOSD(TopicStateReceiver receiver, String sn, String workspaceId, JsonNode hostNode,
+                                      Collection<ConcurrentWebSocketSession> webSessions, CustomWebSocketMessage wsMessage)
+            throws JsonProcessingException;
+}
diff --git a/src/main/java/com/dji/sample/manage/service/impl/CameraVideoServiceImpl.java b/src/main/java/com/dji/sample/manage/service/impl/CameraVideoServiceImpl.java
new file mode 100644
index 0000000..a2196e4
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/service/impl/CameraVideoServiceImpl.java
@@ -0,0 +1,99 @@
+package com.dji.sample.manage.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.dji.sample.manage.dao.ICameraVideoMapper;
+import com.dji.sample.manage.model.dto.CapacityVideoDTO;
+import com.dji.sample.manage.model.entity.CameraVideoEntity;
+import com.dji.sample.manage.model.receiver.CapacityVideoReceiver;
+import com.dji.sample.manage.service.ICameraVideoService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * @author sean.zhou
+ * @version 0.1
+ * @date 2021/11/19
+ */
+@Service
+@Transactional
+public class CameraVideoServiceImpl implements ICameraVideoService {
+
+    @Autowired
+    private ICameraVideoMapper mapper;
+
+    @Override
+    public List<CapacityVideoDTO> getCameraVideosByCameraId(Integer cameraId) {
+        return mapper.selectList(
+                new LambdaQueryWrapper<CameraVideoEntity>()
+                        .eq(CameraVideoEntity::getCameraId, cameraId))
+                .stream()
+                .map(this::entityConvertToDto)
+                .collect(Collectors.toList());
+    }
+
+    @Override
+    public Boolean deleteCameraVideosById(List<Integer> ids) {
+        if (ids.isEmpty()) {
+            return true;
+        }
+        return mapper.deleteBatchIds(ids) > 0;
+    }
+
+    @Override
+    public Boolean saveCameraVideoDTOList(List<CapacityVideoReceiver> capacityVideoReceivers, Integer cameraId) {
+        for (CapacityVideoReceiver videoDTO : capacityVideoReceivers) {
+            CameraVideoEntity videoEntity = videoDTOConvertToEntity(videoDTO);
+            videoEntity.setCameraId(cameraId);
+            int saveId = this.saveOneCameraVideoEntity(videoEntity);
+            if (saveId <= 0) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Save the live capability of the lens of this camera.
+     * @param entity lens data
+     * @return
+     */
+    private Integer saveOneCameraVideoEntity(CameraVideoEntity entity) {
+        return mapper.insert(entity) > 0 ? entity.getId() : 0;
+    }
+
+    /**
+     * Convert the received lens capability object into a database entity object.
+     * @param dto received lens object
+     * @return entity
+     */
+    private CameraVideoEntity videoDTOConvertToEntity(CapacityVideoReceiver dto) {
+        CameraVideoEntity.CameraVideoEntityBuilder builder = CameraVideoEntity.builder();
+        if (dto != null) {
+            builder
+                    .videoIndex(dto.getVideoIndex())
+                    .videoType(dto.getVideoType());
+        }
+        return builder.build();
+    }
+
+    /**
+     * Convert database entity objects into lens data transfer object.
+     * @param entity
+     * @return  data transfer object
+     */
+    private CapacityVideoDTO entityConvertToDto(CameraVideoEntity entity) {
+        CapacityVideoDTO.CapacityVideoDTOBuilder builder = CapacityVideoDTO.builder();
+
+        if (entity != null) {
+            builder
+                    .id(entity.getId())
+                    .index(entity.getVideoIndex())
+                    .type(entity.getVideoType());
+        }
+        return builder.build();
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/manage/service/impl/CapacityCameraServiceImpl.java b/src/main/java/com/dji/sample/manage/service/impl/CapacityCameraServiceImpl.java
new file mode 100644
index 0000000..7ac4fc4
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/service/impl/CapacityCameraServiceImpl.java
@@ -0,0 +1,172 @@
+package com.dji.sample.manage.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.dji.sample.manage.dao.ICapacityCameraMapper;
+import com.dji.sample.manage.model.dto.CapacityCameraDTO;
+import com.dji.sample.manage.model.dto.CapacityVideoDTO;
+import com.dji.sample.manage.model.dto.DeviceDictionaryDTO;
+import com.dji.sample.manage.model.entity.CapacityCameraEntity;
+import com.dji.sample.manage.model.enums.DeviceDomainEnum;
+import com.dji.sample.manage.model.receiver.CapacityCameraReceiver;
+import com.dji.sample.manage.service.ICameraVideoService;
+import com.dji.sample.manage.service.ICapacityCameraService;
+import com.dji.sample.manage.service.IDeviceDictionaryService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+/**
+ * @author sean.zhou
+ * @date 2021/11/19
+ * @version 0.1
+ */
+@Service
+@Transactional
+public class CapacityCameraServiceImpl implements ICapacityCameraService {
+
+    @Autowired
+    private ICapacityCameraMapper mapper;
+
+    @Autowired
+    private ICameraVideoService cameraVideoService;
+
+    @Autowired
+    private IDeviceDictionaryService dictionaryService;
+
+    @Override
+    public List<CapacityCameraDTO> getCapacityCameraByDeviceSn(String deviceSn) {
+        List<CapacityCameraDTO> capacityCamerasList = mapper.selectList(
+                new LambdaQueryWrapper<CapacityCameraEntity>()
+                        .eq(CapacityCameraEntity::getDeviceSn, deviceSn))
+                .stream()
+                .map(this::entityConvertToDto)
+                .collect(Collectors.toList());
+        capacityCamerasList.forEach(capacityCamera -> {
+            // Set the lens data for this camera.
+            capacityCamera.setVideosList(
+                    cameraVideoService.getCameraVideosByCameraId(capacityCamera.getId()));
+        });
+
+        return capacityCamerasList;
+    }
+
+    @Override
+    public Boolean checkExist(String deviceSn, String cameraIndex) {
+        return mapper.selectOne(
+                new LambdaQueryWrapper<CapacityCameraEntity>()
+                        .eq(CapacityCameraEntity::getDeviceSn, deviceSn)
+                        .eq(CapacityCameraEntity::getCameraIndex, cameraIndex)
+                        .last(" limit 1")) != null;
+    }
+
+    @Override
+    public Boolean deleteCapacityCameraByDeviceSn(String deviceSn) {
+
+        List<CapacityCameraDTO> capacityCamerasList = this.getCapacityCameraByDeviceSn(deviceSn);
+        // Return directly if no data exists in the database.
+        if (capacityCamerasList.isEmpty()) {
+            return true;
+        }
+
+        List<Integer> cameraIds = capacityCamerasList
+                .stream()
+                .map(CapacityCameraDTO::getId)
+                .collect(Collectors.toList());
+
+        List<Integer> videoIds = capacityCamerasList
+                .stream()
+                .flatMap(camera -> camera.getVideosList().stream())
+                .map(CapacityVideoDTO::getId)
+                .collect(Collectors.toList());
+
+        return mapper.deleteBatchIds(cameraIds) > 0 && cameraVideoService.deleteCameraVideosById(videoIds);
+    }
+
+    @Override
+    public Boolean saveCapacityCameraReceiverList(List<CapacityCameraReceiver> capacityCameraReceivers, String deviceSn) {
+        for (CapacityCameraReceiver cameraDTO : capacityCameraReceivers) {
+            CapacityCameraEntity cameraEntity = receiverConvertToEntity(cameraDTO);
+            cameraEntity.setDeviceSn(deviceSn);
+            int cameraId = this.saveOneCapacityCameraEntity(cameraEntity);
+            if (cameraId <= 0) {
+                continue;
+            }
+            boolean saveVideo = cameraVideoService.saveCameraVideoDTOList(
+                    cameraDTO.getVideosList(), cameraId);
+
+            if (!saveVideo) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Save the camera live capability data of the device.
+     * @param cameraEntity
+     * @return
+     */
+    private Integer saveOneCapacityCameraEntity(CapacityCameraEntity cameraEntity) {
+        boolean exist = checkExist(
+                cameraEntity.getDeviceSn(), cameraEntity.getCameraIndex());
+        if (exist) {
+            return -1;
+        }
+        return mapper.insert(cameraEntity) > 0 ? cameraEntity.getId() : 0;
+    }
+
+    /**
+     *  Convert the received camera capability object into a database entity object.
+     * @param receiver
+     * @return
+     */
+    private CapacityCameraEntity receiverConvertToEntity(CapacityCameraReceiver receiver) {
+        CapacityCameraEntity.CapacityCameraEntityBuilder builder = CapacityCameraEntity.builder();
+        if (receiver == null) {
+            return builder.build();
+        }
+        int[] indexArr = Arrays.stream(receiver.getCameraIndex().split("-"))
+                .map(Integer::valueOf)
+                .mapToInt(Integer::intValue)
+                .toArray();
+        // The cameraIndex consists of type and subType and the index of the payload hanging on the drone.
+        // type-subType-index
+        if (indexArr.length == 3) {
+            Optional<DeviceDictionaryDTO> dictionaryOpt = dictionaryService
+                    .getOneDictionaryInfoByDomainTypeSubType(
+                            DeviceDomainEnum.PAYLOAD.getVal(), indexArr[0], indexArr[1]);
+            dictionaryOpt.ifPresent(dictionary ->
+                    builder.name(dictionary.getDeviceName()));
+        }
+        return builder
+                .availableVideoNumber(receiver.getAvailableVideoNumber())
+                .coexistVideoNumberMax(receiver.getCoexistVideoNumberMax())
+                .cameraIndex(receiver.getCameraIndex())
+                .build();
+    }
+
+    /**
+     * Convert database entity objects into camera data transfer object.
+     * @param entity
+     * @return
+     */
+    private CapacityCameraDTO entityConvertToDto(CapacityCameraEntity entity) {
+        CapacityCameraDTO.CapacityCameraDTOBuilder builder = CapacityCameraDTO.builder();
+
+        if (entity != null) {
+            builder
+                .id(entity.getId())
+                .name(entity.getName())
+                .description(entity.getDescription())
+                .deviceSn(entity.getDeviceSn())
+                .index(entity.getCameraIndex())
+                .build();
+        }
+        return builder.build();
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/manage/service/impl/DeviceDictionaryServiceImpl.java b/src/main/java/com/dji/sample/manage/service/impl/DeviceDictionaryServiceImpl.java
new file mode 100644
index 0000000..60da3d5
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/service/impl/DeviceDictionaryServiceImpl.java
@@ -0,0 +1,59 @@
+package com.dji.sample.manage.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.dji.sample.manage.dao.IDeviceDictionaryMapper;
+import com.dji.sample.manage.model.dto.DeviceDictionaryDTO;
+import com.dji.sample.manage.model.entity.DeviceDictionaryEntity;
+import com.dji.sample.manage.service.IDeviceDictionaryService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Optional;
+
+/**
+ *
+ * @author sean.zhou
+ * @version 0.1
+ * @date 2021/11/15
+ */
+@Service
+@Transactional
+public class DeviceDictionaryServiceImpl implements IDeviceDictionaryService {
+
+    @Autowired
+    private IDeviceDictionaryMapper mapper;
+
+    @Override
+    public Optional<DeviceDictionaryDTO> getOneDictionaryInfoByDomainTypeSubType(
+            Integer domain, Integer deviceType, Integer subType) {
+        if (domain == null || deviceType == null || subType == null) {
+            return Optional.empty();
+        }
+        return Optional.ofNullable(
+                entityConvertToDTO(
+                        mapper.selectOne(
+                                new LambdaQueryWrapper<DeviceDictionaryEntity>()
+                                        .eq(DeviceDictionaryEntity::getDomain, domain)
+                                        .eq(DeviceDictionaryEntity::getDeviceType, deviceType)
+                                        .eq(DeviceDictionaryEntity::getSubType, subType))));
+    }
+
+    /**
+     * Convert database entity objects into dictionary data transfer object.
+     * @param entity
+     * @return
+     */
+    private DeviceDictionaryDTO entityConvertToDTO(DeviceDictionaryEntity entity) {
+        DeviceDictionaryDTO.DeviceDictionaryDTOBuilder builder = DeviceDictionaryDTO.builder();
+
+        if (entity != null) {
+            builder.deviceName(entity.getDeviceName())
+                    .deviceDesc(entity.getDeviceDesc())
+                    .deviceType(entity.getDeviceType())
+                    .domain(entity.getDomain())
+                    .subType(entity.getSubType());
+        }
+        return builder.build();
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/manage/service/impl/DeviceOSDServiceImpl.java b/src/main/java/com/dji/sample/manage/service/impl/DeviceOSDServiceImpl.java
new file mode 100644
index 0000000..606343c
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/service/impl/DeviceOSDServiceImpl.java
@@ -0,0 +1,69 @@
+package com.dji.sample.manage.service.impl;
+
+import com.dji.sample.component.mqtt.model.TopicStateReceiver;
+import com.dji.sample.component.websocket.config.ConcurrentWebSocketSession;
+import com.dji.sample.component.websocket.model.BizCodeEnum;
+import com.dji.sample.component.websocket.model.CustomWebSocketMessage;
+import com.dji.sample.manage.model.DeviceStatusManager;
+import com.dji.sample.manage.model.dto.TelemetryDTO;
+import com.dji.sample.manage.model.dto.TelemetryDeviceDTO;
+import com.dji.sample.manage.model.enums.DeviceDomainEnum;
+import com.dji.sample.manage.model.receiver.OsdSubDeviceReceiver;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.util.Collection;
+
+/**
+ * @author sean
+ * @version 0.3
+ * @date 2022/2/21
+ */
+@Service
+public class DeviceOSDServiceImpl extends AbstractTSAService {
+
+    protected DeviceOSDServiceImpl() {
+        super(null);
+    }
+
+    @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
+    protected void handleOSD(TopicStateReceiver receiver, String sn, String workspaceId, JsonNode hostNode,
+                             Collection<ConcurrentWebSocketSession> webSessions, CustomWebSocketMessage wsMessage) throws JsonProcessingException {
+        // Real-time update of device status in memory
+        DeviceStatusManager.STATUS_MANAGER.put(
+                DeviceDomainEnum.SUB_DEVICE.getVal() + "/" + sn, LocalDateTime.now());
+        wsMessage.setBizCode(BizCodeEnum.DEVICE_OSD.getCode());
+
+        ObjectMapper mapper = new ObjectMapper();
+        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+        OsdSubDeviceReceiver data = mapper.treeToValue(hostNode, OsdSubDeviceReceiver.class);
+
+        wsMessage.setData(data);
+
+        sendMessageService.sendBatch(webSessions, wsMessage);
+        this.pushTelemetryData(workspaceId, data, sn);
+    }
+}
diff --git a/src/main/java/com/dji/sample/manage/service/impl/DevicePayloadServiceImpl.java b/src/main/java/com/dji/sample/manage/service/impl/DevicePayloadServiceImpl.java
new file mode 100644
index 0000000..a88504f
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/service/impl/DevicePayloadServiceImpl.java
@@ -0,0 +1,157 @@
+package com.dji.sample.manage.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.dji.sample.manage.dao.IDevicePayloadMapper;
+import com.dji.sample.manage.model.dto.DeviceDictionaryDTO;
+import com.dji.sample.manage.model.dto.DevicePayloadDTO;
+import com.dji.sample.manage.model.entity.DevicePayloadEntity;
+import com.dji.sample.manage.model.enums.DeviceDomainEnum;
+import com.dji.sample.manage.model.receiver.DevicePayloadReceiver;
+import com.dji.sample.manage.service.ICapacityCameraService;
+import com.dji.sample.manage.service.IDeviceDictionaryService;
+import com.dji.sample.manage.service.IDevicePayloadService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+/**
+ * @author sean.zhou
+ * @version 0.1
+ * @date 2021/11/19
+ */
+@Service
+@Transactional
+public class DevicePayloadServiceImpl implements IDevicePayloadService {
+
+    @Autowired
+    private IDevicePayloadMapper mapper;
+
+    @Autowired
+    private IDeviceDictionaryService dictionaryService;
+
+    @Autowired
+    private ICapacityCameraService capacityCameraService;
+
+    @Override
+    public Integer checkPayloadExist(String payloadSn) {
+        DevicePayloadEntity devicePayload = mapper.selectOne(
+                new LambdaQueryWrapper<DevicePayloadEntity>()
+                        .eq(DevicePayloadEntity::getPayloadSn, payloadSn));
+        return devicePayload != null ? devicePayload.getId() : -1;
+    }
+
+    private Integer saveOnePayloadEntity(DevicePayloadEntity entity) {
+        int id = this.checkPayloadExist(entity.getPayloadSn());
+        // If it already exists, update the data directly.
+        if (id > 0) {
+            entity.setId(id);
+            return mapper.updateById(entity);
+        }
+        return mapper.insert(entity) > 0 ? entity.getId() : 0;
+    }
+
+    @Override
+    public Boolean savePayloadDTOs(List<DevicePayloadReceiver> payloadReceiverList) {
+        for (DevicePayloadReceiver payloadReceiver : payloadReceiverList) {
+            int payloadId = this.saveOnePayloadDTO(payloadReceiver);
+            if (payloadId <= 0) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public Integer saveOnePayloadDTO(DevicePayloadReceiver payloadReceiver) {
+        return this.saveOnePayloadEntity(payloadDTOConvertToEntity(payloadReceiver));
+    }
+
+    @Override
+    public List<DevicePayloadDTO> getDevicePayloadEntitiesByDeviceSn(String deviceSn) {
+        return mapper.selectList(
+                new LambdaQueryWrapper<DevicePayloadEntity>()
+                        .eq(DevicePayloadEntity::getDeviceSn, deviceSn))
+                .stream()
+                .map(this::payloadEntityConvertToDTO)
+                .collect(Collectors.toList());
+    }
+
+    @Override
+    public void deletePayloadsByDeviceSn(List<String> deviceSns) {
+        deviceSns.forEach(deviceSn -> {
+            mapper.delete(
+                    new LambdaQueryWrapper<DevicePayloadEntity>()
+                            .eq(DevicePayloadEntity::getDeviceSn, deviceSn));
+            capacityCameraService.deleteCapacityCameraByDeviceSn(deviceSn);
+        });
+    }
+
+    /**
+     * Convert database entity objects into payload data transfer object.
+     * @param entity
+     * @return
+     */
+    private DevicePayloadDTO payloadEntityConvertToDTO(DevicePayloadEntity entity) {
+        DevicePayloadDTO.DevicePayloadDTOBuilder builder = DevicePayloadDTO.builder();
+        if (entity != null) {
+            builder.payloadSn(entity.getPayloadSn())
+                    .payloadName(entity.getPayloadName())
+                    .payloadDesc(entity.getPayloadDesc())
+                    .payloadIndex(entity.getPayloadIndex());
+        }
+        return builder.build();
+    }
+
+    /**
+     * Convert the received payload object into a database entity object.
+     * @param dto   payload
+     * @return
+     */
+    private DevicePayloadEntity payloadDTOConvertToEntity(DevicePayloadReceiver dto) {
+        if (dto == null) {
+            return new DevicePayloadEntity();
+        }
+        DevicePayloadEntity.DevicePayloadEntityBuilder builder = DevicePayloadEntity.builder();
+
+        // The cameraIndex consists of type and subType and the index of the payload hanging on the drone.
+        // type-subType-index
+        String[] payloadIndexArr = dto.getPayloadIndex().split("-");
+        try {
+            int[] arr = Arrays.stream(payloadIndexArr)
+                    .map(Integer::valueOf)
+                    .mapToInt(Integer::intValue)
+                    .toArray();
+
+            if (arr.length == 3) {
+                Optional<DeviceDictionaryDTO> dictionaryOpt = dictionaryService
+                        .getOneDictionaryInfoByDomainTypeSubType(DeviceDomainEnum.PAYLOAD.getVal(),
+                                arr[0], arr[1]);
+                dictionaryOpt.ifPresent(dictionary ->
+                        builder.payloadName(dictionary.getDeviceName())
+                                .payloadDesc(dictionary.getDeviceDesc()));
+
+            }
+            builder.payloadType(arr[0])
+                    .subType(arr[1])
+                    .payloadIndex(arr[2]);
+        } catch (NumberFormatException e) {
+            builder.payloadType(Integer.valueOf(payloadIndexArr[0]))
+                    .subType(-1)
+                    .payloadIndex(Integer.valueOf(payloadIndexArr[2]));
+        }
+
+        return builder
+                .payloadSn(dto.getSn())
+                .version(dto.getVersion())
+                .deviceSn(dto.getSn()
+                        .substring(0,
+                                dto.getSn().indexOf("-")))
+                .build();
+    }
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/manage/service/impl/DeviceServiceImpl.java b/src/main/java/com/dji/sample/manage/service/impl/DeviceServiceImpl.java
new file mode 100644
index 0000000..80e34df
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/service/impl/DeviceServiceImpl.java
@@ -0,0 +1,553 @@
+package com.dji.sample.manage.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.dji.sample.component.mqtt.model.CommonTopicResponse;
+import com.dji.sample.component.mqtt.model.TopicStateReceiver;
+import com.dji.sample.component.mqtt.service.IMessageSenderService;
+import com.dji.sample.component.mqtt.service.IMqttTopicService;
+import com.dji.sample.component.websocket.config.ConcurrentWebSocketSession;
+import com.dji.sample.component.websocket.model.BizCodeEnum;
+import com.dji.sample.component.websocket.model.CustomWebSocketMessage;
+import com.dji.sample.component.websocket.model.WebSocketManager;
+import com.dji.sample.component.websocket.service.ISendMessageService;
+import com.dji.sample.manage.dao.IDeviceMapper;
+import com.dji.sample.manage.model.dto.*;
+import com.dji.sample.manage.model.entity.DeviceEntity;
+import com.dji.sample.manage.model.enums.DeviceDomainEnum;
+import com.dji.sample.manage.model.enums.IconUrlEnum;
+import com.dji.sample.manage.model.enums.UserTypeEnum;
+import com.dji.sample.manage.model.param.DeviceQueryParam;
+import com.dji.sample.manage.model.receiver.StatusGatewayReceiver;
+import com.dji.sample.manage.model.receiver.StatusSubDeviceReceiver;
+import com.dji.sample.manage.service.IDeviceDictionaryService;
+import com.dji.sample.manage.service.IDevicePayloadService;
+import com.dji.sample.manage.service.IDeviceService;
+import com.dji.sample.manage.service.IWorkspaceService;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+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 org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+
+import static com.dji.sample.component.mqtt.model.TopicConst.*;
+
+/**
+ *
+ * @author sean.zhou
+ * @version 0.1
+ * @date 2021/11/10
+ */
+@Service
+@Slf4j
+@Transactional
+public class DeviceServiceImpl implements IDeviceService {
+
+    @Autowired
+    private IMessageSenderService messageSender;
+
+    @Autowired
+    private IDeviceMapper mapper;
+
+    @Autowired
+    private IDeviceDictionaryService dictionaryService;
+
+    @Autowired
+    private IMqttTopicService topicService;
+
+    @Autowired
+    private IWorkspaceService workspaceService;
+
+    @Autowired
+    private IDevicePayloadService payloadService;
+
+    @Autowired
+    private ISendMessageService sendMessageService;
+
+    @Autowired
+    @Qualifier("gatewayOSDServiceImpl")
+    private AbstractTSAService tsaService;
+
+    @Override
+    public Boolean deviceOffline(String gatewaySn) {
+        List<DeviceDTO> gatewaysList = this.getDevicesByParams(
+                DeviceQueryParam.builder()
+                        .deviceSn(gatewaySn)
+                        .build());
+
+        // If no information about this gateway device exists in the database, the drone is considered to be offline.
+        if (gatewaysList.isEmpty()) {
+            log.debug("The drone is already offline.");
+            return true;
+        }
+        // Handle the drone connected to the gateway device offline.
+        return this.subDeviceOffline(gatewaysList.get(0).getChildDeviceSn());
+    }
+
+    @Override
+    public Boolean subDeviceOffline(String deviceSn) {
+        // Cancel drone-related subscriptions.
+        this.unsubscribeTopicOffline(deviceSn);
+
+        List<DeviceDTO> devicesList = this.getDevicesByParams(
+                DeviceQueryParam.builder()
+                        .deviceSn(deviceSn)
+                        .build());
+
+        // If no information about this drone exists in the database, the drone is considered to be offline.
+        if (devicesList.isEmpty()) {
+            log.debug("{} is already offline.", deviceSn);
+            return true;
+        }
+
+        List<String> ids = devicesList.stream()
+                .map(DeviceDTO::getDeviceSn)
+                .collect(Collectors.toList());
+
+        // Delete all data related to the drone.
+        boolean isDel = this.delDeviceByDeviceSns(ids);
+        payloadService.deletePayloadsByDeviceSn(ids);
+
+        log.debug("{} offline status: {}.", deviceSn, isDel);
+        return isDel;
+    }
+
+    @Override
+    public Boolean deviceOnline(StatusGatewayReceiver deviceGateway) {
+        String deviceSn = deviceGateway.getSubDevices().get(0).getSn();
+
+        List<DeviceDTO> devicesList = this.getDevicesByParams(
+                DeviceQueryParam.builder()
+                        .deviceSn(deviceSn)
+                        .build());
+        // If the information about this drone exists in the database, the drone is considered to be online.
+        if (!devicesList.isEmpty()) {
+            log.warn("{} is already online.", deviceSn);
+            // Subscribe to topic related to drone and gateway devices.
+            this.subscribeTopicOnline(deviceGateway.getSn());
+            this.subscribeTopicOnline(deviceSn);
+            return true;
+        }
+
+        // Delete the gateway device information that was previously bound to the drone.
+        this.delDeviceByDeviceSns(
+                this.getDevicesByParams(
+                        DeviceQueryParam.builder()
+                                .childSn(deviceSn)
+                                .build())
+                        .stream()
+                        .map(DeviceDTO::getDeviceSn)
+                        .collect(Collectors.toList()));
+
+        DeviceEntity gateway = deviceGatewayConvertToDeviceEntity(deviceGateway);
+        gateway.setWorkspaceId(WorkspaceDTO.DEFAULT_WORKSPACE_ID);
+        // Set the icon of the gateway device displayed in the pilot's map, required in the TSA module.
+        gateway.setUrlNormal(IconUrlEnum.NORMAL_PERSON.getUrl());
+        // Set the icon of the gateway device displayed in the pilot's map when it is selected, required in the TSA module.
+        gateway.setUrlSelect(IconUrlEnum.SELECT_PERSON.getUrl());
+
+        DeviceEntity subDevice = subDeviceConvertToDeviceEntity(deviceGateway.getSubDevices().get(0));
+        subDevice.setWorkspaceId(WorkspaceDTO.DEFAULT_WORKSPACE_ID);
+        // Set the icon of the drone device displayed in the pilot's map, required in the TSA module.
+        subDevice.setUrlSelect(IconUrlEnum.SELECT_EQUIPMENT.getUrl());
+        // Set the icon of the drone device displayed in the pilot's map when it is selected, required in the TSA module.
+        subDevice.setUrlNormal(IconUrlEnum.NORMAL_EQUIPMENT.getUrl());
+
+        gateway.setChildSn(subDevice.getDeviceSn());
+
+        boolean isSave = this.saveDevice(gateway) > 0 && this.saveDevice(subDevice) > 0;
+
+        log.debug(subDevice.getDeviceSn() + " online status: {}", isSave);
+        if (isSave) {
+            // Subscribe to topic related to drone and gateway devices.
+            this.subscribeTopicOnline(subDevice.getDeviceSn());
+            this.subscribeTopicOnline(gateway.getDeviceSn());
+
+        }
+        return isSave;
+    }
+
+    @Override
+    public void subscribeTopicOnline(String sn) {
+        String[] subscribedTopic = topicService.getSubscribedTopic();
+        for (String s : subscribedTopic) {
+            // If you have already subscribed to the topic of the device, you do not need to subscribe again.
+            if (s.contains(sn)) {
+                return;
+            }
+        }
+        topicService.subscribe(THING_MODEL_PRE + PRODUCT + sn + OSD_SUF);
+        topicService.subscribe(THING_MODEL_PRE + PRODUCT + sn + STATE_SUF);
+        topicService.subscribe(THING_MODEL_PRE + PRODUCT + sn + SERVICES_SUF + _REPLY_SUF);
+    }
+
+    @Override
+    public void unsubscribeTopicOffline(String sn) {
+        topicService.unsubscribe(THING_MODEL_PRE + PRODUCT + sn + OSD_SUF);
+        topicService.unsubscribe(THING_MODEL_PRE + PRODUCT + sn + STATE_SUF);
+        topicService.unsubscribe(THING_MODEL_PRE + PRODUCT + sn + SERVICES_SUF + _REPLY_SUF);
+    }
+
+    @Override
+    public Boolean delDeviceByDeviceSns(List<String> ids) {
+        if (CollectionUtils.isEmpty(ids)) {
+            return true;
+        }
+        return mapper.delete(new LambdaQueryWrapper<DeviceEntity>()
+                .in(DeviceEntity::getDeviceSn, ids))
+                > 0;
+    }
+
+    @Override
+    public void publishStatusReply(String sn, CommonTopicResponse<Object> response) {
+        Map<String, Integer> result = new ConcurrentHashMap<>(1);
+        result.put("result", 0);
+        response.setData(result);
+
+        messageSender.publish(
+                new StringBuilder()
+                        .append(BASIC_PRE)
+                        .append(PRODUCT)
+                        .append(sn)
+                        .append(STATUS_SUF)
+                        .append(_REPLY_SUF)
+                        .toString(),
+                response);
+    }
+
+    @Override
+    public List<DeviceDTO> getDevicesByParams(DeviceQueryParam param) {
+        return mapper.selectList(
+                new LambdaQueryWrapper<DeviceEntity>()
+                        .eq(StringUtils.hasText(param.getDeviceSn()),
+                                    DeviceEntity::getDeviceSn, param.getDeviceSn())
+                        .eq(param.getDeviceType() != null,
+                                DeviceEntity::getDeviceType, param.getDeviceType())
+                        .eq(param.getSubType() != null,
+                                DeviceEntity::getSubType, param.getSubType())
+                        .eq(StringUtils.hasText(param.getChildSn()),
+                                DeviceEntity::getChildSn, param.getChildSn())
+                        .eq(param.getDomain() != null,
+                                DeviceEntity::getDomain, param.getDomain())
+                        .eq(StringUtils.hasText(param.getWorkspaceId()),
+                                DeviceEntity::getWorkspaceId, param.getWorkspaceId())
+                        .orderBy(param.isOrderBy(),
+                                param.isAsc(), DeviceEntity::getId))
+                .stream()
+                .map(this::deviceEntityConvertToDTO)
+                .collect(Collectors.toList());
+    }
+
+    @Override
+    public List<DeviceDTO> getDevicesTopoForWeb(String workspaceId) {
+        List<DeviceDTO> devicesList = this.getDevicesByParams(
+                DeviceQueryParam.builder()
+                        .workspaceId(workspaceId)
+                        .domain(DeviceDomainEnum.SUB_DEVICE.getVal())
+                        .build());
+
+        devicesList.forEach(device -> {
+            this.spliceDeviceTopo(device);
+            device.setWorkspaceId(workspaceId);
+
+        });
+        return devicesList;
+    }
+
+    @Override
+    public void spliceDeviceTopo(DeviceDTO device) {
+
+        // remote controller
+        List<DeviceDTO> gatewaysList = getDevicesByParams(
+                DeviceQueryParam.builder()
+                        .childSn(device.getDeviceSn())
+                        .build());
+
+        // payloads
+        List<DevicePayloadDTO> payloadsList = payloadService
+                .getDevicePayloadEntitiesByDeviceSn(device.getDeviceSn());
+
+
+        device.setGatewaysList(gatewaysList);
+        device.setPayloadsList(payloadsList);
+
+    }
+
+    @Override
+    public Optional<TopologyDeviceDTO> getDeviceTopoForPilot(String sn) {
+        List<TopologyDeviceDTO> topologyDeviceList = this.getDevicesByParams(
+                DeviceQueryParam.builder()
+                        .deviceSn(sn)
+                        .build())
+                .stream()
+                .map(this::deviceConvertToTopologyDTO)
+                .collect(Collectors.toList());
+        if (topologyDeviceList.isEmpty()) {
+            return Optional.empty();
+        }
+        return Optional.of(topologyDeviceList.get(0));
+    }
+
+    @Override
+    public void pushDeviceOnlineTopo(Collection<ConcurrentWebSocketSession> sessions, String sn) {
+
+        CustomWebSocketMessage<TopologyDeviceDTO> pilotMessage =
+                CustomWebSocketMessage.<TopologyDeviceDTO>builder()
+                        .timestamp(System.currentTimeMillis())
+                        .bizCode(BizCodeEnum.DEVICE_ONLINE.getCode())
+                        .data(new TopologyDeviceDTO())
+                        .build();
+
+        this.getDeviceTopoForPilot(sn)
+                .ifPresent(pilotMessage::setData);
+
+        sendMessageService.sendBatch(sessions, pilotMessage);
+    }
+
+    @Override
+    public TopologyDeviceDTO deviceConvertToTopologyDTO(DeviceDTO device) {
+        TopologyDeviceDTO.TopologyDeviceDTOBuilder builder = TopologyDeviceDTO.builder();
+
+        if (device != null) {
+            String domain = String.valueOf(DeviceDomainEnum.getVal(device.getDomain()));
+            String subType = String.valueOf(device.getSubType());
+            String type = String.valueOf(device.getType());
+
+            builder.sn(device.getDeviceSn())
+                    .deviceCallsign(device.getDeviceName())
+                    .deviceModel(DeviceModelDTO.builder()
+                            .domain(domain)
+                            .subType(subType)
+                            .type(type)
+                            .key(domain + "-" + type + "-" + subType)
+                            .build())
+                    .iconUrls(device.getIconUrl())
+                    .build();
+        }
+        return builder.build();
+    }
+
+    @Override
+    public void pushDeviceOnlineTopo(String workspaceId, String deviceSn, String gatewaySn) {
+
+        // All connected accounts on the pilot side of this workspace.
+        Collection<ConcurrentWebSocketSession> pilotSessions = WebSocketManager
+                .getValueWithWorkspaceAndUserType(
+                        workspaceId, UserTypeEnum.PILOT.getVal());
+
+        this.pushDeviceOnlineTopo(pilotSessions, deviceSn);
+        this.pushDeviceOnlineTopo(pilotSessions, gatewaySn);
+        this.pushDeviceUpdateTopo(pilotSessions, deviceSn);
+        this.pushDeviceUpdateTopo(pilotSessions, gatewaySn);
+    }
+
+    @Override
+    public void pushDeviceOfflineTopo(String workspaceId, String gatewaySn) {
+        // All connected accounts on the pilot side of this workspace.
+        Collection<ConcurrentWebSocketSession> pilotSessions = WebSocketManager
+                .getValueWithWorkspaceAndUserType(
+                        workspaceId, UserTypeEnum.PILOT.getVal());
+
+
+        List<DeviceDTO> gatewaysList = this.getDevicesByParams(
+                DeviceQueryParam.builder()
+                        .deviceSn(gatewaySn)
+                        .build());
+
+        if (!gatewaysList.isEmpty()) {
+            String deviceSn = gatewaysList.get(0).getChildDeviceSn();
+            this.pushDeviceOfflineTopo(pilotSessions, deviceSn);
+            this.pushDeviceUpdateTopo(pilotSessions, deviceSn);
+        }
+
+        this.pushDeviceOfflineTopo(pilotSessions, gatewaySn);
+        this.pushDeviceUpdateTopo(pilotSessions, gatewaySn);
+    }
+
+    @Override
+    public void handleOSD(String topic, byte[] payload) {
+        TopicStateReceiver receiver;
+        try {
+            String from = topic.substring((THING_MODEL_PRE + PRODUCT).length(),
+                    topic.indexOf(OSD_SUF));
+
+            List<DeviceDTO> deviceList = this.getDevicesByParams(
+                    DeviceQueryParam.builder().deviceSn(from).build());
+            if (deviceList.isEmpty()) {
+                return;
+            }
+
+            ObjectMapper mapper = new ObjectMapper();
+            mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+
+            receiver = mapper.readValue(payload, TopicStateReceiver.class);
+
+            CustomWebSocketMessage wsMessage = CustomWebSocketMessage.builder()
+                    .timestamp(System.currentTimeMillis()).build();
+
+            JsonNode hostNode = mapper.readTree(payload).findPath("data");
+
+            String workspaceId = deviceList.get(0).getWorkspaceId();
+            Collection<ConcurrentWebSocketSession> webSessions = WebSocketManager
+                    .getValueWithWorkspaceAndUserType(
+                            workspaceId, UserTypeEnum.WEB.getVal());
+
+
+            tsaService.handleOSD(receiver, from, workspaceId, hostNode, webSessions, wsMessage);
+
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * Notify the pilot side that there is an update of the device topology.
+     * @param sessions
+     * @param deviceSn
+     */
+    private void pushDeviceUpdateTopo(Collection<ConcurrentWebSocketSession> sessions, String deviceSn) {
+
+        CustomWebSocketMessage pilotMessage =
+                CustomWebSocketMessage.builder()
+                        .timestamp(System.currentTimeMillis())
+                        .bizCode(BizCodeEnum.DEVICE_UPDATE_TOPO.getCode())
+                        .data(new TopologyDeviceDTO())
+                        .build();
+        sendMessageService.sendBatch(sessions, pilotMessage);
+    }
+
+    /**
+     * Notify the pilot side that device is offline and needs to reacquire topology information.
+     * @param sessions
+     * @param sn
+     */
+    private void pushDeviceOfflineTopo(Collection<ConcurrentWebSocketSession> sessions, String sn) {
+        CustomWebSocketMessage<TopologyDeviceDTO> pilotMessage =
+                CustomWebSocketMessage.<TopologyDeviceDTO>builder()
+                        .timestamp(System.currentTimeMillis())
+                        .bizCode(BizCodeEnum.DEVICE_OFFLINE.getCode())
+                        .data(TopologyDeviceDTO.builder()
+                                .sn(sn)
+                                .onlineStatus(false)
+                                .build())
+                        .build();
+        sendMessageService.sendBatch(sessions, pilotMessage);
+    }
+
+    /**
+     * Save the device information and update the information directly if the device already exists.
+     * @param entity
+     * @return
+     */
+    private Integer saveDevice(DeviceEntity entity) {
+        DeviceEntity deviceEntity = mapper.selectOne(
+                new LambdaQueryWrapper<DeviceEntity>()
+                        .eq(DeviceEntity::getDeviceSn, entity.getDeviceSn()));
+        // Update the information directly if the device already exists.
+        if (deviceEntity != null) {
+            entity.setId(deviceEntity.getId());
+            mapper.updateById(entity);
+            return deviceEntity.getId();
+        }
+        return mapper.insert(entity) > 0 ? entity.getId() : 0;
+    }
+
+    /**
+     * Convert the received gateway device object into a database entity object.
+     * @param gateway
+     * @return
+     */
+    private DeviceEntity deviceGatewayConvertToDeviceEntity(StatusGatewayReceiver gateway) {
+        if (gateway == null) {
+            return new DeviceEntity();
+        }
+        DeviceEntity.DeviceEntityBuilder builder = DeviceEntity.builder();
+
+        // Query the model information of this gateway device.
+        Optional<DeviceDictionaryDTO> dictionaryOpt = dictionaryService
+                .getOneDictionaryInfoByDomainTypeSubType(gateway.getDomain(),
+                        gateway.getType(), gateway.getSubType());
+
+        dictionaryOpt.ifPresent(entity ->
+                builder.deviceName(entity.getDeviceName())
+                        .deviceDesc(entity.getDeviceDesc()));
+
+        return builder
+                .deviceSn(gateway.getSn())
+                .domain(gateway.getDomain())
+                .subType(gateway.getSubType())
+                .deviceType(gateway.getType())
+                .version(gateway.getVersion())
+                .build();
+    }
+
+    /**
+     * Convert the received drone device object into a database entity object.
+     * @param device
+     * @return
+     */
+    private DeviceEntity subDeviceConvertToDeviceEntity(StatusSubDeviceReceiver device) {
+        if (device == null) {
+            return new DeviceEntity();
+        }
+        DeviceEntity.DeviceEntityBuilder builder = DeviceEntity.builder();
+
+        // Query the model information of this drone device.
+        Optional<DeviceDictionaryDTO> dictionaryOpt = dictionaryService
+                .getOneDictionaryInfoByDomainTypeSubType(DeviceDomainEnum.SUB_DEVICE.getVal(),
+                        device.getType(), device.getSubType());
+
+        dictionaryOpt.ifPresent(dictionary ->
+                builder.deviceName(dictionary.getDeviceName())
+                        .deviceDesc(dictionary.getDeviceDesc()));
+
+        return builder
+                .deviceSn(device.getSn())
+                .deviceType(device.getType())
+                .subType(device.getSubType())
+                .domain(DeviceDomainEnum.SUB_DEVICE.getVal())
+                .version(device.getVersion())
+                .deviceIndex(device.getIndex())
+                .build();
+    }
+
+    /**
+     * Convert database entity objects into device data transfer object.
+     * @param entity
+     * @return
+     */
+    private DeviceDTO deviceEntityConvertToDTO(DeviceEntity entity) {
+        DeviceDTO.DeviceDTOBuilder builder = DeviceDTO.builder();
+
+        if (entity != null) {
+            builder.deviceSn(entity.getDeviceSn())
+                    .childDeviceSn(entity.getChildSn())
+                    .deviceName(entity.getDeviceName())
+                    .deviceDesc(entity.getDeviceDesc())
+                    .deviceIndex(entity.getDeviceIndex())
+                    .workspaceId(entity.getWorkspaceId())
+                    .type(entity.getDeviceType())
+                    .subType(entity.getSubType())
+                    .domain(DeviceDomainEnum.getDesc(entity.getDomain()))
+                    .iconUrl(IconUrlDTO.builder()
+                            .normalUrl(entity.getUrlNormal())
+                            .selectUrl(entity.getUrlSelect())
+                            .build())
+                    .build();
+        }
+        return builder.build();
+    }
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/manage/service/impl/GatewayOSDServiceImpl.java b/src/main/java/com/dji/sample/manage/service/impl/GatewayOSDServiceImpl.java
new file mode 100644
index 0000000..a69b9fe
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/service/impl/GatewayOSDServiceImpl.java
@@ -0,0 +1,74 @@
+package com.dji.sample.manage.service.impl;
+
+import com.dji.sample.component.mqtt.model.TopicStateReceiver;
+import com.dji.sample.component.websocket.config.ConcurrentWebSocketSession;
+import com.dji.sample.component.websocket.model.BizCodeEnum;
+import com.dji.sample.component.websocket.model.CustomWebSocketMessage;
+import com.dji.sample.manage.model.DeviceStatusManager;
+import com.dji.sample.manage.model.dto.TelemetryDTO;
+import com.dji.sample.manage.model.dto.TelemetryDeviceDTO;
+import com.dji.sample.manage.model.enums.DeviceDomainEnum;
+import com.dji.sample.manage.model.receiver.OsdGatewayReceiver;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.util.Collection;
+
+/**
+ * @author sean
+ * @version 0.3
+ * @date 2022/2/21
+ */
+@Service
+public class GatewayOSDServiceImpl extends AbstractTSAService {
+
+    public GatewayOSDServiceImpl(@Autowired @Qualifier("deviceOSDServiceImpl") 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
+    protected void handleOSD(TopicStateReceiver receiver, String sn, String workspaceId, JsonNode hostNode,
+                             Collection<ConcurrentWebSocketSession> webSessions, CustomWebSocketMessage wsMessage) throws JsonProcessingException {
+        if (sn.equals(receiver.getGateway())) {
+            // Real-time update of device status in memory
+            DeviceStatusManager.STATUS_MANAGER.put(DeviceDomainEnum.GATEWAY.getVal() + "/" + sn,
+                    LocalDateTime.now());
+
+            ObjectMapper mapper = new ObjectMapper();
+            mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+
+            wsMessage.setBizCode(BizCodeEnum.GATEWAY_OSD.getCode());
+            OsdGatewayReceiver data = mapper.treeToValue(hostNode, OsdGatewayReceiver.class);
+            wsMessage.setData(data);
+
+            this.sendMessageService.sendBatch(webSessions, wsMessage);
+
+            this.pushTelemetryData(workspaceId, data, sn);
+            return;
+        }
+
+        tsaService.handleOSD(receiver, sn, workspaceId, hostNode, webSessions, wsMessage);
+    }
+}
diff --git a/src/main/java/com/dji/sample/manage/service/impl/LiveStreamServiceImpl.java b/src/main/java/com/dji/sample/manage/service/impl/LiveStreamServiceImpl.java
new file mode 100644
index 0000000..371ad97
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/service/impl/LiveStreamServiceImpl.java
@@ -0,0 +1,326 @@
+package com.dji.sample.manage.service.impl;
+
+import com.dji.sample.common.error.LiveErrorEnum;
+import com.dji.sample.common.model.ResponseResult;
+import com.dji.sample.component.mqtt.model.CommonTopicReceiver;
+import com.dji.sample.component.mqtt.model.CommonTopicResponse;
+import com.dji.sample.component.mqtt.service.IMessageSenderService;
+import com.dji.sample.manage.model.Chan;
+import com.dji.sample.manage.model.dto.*;
+import com.dji.sample.manage.model.enums.DeviceDomainEnum;
+import com.dji.sample.manage.model.enums.LiveMethodEnum;
+import com.dji.sample.manage.model.enums.LiveUrlTypeEnum;
+import com.dji.sample.manage.model.enums.LiveVideoQualityEnum;
+import com.dji.sample.manage.model.param.DeviceQueryParam;
+import com.dji.sample.manage.model.receiver.CapacityDeviceReceiver;
+import com.dji.sample.manage.model.receiver.ServiceReplyReceiver;
+import com.dji.sample.manage.service.ICapacityCameraService;
+import com.dji.sample.manage.service.IDeviceService;
+import com.dji.sample.manage.service.ILiveStreamService;
+import com.dji.sample.manage.service.IWorkspaceService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+
+import java.lang.reflect.Field;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static com.dji.sample.component.mqtt.model.TopicConst.*;
+
+/**
+ * @author sean.zhou
+ * @date 2021/11/22
+ * @version 0.1
+ */
+@Service
+@Transactional
+public class LiveStreamServiceImpl implements ILiveStreamService {
+
+    @Autowired
+    private ICapacityCameraService capacityCameraService;
+
+    @Autowired
+    private IDeviceService deviceService;
+
+    @Autowired
+    private IWorkspaceService workspaceService;
+
+    @Autowired
+    private IMessageSenderService messageSender;
+
+    @Override
+    public List<CapacityDeviceDTO> getLiveCapacity(String workspaceId) {
+
+        // Query all drone data in this workspace.
+        List<DeviceDTO> devicesList = deviceService.getDevicesByParams(
+                DeviceQueryParam.builder()
+                        .workspaceId(workspaceId)
+                        .domain(DeviceDomainEnum.SUB_DEVICE.getVal())
+                        .build());
+
+        List<CapacityDeviceDTO> capacityDevicesList = new ArrayList<>();
+        // Query the live capability of each drone.
+        devicesList.forEach(device -> capacityDevicesList.add(CapacityDeviceDTO.builder()
+                .name(device.getDeviceName())
+                .sn(device.getDeviceSn())
+                .camerasList(capacityCameraService.getCapacityCameraByDeviceSn(device.getDeviceSn()))
+                .build()));
+
+        return capacityDevicesList;
+    }
+
+    @Override
+    public Boolean saveLiveCapacity(CapacityDeviceReceiver capacityDeviceReceiver) {
+        return capacityCameraService.saveCapacityCameraReceiverList(
+                capacityDeviceReceiver.getCameraList(),
+                capacityDeviceReceiver.getSn());
+    }
+
+    @Override
+    public ResponseResult liveStart(LiveTypeDTO liveParam) {
+        // Check if this lens is available live.
+        ResponseResult responseResult = this.checkBeforeLive(liveParam.getVideoId());
+        if (responseResult.getCode() != 0) {
+            return responseResult;
+        }
+
+        List<DeviceDTO> data = (List<DeviceDTO>)responseResult.getData();
+        // target topic
+        String respTopic = THING_MODEL_PRE + PRODUCT +
+                data.get(0).getDeviceSn() + SERVICES_SUF;
+        Optional<ServiceReplyReceiver> receiveReplyOpt = this.publishLiveStart(respTopic, liveParam);
+
+        if (receiveReplyOpt.isEmpty()) {
+            return ResponseResult.error(LiveErrorEnum.NO_REPLY);
+        }
+        if (receiveReplyOpt.get().getResult() != 0) {
+            return ResponseResult.error(LiveErrorEnum.find(receiveReplyOpt.get().getResult()));
+        }
+
+        LiveUrlTypeEnum urlType = LiveUrlTypeEnum.find(liveParam.getUrlType());
+        LiveDTO live = new LiveDTO();
+
+        switch (urlType) {
+            case RTMP:
+                live.setUrl(liveParam.getUrl().replace("rtmp", "webrtc"));
+                break;
+            case GB28181:
+                LiveUrlGB28181DTO gb28181 = urlToGB28181(liveParam.getUrl());
+                live.setUrl(new StringBuilder()
+                        .append("webrtc://")
+                        .append(gb28181.getServerIP())
+                        .append("/live/")
+                        .append(gb28181.getAgentID())
+                        .append("@")
+                        .append(gb28181.getChannel())
+                        .toString());
+                break;
+            case RTSP:
+                String url = receiveReplyOpt.get().getInfo();
+                this.resolveUrlUser(url, live);
+                break;
+            case UNKNOWN:
+                return ResponseResult.error(LiveErrorEnum.URL_TYPE_NOT_SUPPORTED);
+        }
+
+        return ResponseResult.success(live);
+    }
+
+    @Override
+    public ResponseResult liveStop(String videoId) {
+        ResponseResult<List<DeviceDTO>> responseResult = this.checkBeforeLive(videoId);
+        if (responseResult.getCode() != 0) {
+            return responseResult;
+        }
+
+        String respTopic = THING_MODEL_PRE + PRODUCT + responseResult.getData().get(0).getDeviceSn() + SERVICES_SUF;
+
+        Optional<ServiceReplyReceiver> receiveReplyOpt = this.publishLiveStop(respTopic, videoId);
+        if (receiveReplyOpt.isEmpty()) {
+            return ResponseResult.error(LiveErrorEnum.NO_REPLY);
+        }
+        if (receiveReplyOpt.get().getResult() != 0) {
+            return ResponseResult.error(LiveErrorEnum.find(receiveReplyOpt.get().getResult()));
+        }
+
+        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<List<DeviceDTO>> responseResult = this.checkBeforeLive(liveParam.getVideoId());
+
+        if (responseResult.getCode() != 0) {
+            return responseResult;
+        }
+
+        String respTopic = THING_MODEL_PRE + PRODUCT + responseResult.getData().get(0).getDeviceSn() + SERVICES_SUF;
+
+        Optional<ServiceReplyReceiver> receiveReplyOpt = this.publishLiveSetQuality(respTopic, liveParam);
+        if (receiveReplyOpt.isEmpty()) {
+            return ResponseResult.error(LiveErrorEnum.NO_REPLY);
+        }
+        if (receiveReplyOpt.get().getResult() != 0) {
+            return ResponseResult.error(LiveErrorEnum.find(receiveReplyOpt.get().getResult()));
+        }
+
+        return ResponseResult.success();
+    }
+
+    /**
+     * Check if this lens is available live.
+     * @param videoId
+     * @return
+     */
+    private ResponseResult checkBeforeLive(String videoId) {
+        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);
+        }
+
+        List<DeviceDTO> gatewayList = deviceService.getDevicesByParams(
+                DeviceQueryParam.builder()
+                        .childSn(videoIdArr[0])
+                        .build());
+        // Check if the gateway device connected to this drone exists
+        if (gatewayList.isEmpty()) {
+            return ResponseResult.error(LiveErrorEnum.NO_FLIGHT_CONTROL);
+        }
+        return ResponseResult.success(gatewayList);
+    }
+
+    /**
+     * 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 Optional<ServiceReplyReceiver> 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(LiveMethodEnum.LIVE_START_PUSH.getMethod());
+
+        return this.publishLive(topic, response);
+    }
+
+    /**
+     * Send a message to the pilot via mqtt to set quality.
+     * @param respTopic
+     * @param liveParam
+     * @return
+     */
+    private Optional<ServiceReplyReceiver> 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(LiveMethodEnum.LIVE_SET_QUALITY.getMethod());
+        response.setData(data);
+
+        return this.publishLive(respTopic, response);
+    }
+
+    /**
+     * Send a message to the pilot via mqtt to stop the live streaming.
+     * @param topic
+     * @param videoId
+     * @return
+     */
+    private Optional<ServiceReplyReceiver> 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(LiveMethodEnum.LIVE_STOP_PUSH.getMethod());
+
+        return this.publishLive(topic, response);
+    }
+
+    /**
+     * Send live streaming start message and receive a response at the same time
+     * @param topic
+     * @param response  notification of whether the start is successful.
+     * @return
+     */
+    private Optional<ServiceReplyReceiver> publishLive(String topic, CommonTopicResponse response) {
+        AtomicInteger time = new AtomicInteger(0);
+        // Retry three times
+        while (time.getAndIncrement() < 3) {
+            messageSender.publish(topic, response);
+
+            Chan<CommonTopicReceiver<ServiceReplyReceiver>> chan = Chan.getInstance();
+            // If the message is not received in 0.5 seconds then resend it again.
+            CommonTopicReceiver<ServiceReplyReceiver> receiver = chan.get(response.getMethod());
+            if (receiver == null) {
+                continue;
+            }
+            // Need to match tid and bid.
+            if (receiver.getTid().equals(response.getTid()) &&
+                    receiver.getBid().equals(response.getBid())) {
+                return Optional.ofNullable(receiver.getData());
+            }
+        }
+        return Optional.empty();
+    }
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/manage/service/impl/TopologyServiceImpl.java b/src/main/java/com/dji/sample/manage/service/impl/TopologyServiceImpl.java
new file mode 100644
index 0000000..aefc059
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/service/impl/TopologyServiceImpl.java
@@ -0,0 +1,55 @@
+package com.dji.sample.manage.service.impl;
+
+import com.dji.sample.manage.model.dto.DeviceDTO;
+import com.dji.sample.manage.model.dto.TopologyDTO;
+import com.dji.sample.manage.model.dto.TopologyDeviceDTO;
+import com.dji.sample.manage.model.enums.DeviceDomainEnum;
+import com.dji.sample.manage.model.param.DeviceQueryParam;
+import com.dji.sample.manage.service.IDeviceService;
+import com.dji.sample.manage.service.ITopologyService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/12/8
+ */
+@Service
+public class TopologyServiceImpl implements ITopologyService {
+
+    @Autowired
+    private IDeviceService deviceService;
+
+    @Override
+    public List<TopologyDTO> getDeviceTopology(String workspaceId) {
+        // Query the information of all gateway devices in the workspace.
+        List<DeviceDTO> gatewayList = deviceService.getDevicesByParams(
+                DeviceQueryParam.builder()
+                        .workspaceId(workspaceId)
+                        .domain(DeviceDomainEnum.GATEWAY.getVal())
+                        .build());
+
+        List<TopologyDTO> topologyList = new ArrayList<>();
+
+        gatewayList.forEach(device -> {
+            List<TopologyDeviceDTO> parents = new ArrayList<>();
+            TopologyDeviceDTO gateway = deviceService.deviceConvertToTopologyDTO(device);
+            parents.add(gateway);
+
+            // Query the topology data of the drone based on the drone sn.
+            Optional<TopologyDeviceDTO> deviceTopo = deviceService.getDeviceTopoForPilot(device.getChildDeviceSn());
+            List<TopologyDeviceDTO> deviceTopoList = new ArrayList<>();
+            deviceTopo.ifPresent(deviceTopoList::add);
+
+            topologyList.add(TopologyDTO.builder().parents(parents).hosts(deviceTopoList).build());
+        });
+
+        return topologyList;
+    }
+
+}
diff --git a/src/main/java/com/dji/sample/manage/service/impl/UserServiceImpl.java b/src/main/java/com/dji/sample/manage/service/impl/UserServiceImpl.java
new file mode 100644
index 0000000..c3c9a1b
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/service/impl/UserServiceImpl.java
@@ -0,0 +1,141 @@
+package com.dji.sample.manage.service.impl;
+
+import com.auth0.jwt.JWT;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.dji.sample.common.model.CustomClaim;
+import com.dji.sample.common.model.ResponseResult;
+import com.dji.sample.common.util.JwtUtil;
+import com.dji.sample.component.mqtt.config.MqttConfiguration;
+import com.dji.sample.manage.dao.IUserMapper;
+import com.dji.sample.manage.model.dto.UserDTO;
+import com.dji.sample.manage.model.dto.WorkspaceDTO;
+import com.dji.sample.manage.model.entity.UserEntity;
+import com.dji.sample.manage.service.IUserService;
+import com.dji.sample.manage.service.IWorkspaceService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+
+import java.util.Optional;
+
+@Service
+@Transactional
+public class UserServiceImpl implements IUserService {
+
+    @Autowired
+    private IUserMapper mapper;
+
+    @Autowired
+    private MqttConfiguration mqttConfiguration;
+
+    @Autowired
+    private IWorkspaceService workspaceService;
+
+    @Override
+    public ResponseResult getUserByUsername(String username, String workspaceId) {
+
+        UserEntity userEntity = this.getUserByUsername(username);
+        if (userEntity == null) {
+            return ResponseResult.builder()
+                    .code(HttpStatus.UNAUTHORIZED.value())
+                    .message("invalid username")
+                    .build();
+        }
+
+        UserDTO user = this.entityConvertToDTO(userEntity);
+        user.setWorkspaceId(workspaceId);
+
+        return ResponseResult.success(user);
+    }
+
+    @Override
+    public ResponseResult userLogin(String username, String password) {
+        // check user
+        UserEntity userEntity = this.getUserByUsername(username);
+        if (userEntity == null) {
+            return ResponseResult.builder()
+                    .code(HttpStatus.UNAUTHORIZED.value())
+                    .message("invalid username")
+                    .build();
+        }
+        if (!password.equals(userEntity.getPassword())) {
+            return ResponseResult.builder()
+                    .code(HttpStatus.UNAUTHORIZED.value())
+                    .message("invalid password")
+                    .build();
+        }
+
+        Optional<WorkspaceDTO> workspaceOpt = workspaceService.getWorkspaceById(userEntity.getWorkspaceId());
+        if (workspaceOpt.isEmpty()) {
+            return ResponseResult.builder()
+                    .code(HttpStatus.UNAUTHORIZED.value())
+                    .message("invalid workspace id")
+                    .build();
+        }
+
+        CustomClaim customClaim = new CustomClaim(userEntity.getUserId(),
+                userEntity.getUsername(), userEntity.getUserType(),
+                workspaceOpt.get().getWorkspaceId());
+
+        // create token
+        String token = JwtUtil.createToken(customClaim.convertToMap());
+
+        UserDTO userDTO = entityConvertToDTO(userEntity);
+        userDTO.setMqttAddr(new StringBuilder()
+                .append(mqttConfiguration.getProtocol().trim())
+                .append("://")
+                .append(mqttConfiguration.getHost().trim())
+                .append(":")
+                .append(mqttConfiguration.getPort())
+                .toString());
+        userDTO.setAccessToken(token);
+        userDTO.setWorkspaceId(workspaceOpt.get().getWorkspaceId());
+        return ResponseResult.success(userDTO);
+    }
+
+    @Override
+    public Optional<UserDTO> refreshToken(String token) {
+        if (!StringUtils.hasText(token)) {
+            return Optional.empty();
+        }
+        CustomClaim customClaim = new CustomClaim(JWT.decode(token).getClaims());
+        String refreshToken = JwtUtil.createToken(customClaim.convertToMap());
+
+        UserDTO user = entityConvertToDTO(this.getUserByUsername(customClaim.getUsername()));
+        user.setWorkspaceId(customClaim.getWorkspaceId());
+        user.setAccessToken(refreshToken);
+        return Optional.of(user);
+    }
+
+    /**
+     * Query a user by username.
+     * @param username
+     * @return
+     */
+    private UserEntity getUserByUsername(String username) {
+        return mapper.selectOne(new QueryWrapper<UserEntity>()
+                .eq("username", username));
+    }
+
+    private UserDTO entityConvertToDTO(UserEntity entity) {
+        if (entity == null) {
+            return new UserDTO();
+        }
+        return UserDTO.builder()
+                .userId(entity.getUserId())
+                .username(entity.getUsername())
+                .userType(entity.getUserType())
+                .mqttUsername(entity.getMqttUsername())
+                .mqttPassword(entity.getMqttPassword())
+                .mqttAddr(new StringBuilder()
+                        .append(mqttConfiguration.getProtocol().trim())
+                        .append("://")
+                        .append(mqttConfiguration.getHost().trim())
+                        .append(":")
+                        .append(mqttConfiguration.getPort())
+                        .toString())
+                .build();
+    }
+}
diff --git a/src/main/java/com/dji/sample/manage/service/impl/WorkspaceServiceImpl.java b/src/main/java/com/dji/sample/manage/service/impl/WorkspaceServiceImpl.java
new file mode 100644
index 0000000..d6ed3ab
--- /dev/null
+++ b/src/main/java/com/dji/sample/manage/service/impl/WorkspaceServiceImpl.java
@@ -0,0 +1,52 @@
+package com.dji.sample.manage.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.dji.sample.manage.dao.IWorkspaceMapper;
+import com.dji.sample.manage.model.dto.WorkspaceDTO;
+import com.dji.sample.manage.model.entity.WorkspaceEntity;
+import com.dji.sample.manage.service.IWorkspaceService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Optional;
+
+@Service
+@Transactional
+public class WorkspaceServiceImpl implements IWorkspaceService {
+
+    @Autowired
+    private IWorkspaceMapper mapper;
+
+    @Override
+    public Optional<WorkspaceDTO> getWorkspaceById(int id) {
+        return Optional.ofNullable(entityConvertToDto(mapper.selectById(id)));
+    }
+
+    @Override
+    public Optional<WorkspaceDTO> getWorkspaceByWorkspaceId(String workspaceId) {
+        return Optional.ofNullable(entityConvertToDto(
+                mapper.selectOne(
+                        new LambdaQueryWrapper<WorkspaceEntity>()
+                                .eq(WorkspaceEntity::getWorkspaceId, workspaceId))));
+    }
+
+    /**
+     * Convert database entity objects into workspace data transfer object.
+     * @param entity
+     * @return
+     */
+    private WorkspaceDTO entityConvertToDto(WorkspaceEntity entity) {
+        WorkspaceDTO.WorkspaceDTOBuilder builder = WorkspaceDTO.builder();
+        if (entity == null) {
+            return builder.build();
+        }
+        return builder
+                .id(entity.getId())
+                .workspaceId(entity.getWorkspaceId())
+                .platformName(entity.getPlatformName())
+                .workspaceDesc(entity.getWorkspaceDesc())
+                .workspaceName(entity.getWorkspaceName())
+                .build();
+    }
+}
diff --git a/src/main/java/com/dji/sample/map/controller/WorkspaceElementController.java b/src/main/java/com/dji/sample/map/controller/WorkspaceElementController.java
new file mode 100644
index 0000000..1ad52b8
--- /dev/null
+++ b/src/main/java/com/dji/sample/map/controller/WorkspaceElementController.java
@@ -0,0 +1,184 @@
+package com.dji.sample.map.controller;
+
+import com.dji.sample.common.model.CustomClaim;
+import com.dji.sample.common.model.ResponseResult;
+import com.dji.sample.component.websocket.model.BizCodeEnum;
+import com.dji.sample.component.websocket.model.CustomWebSocketMessage;
+import com.dji.sample.component.websocket.model.WebSocketManager;
+import com.dji.sample.component.websocket.service.ISendMessageService;
+import com.dji.sample.map.model.dto.*;
+import com.dji.sample.map.service.IWorkspaceElementService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+
+import static com.dji.sample.component.AuthInterceptor.TOKEN_CLAIM;
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/11/29
+ */
+@RestController
+@RequestMapping("${url.map.prefix}${url.map.version}/workspaces")
+public class WorkspaceElementController {
+
+    @Autowired
+    private IWorkspaceElementService elementService;
+
+    @Autowired
+    private ISendMessageService sendMessageService;
+
+    /**
+     * In the first connection, pilot will send out this http request to obtain the group element list.
+     * Also, if pilot receives a group refresh instruction from WebSocket,
+     * it needs the same interface to request the group element list.
+     * @param workspaceId
+     * @param groupId
+     * @param isDistributed
+     * @return
+     */
+    @GetMapping("/{workspace_id}/element-groups")
+    public ResponseResult<List<GroupDTO>> getAllElements(@PathVariable(name = "workspace_id") String workspaceId,
+                               @RequestParam(name = "group_id", required = false) String groupId,
+                               @RequestParam(name = "is_distributed", required = false) Boolean isDistributed) {
+        List<GroupDTO> groupsList = elementService.getAllGroupsByWorkspaceId(workspaceId, groupId, isDistributed);
+        return ResponseResult.success(groupsList);
+    }
+
+    /**
+     * When user draws a point, line or polygon on the PILOT/Web side.
+     * Save the element information to the database.
+     * @param request
+     * @param workspaceId
+     * @param groupId
+     * @param elementCreate
+     * @return
+     */
+    @PostMapping("/{workspace_id}/element-groups/{group_id}/elements")
+    public ResponseResult saveElement(HttpServletRequest request,
+                            @PathVariable(name = "workspace_id") String workspaceId,
+                            @PathVariable(name = "group_id") String groupId,
+                            @RequestBody ElementCreateDTO elementCreate) {
+        CustomClaim claims = (CustomClaim) request.getAttribute(TOKEN_CLAIM);
+        // Set the creator of the element
+        elementCreate.getResource().setUsername(claims.getUsername());
+
+        ResponseResult response = elementService.saveElement(groupId, elementCreate);
+        if (response.getCode() != ResponseResult.CODE_SUCCESS) {
+            return response;
+        }
+
+        // Notify all WebSocket connections in this workspace to be updated when an element is created.
+        elementService.getElementByElementId(elementCreate.getId())
+                .ifPresent(groupElement ->
+                        sendMessageService.sendBatch(
+                                WebSocketManager.getValueWithWorkspace(workspaceId),
+                                CustomWebSocketMessage.<GroupElementDTO>builder()
+                                        .timestamp(System.currentTimeMillis())
+                                        .bizCode(BizCodeEnum.MAP_ELEMENT_CREATE.getCode())
+                                        .data(groupElement)
+                                        .build()));
+
+        return ResponseResult.success(new ConcurrentHashMap<>(Map.of("id", elementCreate.getId())));
+    }
+
+    /**
+     * When user edits a point, line or polygon on the PILOT/Web side.
+     * Update the element information to the database.
+     * @param request
+     * @param workspaceId
+     * @param elementId
+     * @param elementUpdate
+     * @return
+     */
+    @PutMapping("/{workspace_id}/elements/{element_id}")
+    public ResponseResult updateElement(HttpServletRequest request,
+                              @PathVariable(name = "workspace_id") String workspaceId,
+                              @PathVariable(name = "element_id") String elementId,
+                              @RequestBody ElementUpdateDTO elementUpdate) {
+
+        CustomClaim claims = (CustomClaim) request.getAttribute(TOKEN_CLAIM);
+
+        ResponseResult response = elementService.updateElement(elementId, elementUpdate, claims.getUsername());
+        if (response.getCode() != ResponseResult.CODE_SUCCESS) {
+            return response;
+        }
+
+        // Notify all WebSocket connections in this workspace to update when there is an element update.
+        elementService.getElementByElementId(elementId)
+                .ifPresent(groupElement ->
+                        sendMessageService.sendBatch(
+                                WebSocketManager.getValueWithWorkspace(workspaceId),
+                                CustomWebSocketMessage.<GroupElementDTO>builder()
+                                        .timestamp(System.currentTimeMillis())
+                                        .bizCode(BizCodeEnum.MAP_ELEMENT_UPDATE.getCode())
+                                        .data(groupElement)
+                                        .build()));
+        return response;
+    }
+
+    /**
+     * When user delete a point, line or polygon on the PILOT/Web side,
+     * Delete the element information in the database.
+     * @param workspaceId
+     * @param elementId
+     * @return
+     */
+    @DeleteMapping("/{workspace_id}/elements/{element_id}")
+    public ResponseResult deleteElement(@PathVariable(name = "workspace_id") String workspaceId,
+                              @PathVariable(name = "element_id") String elementId) {
+
+        Optional<GroupElementDTO> elementOpt = elementService.getElementByElementId(elementId);
+
+        ResponseResult response = elementService.deleteElement(elementId);
+
+        // Notify all WebSocket connections in this workspace to update when an element is deleted.
+        if (ResponseResult.CODE_SUCCESS == response.getCode()) {
+            elementOpt.ifPresent(element ->
+                    sendMessageService.sendBatch(
+                    WebSocketManager.getValueWithWorkspace(workspaceId),
+                            CustomWebSocketMessage.<WebSocketElementDelDTO>builder()
+                                    .timestamp(System.currentTimeMillis())
+                                    .bizCode(BizCodeEnum.MAP_ELEMENT_DELETE.getCode())
+                                    .data(WebSocketElementDelDTO.builder()
+                                            .elementId(elementId)
+                                            .groupId(element.getGroupId())
+                                            .build())
+                                    .build()));
+        }
+        return response;
+    }
+
+    /**
+     * Delete all the element information in this group based on the group id.
+     * @param workspaceId
+     * @param groupId
+     * @return
+     */
+    @DeleteMapping("/{workspace_id}/element-groups/{group_id}/elements")
+    public ResponseResult deleteAllElementByGroupId(@PathVariable(name = "workspace_id") String workspaceId,
+                                          @PathVariable(name = "group_id") String groupId) {
+
+        ResponseResult response = elementService.deleteAllElementByGroupId(groupId);
+
+        // Notify all WebSocket connections in this workspace to update when elements are deleted.
+        if (ResponseResult.CODE_SUCCESS == response.getCode()) {
+
+            sendMessageService.sendBatch(
+                    WebSocketManager.getValueWithWorkspace(workspaceId),
+                    CustomWebSocketMessage.builder()
+                            .timestamp(System.currentTimeMillis())
+                            .bizCode(BizCodeEnum.MAP_GROUP_REFRESH.getCode())
+                            // Group ids that need to re-request data
+                            .data(new ConcurrentHashMap<String, String[]>(Map.of("ids", new String[]{groupId})))
+                            .build());
+        }
+        return response;
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/map/dao/IElementCoordinateMapper.java b/src/main/java/com/dji/sample/map/dao/IElementCoordinateMapper.java
new file mode 100644
index 0000000..5073ce5
--- /dev/null
+++ b/src/main/java/com/dji/sample/map/dao/IElementCoordinateMapper.java
@@ -0,0 +1,12 @@
+package com.dji.sample.map.dao;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.dji.sample.map.model.entity.ElementCoordinateEntity;
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/11/29
+ */
+public interface IElementCoordinateMapper extends BaseMapper<ElementCoordinateEntity> {
+}
diff --git a/src/main/java/com/dji/sample/map/dao/IGroupElementMapper.java b/src/main/java/com/dji/sample/map/dao/IGroupElementMapper.java
new file mode 100644
index 0000000..4b572c7
--- /dev/null
+++ b/src/main/java/com/dji/sample/map/dao/IGroupElementMapper.java
@@ -0,0 +1,12 @@
+package com.dji.sample.map.dao;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.dji.sample.map.model.entity.GroupElementEntity;
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/11/29
+ */
+public interface IGroupElementMapper extends BaseMapper<GroupElementEntity> {
+}
diff --git a/src/main/java/com/dji/sample/map/dao/IGroupMapper.java b/src/main/java/com/dji/sample/map/dao/IGroupMapper.java
new file mode 100644
index 0000000..747f7fd
--- /dev/null
+++ b/src/main/java/com/dji/sample/map/dao/IGroupMapper.java
@@ -0,0 +1,12 @@
+package com.dji.sample.map.dao;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.dji.sample.map.model.entity.GroupEntity;
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/11/29
+ */
+public interface IGroupMapper extends BaseMapper<GroupEntity> {
+}
diff --git a/src/main/java/com/dji/sample/map/model/dto/ContentPropertyDTO.java b/src/main/java/com/dji/sample/map/model/dto/ContentPropertyDTO.java
new file mode 100644
index 0000000..e8fe1d2
--- /dev/null
+++ b/src/main/java/com/dji/sample/map/model/dto/ContentPropertyDTO.java
@@ -0,0 +1,25 @@
+package com.dji.sample.map.model.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/11/30
+ */
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+public class ContentPropertyDTO {
+
+    private String color;
+
+    @JsonProperty("clampToGround")
+    private Boolean clampToGround;
+
+}
diff --git a/src/main/java/com/dji/sample/map/model/dto/ElementCoordinateDTO.java b/src/main/java/com/dji/sample/map/model/dto/ElementCoordinateDTO.java
new file mode 100644
index 0000000..e0799c4
--- /dev/null
+++ b/src/main/java/com/dji/sample/map/model/dto/ElementCoordinateDTO.java
@@ -0,0 +1,25 @@
+package com.dji.sample.map.model.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/11/30
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder
+public class ElementCoordinateDTO {
+
+    private Double longitude;
+
+    private Double latitude;
+
+    private Double altitude;
+
+}
diff --git a/src/main/java/com/dji/sample/map/model/dto/ElementCreateDTO.java b/src/main/java/com/dji/sample/map/model/dto/ElementCreateDTO.java
new file mode 100644
index 0000000..a594bba
--- /dev/null
+++ b/src/main/java/com/dji/sample/map/model/dto/ElementCreateDTO.java
@@ -0,0 +1,18 @@
+package com.dji.sample.map.model.dto;
+
+import lombok.Data;
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/11/30
+ */
+@Data
+public class ElementCreateDTO {
+
+    private String id;
+
+    private String name;
+
+    private ElementResourceDTO resource;
+}
diff --git a/src/main/java/com/dji/sample/map/model/dto/ElementLineStringDTO.java b/src/main/java/com/dji/sample/map/model/dto/ElementLineStringDTO.java
new file mode 100644
index 0000000..117790b
--- /dev/null
+++ b/src/main/java/com/dji/sample/map/model/dto/ElementLineStringDTO.java
@@ -0,0 +1,47 @@
+package com.dji.sample.map.model.dto;
+
+import com.dji.sample.map.model.enums.ElementTypeEnum;
+import lombok.Data;
+import org.springframework.util.CollectionUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/11/30
+ */
+@Data
+public class ElementLineStringDTO extends ElementType {
+
+    private Double[][] coordinates;
+
+    public ElementLineStringDTO() {
+        super(ElementTypeEnum.LINE_STRING.getDesc());
+    }
+
+    @Override
+    public List<ElementCoordinateDTO> convertToList() {
+        List<ElementCoordinateDTO> coordinateList = new ArrayList<>();
+        for (Double[] coordinate : this.coordinates) {
+            coordinateList.add(ElementCoordinateDTO.builder()
+                    .longitude(coordinate[0])
+                    .latitude(coordinate[1])
+                    .build());
+        }
+        return coordinateList;
+    }
+
+    @Override
+    public void adapterCoordinateType(List<ElementCoordinateDTO> coordinateList) {
+        if (CollectionUtils.isEmpty(coordinateList)) {
+            return;
+        }
+        this.coordinates = new Double[coordinateList.size()][2];
+        for (int i = 0; i < this.coordinates.length; i++) {
+            this.coordinates[i][0] = coordinateList.get(i).getLongitude();
+            this.coordinates[i][1] = coordinateList.get(i).getLatitude();
+        }
+    }
+}
diff --git a/src/main/java/com/dji/sample/map/model/dto/ElementPointDTO.java b/src/main/java/com/dji/sample/map/model/dto/ElementPointDTO.java
new file mode 100644
index 0000000..3eeee7d
--- /dev/null
+++ b/src/main/java/com/dji/sample/map/model/dto/ElementPointDTO.java
@@ -0,0 +1,46 @@
+package com.dji.sample.map.model.dto;
+
+import com.dji.sample.map.model.enums.ElementTypeEnum;
+import lombok.Data;
+import org.springframework.util.CollectionUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/11/30
+ */
+@Data
+public class ElementPointDTO extends ElementType {
+
+    private Double[] coordinates;
+
+    public ElementPointDTO() {
+        super(ElementTypeEnum.POINT.getDesc());
+    }
+
+    @Override
+    public List<ElementCoordinateDTO> convertToList() {
+        List<ElementCoordinateDTO> coordinateList = new ArrayList<>();
+        coordinateList.add(ElementCoordinateDTO.builder()
+                .longitude(this.coordinates[0])
+                .latitude(this.coordinates[1])
+                .altitude(this.coordinates.length == 3 ? this.coordinates[2] : null)
+                .build());
+        return coordinateList;
+    }
+
+    @Override
+    public void adapterCoordinateType(List<ElementCoordinateDTO> coordinateList) {
+        if (CollectionUtils.isEmpty(coordinateList)) {
+            return;
+        }
+        this.coordinates = new Double[]{
+                coordinateList.get(0).getLongitude(),
+                coordinateList.get(0).getLatitude(),
+                coordinateList.get(0).getAltitude()
+        };
+    }
+}
diff --git a/src/main/java/com/dji/sample/map/model/dto/ElementPolygonDTO.java b/src/main/java/com/dji/sample/map/model/dto/ElementPolygonDTO.java
new file mode 100644
index 0000000..35a8d35
--- /dev/null
+++ b/src/main/java/com/dji/sample/map/model/dto/ElementPolygonDTO.java
@@ -0,0 +1,48 @@
+package com.dji.sample.map.model.dto;
+
+import com.dji.sample.map.model.enums.ElementTypeEnum;
+import lombok.Data;
+import org.springframework.util.CollectionUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/11/30
+ */
+@Data
+public class ElementPolygonDTO extends ElementType {
+
+    private Double[][][] coordinates;
+
+    public ElementPolygonDTO() {
+        super(ElementTypeEnum.POLYGON.getDesc());
+    }
+
+    @Override
+    public List<ElementCoordinateDTO> convertToList() {
+        List<ElementCoordinateDTO> coordinateList = new ArrayList<>();
+
+        for (Double[] coordinate : this.coordinates[0]) {
+            coordinateList.add(ElementCoordinateDTO.builder()
+                    .longitude(coordinate[0])
+                    .latitude(coordinate[1])
+                    .build());
+        }
+        return coordinateList;
+    }
+
+    @Override
+    public void adapterCoordinateType(List<ElementCoordinateDTO> coordinateList) {
+        if (CollectionUtils.isEmpty(coordinateList)) {
+            return;
+        }
+        this.coordinates = new Double[1][coordinateList.size()][2];
+        for (int i = 0; i < this.coordinates[0].length; i++) {
+            this.coordinates[0][i][0] = coordinateList.get(i).getLongitude();
+            this.coordinates[0][i][1] = coordinateList.get(i).getLatitude();
+        }
+    }
+}
diff --git a/src/main/java/com/dji/sample/map/model/dto/ElementResourceDTO.java b/src/main/java/com/dji/sample/map/model/dto/ElementResourceDTO.java
new file mode 100644
index 0000000..b89df70
--- /dev/null
+++ b/src/main/java/com/dji/sample/map/model/dto/ElementResourceDTO.java
@@ -0,0 +1,26 @@
+package com.dji.sample.map.model.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/11/29
+ */
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+public class ElementResourceDTO {
+
+    private Integer type;
+
+    @JsonProperty(value = "user_name")
+    private String username;
+
+    private ResourceContentDTO content;
+}
diff --git a/src/main/java/com/dji/sample/map/model/dto/ElementType.java b/src/main/java/com/dji/sample/map/model/dto/ElementType.java
new file mode 100644
index 0000000..1fdf3f6
--- /dev/null
+++ b/src/main/java/com/dji/sample/map/model/dto/ElementType.java
@@ -0,0 +1,43 @@
+package com.dji.sample.map.model.dto;
+
+import com.fasterxml.jackson.annotation.JsonSubTypes;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/11/30
+ */
+@Data
+@NoArgsConstructor
+@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type",
+        include = JsonTypeInfo.As.EXISTING_PROPERTY, defaultImpl = ElementType.class)
+@JsonSubTypes({
+        @JsonSubTypes.Type(value = ElementPointDTO.class, name = "Point"),
+        @JsonSubTypes.Type(value = ElementLineStringDTO.class, name = "LineString"),
+        @JsonSubTypes.Type(value = ElementPolygonDTO.class, name = "Polygon")
+})
+public abstract class ElementType {
+
+    private String type;
+
+    ElementType(String type) {
+        this.type = type;
+    }
+
+    /**
+     * Convert coordinate data into objects and then add them to the collection.
+     * @return
+     */
+    public abstract List<ElementCoordinateDTO> convertToList();
+
+    /**
+     * Converts coordinates in a collection of objects to a specific type.
+     * @param coordinateList
+     */
+    public abstract void adapterCoordinateType(List<ElementCoordinateDTO> coordinateList);
+}
diff --git a/src/main/java/com/dji/sample/map/model/dto/ElementUpdateDTO.java b/src/main/java/com/dji/sample/map/model/dto/ElementUpdateDTO.java
new file mode 100644
index 0000000..58897e2
--- /dev/null
+++ b/src/main/java/com/dji/sample/map/model/dto/ElementUpdateDTO.java
@@ -0,0 +1,17 @@
+package com.dji.sample.map.model.dto;
+
+import lombok.Data;
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/12/1
+ */
+@Data
+public class ElementUpdateDTO {
+
+    private String name;
+
+    private ResourceContentDTO content;
+
+}
diff --git a/src/main/java/com/dji/sample/map/model/dto/GroupDTO.java b/src/main/java/com/dji/sample/map/model/dto/GroupDTO.java
new file mode 100644
index 0000000..1c75e67
--- /dev/null
+++ b/src/main/java/com/dji/sample/map/model/dto/GroupDTO.java
@@ -0,0 +1,38 @@
+package com.dji.sample.map.model.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/11/29
+ */
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder
+@Data
+public class GroupDTO {
+
+    private String id;
+
+    private String name;
+
+    private Integer type;
+    
+    private List<GroupElementDTO> elements;
+
+    @JsonProperty(value = "is_distributed")
+    private Boolean isDistributed;
+
+    @JsonProperty(value = "is_lock")
+    private Boolean isLock;
+
+    @JsonProperty(value = "create_time")
+    private Long createTime;
+}
diff --git a/src/main/java/com/dji/sample/map/model/dto/GroupElementDTO.java b/src/main/java/com/dji/sample/map/model/dto/GroupElementDTO.java
new file mode 100644
index 0000000..b48d1b6
--- /dev/null
+++ b/src/main/java/com/dji/sample/map/model/dto/GroupElementDTO.java
@@ -0,0 +1,38 @@
+package com.dji.sample.map.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;
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/11/29
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class GroupElementDTO {
+    private Integer display;
+
+    @JsonProperty("id")
+    private String elementId;
+
+    private String name;
+
+    @JsonProperty(value = "create_time")
+    private Long createTime;
+
+    @JsonProperty(value = "update_time")
+    private Long updateTime;
+
+    private ElementResourceDTO resource;
+
+    @JsonProperty("group_id")
+    private String groupId;
+}
diff --git a/src/main/java/com/dji/sample/map/model/dto/ResourceContentDTO.java b/src/main/java/com/dji/sample/map/model/dto/ResourceContentDTO.java
new file mode 100644
index 0000000..6ae3c9a
--- /dev/null
+++ b/src/main/java/com/dji/sample/map/model/dto/ResourceContentDTO.java
@@ -0,0 +1,25 @@
+package com.dji.sample.map.model.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/11/30
+ */
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+public class ResourceContentDTO {
+
+    @Builder.Default
+    private String type = "Feature";
+
+    private ContentPropertyDTO properties;
+
+    private ElementType geometry;
+}
diff --git a/src/main/java/com/dji/sample/map/model/dto/WebSocketElementDelDTO.java b/src/main/java/com/dji/sample/map/model/dto/WebSocketElementDelDTO.java
new file mode 100644
index 0000000..5d99272
--- /dev/null
+++ b/src/main/java/com/dji/sample/map/model/dto/WebSocketElementDelDTO.java
@@ -0,0 +1,25 @@
+package com.dji.sample.map.model.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/12/2
+ */
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class WebSocketElementDelDTO {
+
+    @JsonProperty("id")
+    private String elementId;
+
+    @JsonProperty("group_id")
+    private String groupId;
+}
diff --git a/src/main/java/com/dji/sample/map/model/entity/ElementCoordinateEntity.java b/src/main/java/com/dji/sample/map/model/entity/ElementCoordinateEntity.java
new file mode 100644
index 0000000..26d2732
--- /dev/null
+++ b/src/main/java/com/dji/sample/map/model/entity/ElementCoordinateEntity.java
@@ -0,0 +1,41 @@
+package com.dji.sample.map.model.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/11/29
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@TableName(value = "map_element_coordinate")
+public class ElementCoordinateEntity implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Integer id;
+
+    @TableField("element_id")
+    private String elementId;
+
+    @TableField("longitude")
+    private Double longitude;
+
+    @TableField("latitude")
+    private Double latitude;
+
+    @TableField("altitude")
+    private Double altitude;
+
+}
diff --git a/src/main/java/com/dji/sample/map/model/entity/GroupElementEntity.java b/src/main/java/com/dji/sample/map/model/entity/GroupElementEntity.java
new file mode 100644
index 0000000..7c6748a
--- /dev/null
+++ b/src/main/java/com/dji/sample/map/model/entity/GroupElementEntity.java
@@ -0,0 +1,55 @@
+package com.dji.sample.map.model.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/11/29
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@TableName(value = "map_group_element")
+public class GroupElementEntity implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Integer id;
+
+    @TableField("group_id")
+    private String groupId;
+
+    @TableField("element_id")
+    private String elementId;
+
+    @TableField("element_name")
+    private String elementName;
+
+    @TableField("display")
+    private Integer display;
+
+    @TableField("element_type")
+    private Integer elementType;
+
+    @TableField("username")
+    private String username;
+
+    @TableField("color")
+    private String color;
+
+    @TableField("clamp_to_ground")
+    private Boolean clampToGround;
+
+    @TableField(value = "create_time", fill = FieldFill.INSERT)
+    private Long createTime;
+
+    @TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
+    private Long updateTime;
+}
diff --git a/src/main/java/com/dji/sample/map/model/entity/GroupEntity.java b/src/main/java/com/dji/sample/map/model/entity/GroupEntity.java
new file mode 100644
index 0000000..c639a9b
--- /dev/null
+++ b/src/main/java/com/dji/sample/map/model/entity/GroupEntity.java
@@ -0,0 +1,49 @@
+package com.dji.sample.map.model.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/11/29
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@TableName(value = "map_group")
+public class GroupEntity implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Integer id;
+
+    @TableField("group_id")
+    private String groupId;
+
+    @TableField("group_name")
+    private String groupName;
+
+    @TableField("group_type")
+    private Integer groupType;
+
+    @TableField("workspace_id")
+    private String workspaceId;
+
+    @TableField("is_distributed")
+    private Boolean isDistributed;
+
+    @TableField("is_lock")
+    private Boolean isLock;
+
+    @TableField(value = "create_time", fill = FieldFill.INSERT)
+    private Long createTime;
+
+    @TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
+    private Long updateTime;
+}
diff --git a/src/main/java/com/dji/sample/map/model/enums/ElementTypeEnum.java b/src/main/java/com/dji/sample/map/model/enums/ElementTypeEnum.java
new file mode 100644
index 0000000..886ebe2
--- /dev/null
+++ b/src/main/java/com/dji/sample/map/model/enums/ElementTypeEnum.java
@@ -0,0 +1,69 @@
+package com.dji.sample.map.model.enums;
+
+import com.dji.sample.map.model.dto.ElementLineStringDTO;
+import com.dji.sample.map.model.dto.ElementPointDTO;
+import com.dji.sample.map.model.dto.ElementPolygonDTO;
+import com.dji.sample.map.model.dto.ElementType;
+
+import java.util.Optional;
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/11/30
+ */
+public enum ElementTypeEnum {
+
+    POINT(0, "Point"),
+
+    LINE_STRING(1, "LineString"),
+
+    POLYGON(2, "Polygon"),
+
+    UNKNOWN(-1, "Unknown");
+
+    private int val;
+
+    private String desc;
+
+    ElementTypeEnum(int val, String desc) {
+        this.val = val;
+        this.desc = desc;
+    }
+
+    public static Optional<ElementType> findType(int val) {
+        if (POINT.val == val) {
+            return Optional.of(new ElementPointDTO());
+        }
+
+        if (LINE_STRING.val == val) {
+            return Optional.of(new ElementLineStringDTO());
+        }
+
+        if (POLYGON.val == val) {
+            return Optional.of(new ElementPolygonDTO());
+        }
+
+        return Optional.empty();
+    }
+
+    public String getDesc() {
+        return desc;
+    }
+
+    public static int findVal(String desc) {
+        if (POINT.desc.equals(desc)) {
+            return POINT.val;
+        }
+
+        if (LINE_STRING.desc.equals(desc)) {
+            return LINE_STRING.val;
+        }
+
+        if (POLYGON.desc.equals(desc)) {
+            return POLYGON.val;
+        }
+
+        return UNKNOWN.val;
+    }
+}
diff --git a/src/main/java/com/dji/sample/map/service/IElementCoordinateService.java b/src/main/java/com/dji/sample/map/service/IElementCoordinateService.java
new file mode 100644
index 0000000..7668205
--- /dev/null
+++ b/src/main/java/com/dji/sample/map/service/IElementCoordinateService.java
@@ -0,0 +1,35 @@
+package com.dji.sample.map.service;
+
+import com.dji.sample.map.model.dto.ElementCoordinateDTO;
+
+import java.util.List;
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/11/29
+ */
+public interface IElementCoordinateService {
+
+    /**
+     * Query all the coordinates of the element according to its id.
+     * @param elementId
+     * @return
+     */
+    List<ElementCoordinateDTO> getCoordinateByElementId(String elementId);
+
+    /**
+     * Save all the coordinate data of this element.
+     * @param coordinate
+     * @param elementId
+     * @return
+     */
+    Boolean saveCoordinate(List<ElementCoordinateDTO> coordinate, String elementId);
+
+    /**
+     * Delete all the coordinates of the element according to its id.
+     * @param elementId
+     * @return
+     */
+    Boolean deleteCoordinateByElementId(String elementId);
+}
diff --git a/src/main/java/com/dji/sample/map/service/IGroupElementService.java b/src/main/java/com/dji/sample/map/service/IGroupElementService.java
new file mode 100644
index 0000000..d6513fa
--- /dev/null
+++ b/src/main/java/com/dji/sample/map/service/IGroupElementService.java
@@ -0,0 +1,54 @@
+package com.dji.sample.map.service;
+
+import com.dji.sample.map.model.dto.ElementCreateDTO;
+import com.dji.sample.map.model.dto.ElementUpdateDTO;
+import com.dji.sample.map.model.dto.GroupElementDTO;
+
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/11/29
+ */
+public interface IGroupElementService {
+
+    /**
+     * Query all the elements in this group based on the group id.
+     * @param groupId
+     * @return
+     */
+    List<GroupElementDTO> getElementsByGroupId(String groupId);
+
+    /**
+     * Save all the elements.
+     * @param groupId
+     * @param elementCreate
+     * @return
+     */
+    Boolean saveElement(String groupId, ElementCreateDTO elementCreate);
+
+    /**
+     * Query the element information based on the element id and update element.
+     * @param elementId
+     * @param elementUpdate
+     * @param username
+     * @return
+     */
+    Boolean updateElement(String elementId, ElementUpdateDTO elementUpdate, String username);
+
+    /**
+     * Delete the element based on the element id.
+     * @param elementId
+     * @return
+     */
+    Boolean deleteElement(String elementId);
+
+    /**
+     * Query an element based on the element id, including the coordinate information in the elements.
+     * @param elementId
+     * @return
+     */
+    Optional<GroupElementDTO> getElementByElementId(String elementId);
+}
diff --git a/src/main/java/com/dji/sample/map/service/IGroupService.java b/src/main/java/com/dji/sample/map/service/IGroupService.java
new file mode 100644
index 0000000..bead14b
--- /dev/null
+++ b/src/main/java/com/dji/sample/map/service/IGroupService.java
@@ -0,0 +1,24 @@
+package com.dji.sample.map.service;
+
+import com.dji.sample.map.model.dto.GroupDTO;
+
+import java.util.List;
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/11/29
+ */
+public interface IGroupService {
+
+    /**
+     * Query all groups in the workspace based on the workspace's id.
+     * If the group id does not exist, do not add this filter condition.
+     * @param workspaceId
+     * @param groupId
+     * @param isDistributed Used to define if the group needs to be distributed. Default is true.
+     * @return
+     */
+    List<GroupDTO> getAllGroupsByWorkspaceId(String workspaceId, String groupId, Boolean isDistributed);
+
+}
diff --git a/src/main/java/com/dji/sample/map/service/IWorkspaceElementService.java b/src/main/java/com/dji/sample/map/service/IWorkspaceElementService.java
new file mode 100644
index 0000000..4d24769
--- /dev/null
+++ b/src/main/java/com/dji/sample/map/service/IWorkspaceElementService.java
@@ -0,0 +1,71 @@
+package com.dji.sample.map.service;
+
+import com.dji.sample.common.model.ResponseResult;
+import com.dji.sample.map.model.dto.ElementCreateDTO;
+import com.dji.sample.map.model.dto.ElementUpdateDTO;
+import com.dji.sample.map.model.dto.GroupDTO;
+import com.dji.sample.map.model.dto.GroupElementDTO;
+
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/11/30
+ */
+public interface IWorkspaceElementService {
+
+    /**
+     * Query all groups in the workspace based on the workspace's id,
+     * including the information of the elements in the group, and the coordinate information in the elements.
+     * @param workspaceId
+     * @param groupId
+     * @param isDistributed
+     * @return
+     */
+    List<GroupDTO> getAllGroupsByWorkspaceId(String workspaceId, String groupId, Boolean isDistributed);
+
+    /**
+     * Save all the elements, including the information of the elements in the group,
+     * and the coordinate information in the elements.
+     * @param groupId
+     * @param elementCreate
+     * @return
+     */
+    ResponseResult saveElement(String groupId, ElementCreateDTO elementCreate);
+
+    /**
+     * Update the element information based on the element id,
+     * including the information of the elements in the group, and the coordinate information in the elements.
+     * @param elementId
+     * @param elementUpdate
+     * @param username
+     * @return
+     */
+    ResponseResult updateElement(String elementId, ElementUpdateDTO elementUpdate, String username);
+
+    /**
+     * Delete the element information based on the element id,
+     * including the information of the elements in the group, and the coordinate information in the elements.
+     * @param elementId
+     * @return
+     */
+    ResponseResult deleteElement(String elementId);
+
+    /**
+     * Query an element based on the element id,
+     * including the information of the elements in the group, and the coordinate information in the elements.
+     * @param elementId
+     * @return
+     */
+    Optional<GroupElementDTO> getElementByElementId(String elementId);
+
+    /**
+     * Delete all the elements information based on the group id,
+     * including the information of the elements in the group, and the coordinate information in the elements.
+     * @param groupId
+     * @return
+     */
+    ResponseResult deleteAllElementByGroupId(String groupId);
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/map/service/impl/ElementCoordinateServiceImpl.java b/src/main/java/com/dji/sample/map/service/impl/ElementCoordinateServiceImpl.java
new file mode 100644
index 0000000..bab9775
--- /dev/null
+++ b/src/main/java/com/dji/sample/map/service/impl/ElementCoordinateServiceImpl.java
@@ -0,0 +1,94 @@
+package com.dji.sample.map.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.dji.sample.map.dao.IElementCoordinateMapper;
+import com.dji.sample.map.model.dto.ElementCoordinateDTO;
+import com.dji.sample.map.model.entity.ElementCoordinateEntity;
+import com.dji.sample.map.service.IElementCoordinateService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/11/29
+ */
+@Service
+@Transactional
+public class ElementCoordinateServiceImpl implements IElementCoordinateService {
+
+    @Autowired
+    private IElementCoordinateMapper mapper;
+
+    @Override
+    public List<ElementCoordinateDTO> getCoordinateByElementId(String elementId) {
+        return mapper.selectList(
+                new LambdaQueryWrapper<ElementCoordinateEntity>()
+                        .eq(ElementCoordinateEntity::getElementId, elementId))
+                .stream()
+                .map(this::entityConvertToDto)
+                .collect(Collectors.toList());
+    }
+
+    @Override
+    public Boolean saveCoordinate(List<ElementCoordinateDTO> coordinateList, String elementId) {
+        for (ElementCoordinateDTO coordinate : coordinateList) {
+            ElementCoordinateEntity entity = this.dtoConvertToEntity(coordinate);
+            entity.setElementId(elementId);
+
+            int insert = mapper.insert(entity);
+            if (insert <= 0) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public Boolean deleteCoordinateByElementId(String elementId) {
+        return mapper.delete(new LambdaUpdateWrapper<ElementCoordinateEntity>()
+                .eq(ElementCoordinateEntity::getElementId, elementId)) > 0;
+    }
+
+    /**
+     * Convert database entity objects into coordinate data transfer object.
+     * @param entity
+     * @return
+     */
+    private ElementCoordinateDTO entityConvertToDto(ElementCoordinateEntity entity) {
+        ElementCoordinateDTO.ElementCoordinateDTOBuilder builder = ElementCoordinateDTO.builder();
+        if (entity == null) {
+            return builder.build();
+        }
+
+        return builder
+                .longitude(entity.getLongitude())
+                .latitude(entity.getLatitude())
+                .altitude(entity.getAltitude())
+                .build();
+    }
+
+    /**
+     * Convert the received coordinate object into a database entity object.
+     * @param coordinate
+     * @return
+     */
+    private ElementCoordinateEntity dtoConvertToEntity(ElementCoordinateDTO coordinate) {
+        ElementCoordinateEntity.ElementCoordinateEntityBuilder builder = ElementCoordinateEntity.builder();
+
+        if (coordinate == null) {
+            return builder.build();
+        }
+
+        return builder
+                .altitude(coordinate.getAltitude())
+                .latitude(coordinate.getLatitude())
+                .longitude(coordinate.getLongitude())
+                .build();
+    }
+}
diff --git a/src/main/java/com/dji/sample/map/service/impl/GroupElementServiceImpl.java b/src/main/java/com/dji/sample/map/service/impl/GroupElementServiceImpl.java
new file mode 100644
index 0000000..7ee4ce9
--- /dev/null
+++ b/src/main/java/com/dji/sample/map/service/impl/GroupElementServiceImpl.java
@@ -0,0 +1,199 @@
+package com.dji.sample.map.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.dji.sample.map.dao.IGroupElementMapper;
+import com.dji.sample.map.model.dto.*;
+import com.dji.sample.map.model.entity.GroupElementEntity;
+import com.dji.sample.map.model.enums.ElementTypeEnum;
+import com.dji.sample.map.service.IElementCoordinateService;
+import com.dji.sample.map.service.IGroupElementService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/11/29
+ */
+@Service
+@Transactional
+public class GroupElementServiceImpl implements IGroupElementService {
+
+    @Autowired
+    private IGroupElementMapper mapper;
+
+    @Autowired
+    private IElementCoordinateService elementCoordinateService;
+
+    @Override
+    public List<GroupElementDTO> getElementsByGroupId(String groupId) {
+        List<GroupElementEntity> elementList = mapper.selectList(
+                new LambdaQueryWrapper<GroupElementEntity>()
+                        .eq(GroupElementEntity::getGroupId, groupId));
+
+        List<GroupElementDTO> groupElementList = new ArrayList<>();
+        for (GroupElementEntity elementEntity : elementList) {
+
+            GroupElementDTO groupElement = this.entityConvertToDto(elementEntity);
+            groupElementList.add(groupElement);
+
+            this.addCoordinateToElement(groupElement, elementEntity);
+        }
+        return groupElementList;
+    }
+
+    @Override
+    public Boolean saveElement(String groupId, ElementCreateDTO elementCreate) {
+        Optional<GroupElementEntity> groupElementOpt = this.getEntityByElementId(elementCreate.getId());
+
+        if (groupElementOpt.isPresent()) {
+            return false;
+        }
+        GroupElementEntity groupElement = this.createDtoConvertToEntity(elementCreate);
+        groupElement.setGroupId(groupId);
+
+        return mapper.insert(groupElement) > 0;
+    }
+
+    @Override
+    public Boolean updateElement(String elementId, ElementUpdateDTO elementUpdate, String username) {
+        Optional<GroupElementEntity> groupElementOpt = this.getEntityByElementId(elementId);
+        if (groupElementOpt.isEmpty()) {
+            return false;
+        }
+
+        GroupElementEntity groupElement = groupElementOpt.get();
+        groupElement.setUsername(username);
+        this.updateEntityWithDto(elementUpdate, groupElement);
+
+        return mapper.updateById(groupElement) > 0;
+    }
+
+    @Override
+    public Boolean deleteElement(String elementId) {
+        Optional<GroupElementEntity> groupElementOpt = this.getEntityByElementId(elementId);
+        if (groupElementOpt.isEmpty()) {
+            return true;
+        }
+
+        GroupElementEntity groupElement = groupElementOpt.get();
+        return mapper.deleteById(groupElement.getId()) > 0;
+    }
+
+
+    @Override
+    public Optional<GroupElementDTO> getElementByElementId(String elementId) {
+        Optional<GroupElementEntity> elementEntityOpt = this.getEntityByElementId(elementId);
+        if (elementEntityOpt.isEmpty()) {
+            return Optional.empty();
+        }
+        GroupElementEntity elementEntity = elementEntityOpt.get();
+        GroupElementDTO groupElement = this.entityConvertToDto(elementEntity);
+
+        this.addCoordinateToElement(groupElement, elementEntity);
+
+        return Optional.ofNullable(groupElement);
+    }
+
+    /**
+     * Adds the received coordinate data to the element object.
+     * @param element
+     * @param elementEntity
+     */
+    private void addCoordinateToElement(GroupElementDTO element, GroupElementEntity elementEntity) {
+        Optional<ElementType> coordinateOpt = ElementTypeEnum.findType(elementEntity.getElementType());
+        if (coordinateOpt.isEmpty()) {
+            return;
+        }
+
+        ElementType elementType = coordinateOpt.get();
+
+        element.getResource().setContent(
+                ResourceContentDTO.builder()
+                        .properties(ContentPropertyDTO.builder()
+                                .clampToGround(elementEntity.getClampToGround())
+                                .color(elementEntity.getColor())
+                                .build())
+                        .geometry(elementType)
+                        .build());
+
+        elementType.adapterCoordinateType(
+                elementCoordinateService.getCoordinateByElementId(elementEntity.getElementId()));
+    }
+
+    /**
+     * Query an element based on the element id。
+     * @param elementId
+     * @return
+     */
+    private Optional<GroupElementEntity> getEntityByElementId(String elementId) {
+        return Optional.ofNullable(mapper.selectOne(
+                new LambdaQueryWrapper<GroupElementEntity>()
+                        .eq(GroupElementEntity::getElementId, elementId)));
+    }
+
+    /**
+     * Convert database entity objects into element data transfer object.
+     * @param entity
+     * @return
+     */
+    private GroupElementDTO entityConvertToDto(GroupElementEntity entity) {
+        GroupElementDTO.GroupElementDTOBuilder builder = GroupElementDTO.builder();
+        if (entity == null) {
+            return builder.build();
+        }
+
+        return builder
+                .display(entity.getDisplay())
+                .groupId(entity.getGroupId())
+                .elementId(entity.getElementId())
+                .name(entity.getElementName())
+                .createTime(entity.getCreateTime())
+                .updateTime(entity.getUpdateTime())
+                .resource(ElementResourceDTO.builder()
+                        .type(entity.getElementType())
+                        .username(entity.getUsername())
+                        .build())
+                .build();
+    }
+
+    /**
+     * Convert the received element object into a database entity object.
+     * @param elementCreate
+     * @return
+     */
+    private GroupElementEntity createDtoConvertToEntity(ElementCreateDTO elementCreate) {
+        ContentPropertyDTO properties = elementCreate.getResource().getContent().getProperties();
+        return GroupElementEntity.builder()
+                .elementId(elementCreate.getId())
+                .elementName(elementCreate.getName())
+                .username(elementCreate.getResource().getUsername())
+                .elementType(ElementTypeEnum.findVal(elementCreate.getResource().getContent().getGeometry().getType()))
+                .clampToGround(properties.getClampToGround() != null && properties.getClampToGround())
+                .color(properties.getColor())
+                .build();
+    }
+
+    /**
+     * Add the content that needs to be updated to the entity object to be updated.
+     * @param elementUpdate
+     * @param groupElement
+     */
+    private void updateEntityWithDto(ElementUpdateDTO elementUpdate, GroupElementEntity groupElement) {
+        if (elementUpdate == null || groupElement == null) {
+            return;
+        }
+
+        groupElement.setElementName(elementUpdate.getName());
+        groupElement.setElementType(ElementTypeEnum.findVal(elementUpdate.getContent().getGeometry().getType()));
+        groupElement.setColor(elementUpdate.getContent().getProperties().getColor());
+
+        boolean clampToGround = elementUpdate.getContent().getProperties().getClampToGround();
+        groupElement.setClampToGround(clampToGround);
+    }
+}
diff --git a/src/main/java/com/dji/sample/map/service/impl/GroupServiceImpl.java b/src/main/java/com/dji/sample/map/service/impl/GroupServiceImpl.java
new file mode 100644
index 0000000..07ac7f1
--- /dev/null
+++ b/src/main/java/com/dji/sample/map/service/impl/GroupServiceImpl.java
@@ -0,0 +1,66 @@
+package com.dji.sample.map.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.dji.sample.map.dao.IGroupMapper;
+import com.dji.sample.map.model.dto.GroupDTO;
+import com.dji.sample.map.model.entity.GroupEntity;
+import com.dji.sample.map.service.IGroupElementService;
+import com.dji.sample.map.service.IGroupService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/11/29
+ */
+@Service
+@Transactional
+public class GroupServiceImpl implements IGroupService {
+
+    @Autowired
+    private IGroupMapper mapper;
+
+    @Autowired
+    private IGroupElementService groupElementService;
+
+    @Override
+    public List<GroupDTO> getAllGroupsByWorkspaceId(String workspaceId, String groupId, Boolean isDistributed) {
+
+        return mapper.selectList(
+                new LambdaQueryWrapper<GroupEntity>()
+                        .eq(GroupEntity::getWorkspaceId, workspaceId)
+                        .eq(StringUtils.hasText(groupId), GroupEntity::getGroupId, groupId)
+                        .eq(isDistributed != null, GroupEntity::getIsDistributed, isDistributed))
+                .stream()
+                .map(this::entityConvertToDto)
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * Convert database entity objects into group data transfer object.
+     * @param entity
+     * @return
+     */
+    private GroupDTO entityConvertToDto(GroupEntity entity) {
+        GroupDTO.GroupDTOBuilder builder = GroupDTO.builder();
+
+        if (entity == null) {
+            return builder.build();
+        }
+
+        return builder
+                .id(entity.getGroupId())
+                .name(entity.getGroupName())
+                .type(entity.getGroupType())
+                .isLock(entity.getIsLock())
+                .isDistributed(entity.getIsDistributed())
+                .createTime(entity.getCreateTime())
+                .build();
+    }
+}
diff --git a/src/main/java/com/dji/sample/map/service/impl/WorkspaceElementServiceImpl.java b/src/main/java/com/dji/sample/map/service/impl/WorkspaceElementServiceImpl.java
new file mode 100644
index 0000000..49b5805
--- /dev/null
+++ b/src/main/java/com/dji/sample/map/service/impl/WorkspaceElementServiceImpl.java
@@ -0,0 +1,109 @@
+package com.dji.sample.map.service.impl;
+
+import com.dji.sample.common.model.ResponseResult;
+import com.dji.sample.map.model.dto.ElementCreateDTO;
+import com.dji.sample.map.model.dto.ElementUpdateDTO;
+import com.dji.sample.map.model.dto.GroupDTO;
+import com.dji.sample.map.model.dto.GroupElementDTO;
+import com.dji.sample.map.service.IElementCoordinateService;
+import com.dji.sample.map.service.IGroupElementService;
+import com.dji.sample.map.service.IGroupService;
+import com.dji.sample.map.service.IWorkspaceElementService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/11/30
+ */
+@Transactional
+@Service
+public class WorkspaceElementServiceImpl implements IWorkspaceElementService {
+
+    @Autowired
+    private IGroupService groupService;
+
+    @Autowired
+    private IGroupElementService groupElementService;
+
+    @Autowired
+    private IElementCoordinateService elementCoordinateService;
+
+    @Override
+    public List<GroupDTO> getAllGroupsByWorkspaceId(String workspaceId, String groupId, Boolean isDistributed) {
+        List<GroupDTO> groupList = groupService.getAllGroupsByWorkspaceId(workspaceId, groupId, isDistributed);
+        groupList.forEach(group -> group.setElements(
+                groupElementService.getElementsByGroupId(group.getId())
+        ));
+        return groupList;
+    }
+
+    @Override
+    public ResponseResult saveElement(String groupId, ElementCreateDTO elementCreate) {
+        boolean saveElement = groupElementService.saveElement(groupId, elementCreate);
+        if (!saveElement) {
+            return ResponseResult.error("Failed to save the element.");
+        }
+
+        // save coordinate
+        boolean saveCoordinate = elementCoordinateService.saveCoordinate(
+                        elementCreate.getResource().getContent().getGeometry().convertToList(), elementCreate.getId());
+
+        return saveCoordinate ?
+                ResponseResult.success() : ResponseResult.error("Failed to save the coordinate.");
+    }
+
+
+    @Override
+    public ResponseResult updateElement(String elementId, ElementUpdateDTO elementUpdate, String username) {
+        boolean updElement = groupElementService.updateElement(elementId, elementUpdate, username);
+        if (!updElement) {
+            return ResponseResult.error("Failed to update the element.");
+        }
+
+        // delete all coordinates according to element id.
+        boolean delCoordinate = elementCoordinateService.deleteCoordinateByElementId(elementId);
+        // save coordinate
+        boolean saveCoordinate = elementCoordinateService.saveCoordinate(
+                elementUpdate.getContent().getGeometry().convertToList(), elementId);
+
+        return delCoordinate && saveCoordinate ?
+                ResponseResult.success() : ResponseResult.error("Failed to update the coordinate.");
+    }
+
+    @Override
+    public ResponseResult deleteElement(String elementId) {
+        boolean delElement = groupElementService.deleteElement(elementId);
+        if (!delElement) {
+            return ResponseResult.error("Failed to delete the element.");
+        }
+
+        // delete all coordinates according to element id.
+        boolean delCoordinate = elementCoordinateService.deleteCoordinateByElementId(elementId);
+
+        return delCoordinate ?
+                ResponseResult.success() : ResponseResult.error("Failed to delete the coordinate.");
+    }
+
+    @Override
+    public Optional<GroupElementDTO> getElementByElementId(String elementId) {
+        return groupElementService.getElementByElementId(elementId);
+    }
+
+    @Override
+    public ResponseResult deleteAllElementByGroupId(String groupId) {
+        List<GroupElementDTO> groupElementList = groupElementService.getElementsByGroupId(groupId);
+        for (GroupElementDTO groupElement : groupElementList) {
+            ResponseResult response = this.deleteElement(groupElement.getElementId());
+            if (ResponseResult.CODE_SUCCESS != response.getCode()) {
+                return response;
+            }
+        }
+        return ResponseResult.success();
+    }
+}
diff --git a/src/main/java/com/dji/sample/media/controller/FileController.java b/src/main/java/com/dji/sample/media/controller/FileController.java
new file mode 100644
index 0000000..25a6015
--- /dev/null
+++ b/src/main/java/com/dji/sample/media/controller/FileController.java
@@ -0,0 +1,36 @@
+package com.dji.sample.media.controller;
+
+import com.dji.sample.common.model.ResponseResult;
+import com.dji.sample.media.model.MediaFileDTO;
+import com.dji.sample.media.service.IFileService;
+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;
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/12/9
+ */
+@RestController
+@RequestMapping("${url.media.prefix}${url.media.version}/files")
+public class FileController {
+
+    @Autowired
+    private IFileService fileService;
+
+    /**
+     * Get information about all the media files in this workspace based on the workspace id.
+     * @param workspaceId
+     * @return
+     */
+    @GetMapping("/{workspace_id}/files")
+    public ResponseResult<List<MediaFileDTO>> getFilesList(@PathVariable(name = "workspace_id") String workspaceId) {
+        List<MediaFileDTO> filesList = fileService.getAllFilesByWorkspaceId(workspaceId);
+        return ResponseResult.success(filesList);
+    }
+}
diff --git a/src/main/java/com/dji/sample/media/controller/MediaController.java b/src/main/java/com/dji/sample/media/controller/MediaController.java
new file mode 100644
index 0000000..0e73743
--- /dev/null
+++ b/src/main/java/com/dji/sample/media/controller/MediaController.java
@@ -0,0 +1,73 @@
+package com.dji.sample.media.controller;
+
+import com.dji.sample.common.model.ResponseResult;
+import com.dji.sample.media.model.FileUploadDTO;
+import com.dji.sample.media.service.IMediaService;
+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 java.util.stream.Collectors;
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/12/7
+ */
+@Slf4j
+@RestController
+@RequestMapping("${url.media.prefix}${url.media.version}/workspaces")
+public class MediaController {
+
+    @Autowired
+    private IMediaService mediaService;
+
+    /**
+     * Check if the file has been uploaded by the fingerprint.
+     * @param workspaceId
+     * @param file
+     * @return
+     */
+    @PostMapping("/{workspace_id}/fast-upload")
+    public ResponseResult fastUpload(@PathVariable(name = "workspace_id") String workspaceId, @RequestBody FileUploadDTO file) {
+
+        boolean isExist = mediaService.fastUpload(workspaceId, file.getFingerprint());
+
+        return isExist ? ResponseResult.success() : ResponseResult.error(file.getFingerprint() + "already exists.");
+    }
+
+    /**
+     * When the file is uploaded to the storage server by pilot,
+     * the basic information of the file is reported through this interface.
+     * @param workspaceId
+     * @param file
+     * @return
+     */
+    @PostMapping("/{workspace_id}/upload-callback")
+    public ResponseResult<String> uploadCallback(@PathVariable(name = "workspace_id") String workspaceId, @RequestBody FileUploadDTO file) {
+        mediaService.saveMediaFile(workspaceId, file);
+        return ResponseResult.success(file.getObjectKey());
+
+    }
+
+    /**
+     * Query the files that already exist in this workspace based on the workspace id and the collection of tiny fingerprints.
+     * @param workspaceId
+     * @param tinyFingerprints
+     * @return
+     */
+    @GetMapping("/{workspace_id}/files/tiny-fingerprints")
+    public ResponseResult<Map<String, List<String>>> uploadCallback(@PathVariable(name = "workspace_id") String workspaceId,
+                               @RequestParam(value = "tiny_fingerprint") List<String> tinyFingerprints) {
+        List<String> tinyFingerprintList = mediaService.getAllTinyFingerprintsByWorkspaceId(workspaceId);
+        List<String> existingList = tinyFingerprints
+                .stream()
+                .filter(tinyFingerprintList::contains)
+                .collect(Collectors.toList());
+        return ResponseResult.success(new ConcurrentHashMap<>(Map.of("tiny_fingerprints", existingList)));
+    }
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/dji/sample/media/dao/IFileMapper.java b/src/main/java/com/dji/sample/media/dao/IFileMapper.java
new file mode 100644
index 0000000..ec01af1
--- /dev/null
+++ b/src/main/java/com/dji/sample/media/dao/IFileMapper.java
@@ -0,0 +1,12 @@
+package com.dji.sample.media.dao;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.dji.sample.media.model.MediaFileEntity;
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/12/9
+ */
+public interface IFileMapper extends BaseMapper<MediaFileEntity> {
+}
diff --git a/src/main/java/com/dji/sample/media/model/CredentialsDTO.java b/src/main/java/com/dji/sample/media/model/CredentialsDTO.java
new file mode 100644
index 0000000..2662254
--- /dev/null
+++ b/src/main/java/com/dji/sample/media/model/CredentialsDTO.java
@@ -0,0 +1,40 @@
+package com.dji.sample.media.model;
+
+import com.aliyuncs.sts.model.v20150401.AssumeRoleResponse;
+import io.minio.credentials.Credentials;
+import lombok.Data;
+
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/12/7
+ */
+@Data
+public class CredentialsDTO {
+
+    private String accessKeyId;
+
+    private String accessKeySecret;
+
+    private Integer expire;
+
+    private String securityToken;
+
+    public CredentialsDTO(Credentials credentials, int expire) {
+        this.accessKeyId = credentials.accessKey();
+        this.accessKeySecret = credentials.secretKey();
+        this.securityToken = credentials.sessionToken();
+        this.expire = expire;
+    }
+
+    public CredentialsDTO(AssumeRoleResponse.Credentials credentials, long expire) {
+        this.accessKeyId = credentials.getAccessKeyId();
+        this.accessKeySecret = credentials.getAccessKeySecret();
+        this.securityToken = credentials.getSecurityToken();
+        this.expire = Math.toIntExact(expire);
+    }
+
+    public CredentialsDTO() {
+    }
+}
diff --git a/src/main/java/com/dji/sample/media/model/FileExtensionDTO.java b/src/main/java/com/dji/sample/media/model/FileExtensionDTO.java
new file mode 100644
index 0000000..5a33bc6
--- /dev/null
+++ b/src/main/java/com/dji/sample/media/model/FileExtensionDTO.java
@@ -0,0 +1,26 @@
+package com.dji.sample.media.model;
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import lombok.Data;
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/12/7
+ */
+@Data
+@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
+public class FileExtensionDTO {
+
+    private String droneModelKey;
+
+    private Boolean isOriginal;
+
+    private String payloadModelKey;
+
+    private String tinnyFingerprint;
+
+    private String sn;
+
+}
diff --git a/src/main/java/com/dji/sample/media/model/FileMetadataDTO.java b/src/main/java/com/dji/sample/media/model/FileMetadataDTO.java
new file mode 100644
index 0000000..bab4b5a
--- /dev/null
+++ b/src/main/java/com/dji/sample/media/model/FileMetadataDTO.java
@@ -0,0 +1,31 @@
+package com.dji.sample.media.model;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/12/7
+ */
+@Data
+@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
+public class FileMetadataDTO {
+
+    private Double absoluteAltitude;
+
+    @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ssX")
+    private Date createdTime;
+
+    private Double gimbalYawDegree;
+
+    private PositionDTO photoedPosition;
+
+    private PositionDTO shootPosition;
+
+    private Double relativeAltitude;
+}
diff --git a/src/main/java/com/dji/sample/media/model/FileUploadDTO.java b/src/main/java/com/dji/sample/media/model/FileUploadDTO.java
new file mode 100644
index 0000000..acfa910
--- /dev/null
+++ b/src/main/java/com/dji/sample/media/model/FileUploadDTO.java
@@ -0,0 +1,26 @@
+package com.dji.sample.media.model;
+
+import lombok.Data;
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/12/7
+ */
+@Data
+public class FileUploadDTO {
+
+    private FileExtensionDTO ext;
+
+    private String fingerprint;
+
+    private String name;
+
+    private String path;
+
+    private FileMetadataDTO metadata;
+
+    private String objectKey;
+
+    private Integer subFileType;
+}
diff --git a/src/main/java/com/dji/sample/media/model/MediaFileDTO.java b/src/main/java/com/dji/sample/media/model/MediaFileDTO.java
new file mode 100644
index 0000000..b42acf8
--- /dev/null
+++ b/src/main/java/com/dji/sample/media/model/MediaFileDTO.java
@@ -0,0 +1,34 @@
+package com.dji.sample.media.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/12/9
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class MediaFileDTO {
+
+    private String fileName;
+
+    private String filePath;
+
+    private String objectKey;
+
+    private String subFileType;
+
+    private Boolean isOriginal;
+
+    private String drone;
+
+    private String payload;
+
+    private String tinnyFingerprint;
+}
diff --git a/src/main/java/com/dji/sample/media/model/MediaFileEntity.java b/src/main/java/com/dji/sample/media/model/MediaFileEntity.java
new file mode 100644
index 0000000..a3dd2aa
--- /dev/null
+++ b/src/main/java/com/dji/sample/media/model/MediaFileEntity.java
@@ -0,0 +1,62 @@
+package com.dji.sample.media.model;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/12/9
+ */
+@TableName(value = "media_file")
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class MediaFileEntity implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Integer id;
+
+    @TableField("file_name")
+    private String fileName;
+
+    @TableField("file_path")
+    private String filePath;
+
+    @TableField("workspace_id")
+    private String workspaceId;
+
+    @TableField("fingerprint")
+    private String fingerprint;
+
+    @TableField("tinny_fingerprint")
+    private String tinnyFingerprint;
+
+    @TableField("object_key")
+    private String objectKey;
+
+    @TableField("sub_file_type")
+    private Integer subFileType;
+
+    @TableField("is_original")
+    private Boolean isOriginal;
+
+    @TableField("drone")
+    private String drone;
+
+    @TableField("payload")
+    private String payload;
+
+    @TableField(value = "create_time", fill = FieldFill.INSERT)
+    private Long createTime;
+
+    @TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
+    private Long updateTime;
+}
+
diff --git a/src/main/java/com/dji/sample/media/model/PositionDTO.java b/src/main/java/com/dji/sample/media/model/PositionDTO.java
new file mode 100644
index 0000000..1b5cda4
--- /dev/null
+++ b/src/main/java/com/dji/sample/media/model/PositionDTO.java
@@ -0,0 +1,16 @@
+package com.dji.sample.media.model;
+
+import lombok.Data;
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/12/7
+ */
+@Data
+public class PositionDTO {
+
+    private Double lat;
+
+    private Double lng;
+}
diff --git a/src/main/java/com/dji/sample/media/model/StsCredentialsDTO.java b/src/main/java/com/dji/sample/media/model/StsCredentialsDTO.java
new file mode 100644
index 0000000..a4da2ab
--- /dev/null
+++ b/src/main/java/com/dji/sample/media/model/StsCredentialsDTO.java
@@ -0,0 +1,33 @@
+package com.dji.sample.media.model;
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/12/7
+ */
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
+public class StsCredentialsDTO {
+
+    private String bucket;
+
+    private CredentialsDTO credentials;
+
+    private String endpoint;
+
+    private String objectKeyPrefix;
+
+    private String provider;
+
+    private String region;
+}
diff --git a/src/main/java/com/dji/sample/media/service/IFileService.java b/src/main/java/com/dji/sample/media/service/IFileService.java
new file mode 100644
index 0000000..9347ccf
--- /dev/null
+++ b/src/main/java/com/dji/sample/media/service/IFileService.java
@@ -0,0 +1,37 @@
+package com.dji.sample.media.service;
+
+import com.dji.sample.media.model.FileUploadDTO;
+import com.dji.sample.media.model.MediaFileDTO;
+
+import java.util.List;
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/12/9
+ */
+public interface IFileService {
+
+    /**
+     * Query if the file already exists based on the workspace id and the fingerprint of the file.
+     * @param workspaceId
+     * @param fingerprint
+     * @return
+     */
+    Boolean checkExist(String workspaceId, String fingerprint);
+
+    /**
+     * Save the basic information of the file to the database.
+     * @param workspaceId
+     * @param file
+     * @return
+     */
+    Integer saveFile(String workspaceId, FileUploadDTO file);
+
+    /**
+     * Query information about all files in this workspace based on the workspace id.
+     * @param workspaceId
+     * @return
+     */
+    List<MediaFileDTO> getAllFilesByWorkspaceId(String workspaceId);
+}
diff --git a/src/main/java/com/dji/sample/media/service/IMediaService.java b/src/main/java/com/dji/sample/media/service/IMediaService.java
new file mode 100644
index 0000000..f8fc42d
--- /dev/null
+++ b/src/main/java/com/dji/sample/media/service/IMediaService.java
@@ -0,0 +1,36 @@
+package com.dji.sample.media.service;
+
+import com.dji.sample.media.model.FileUploadDTO;
+
+import java.util.List;
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/12/9
+ */
+public interface IMediaService {
+
+    /**
+     * Check if the file has been uploaded by the fingerprint.
+     * @param workspaceId
+     * @param fingerprint
+     * @return
+     */
+    Boolean fastUpload(String workspaceId, String fingerprint);
+
+    /**
+     * Save the basic information of the file to the database.
+     * @param workspaceId
+     * @param file
+     * @return
+     */
+    Integer saveMediaFile(String workspaceId, FileUploadDTO file);
+
+    /**
+     * Query tiny fingerprints about all files in this workspace based on the workspace id.
+     * @param workspaceId
+     * @return
+     */
+    List<String> getAllTinyFingerprintsByWorkspaceId(String workspaceId);
+}
diff --git a/src/main/java/com/dji/sample/media/service/impl/FileServiceImpl.java b/src/main/java/com/dji/sample/media/service/impl/FileServiceImpl.java
new file mode 100644
index 0000000..744d9d7
--- /dev/null
+++ b/src/main/java/com/dji/sample/media/service/impl/FileServiceImpl.java
@@ -0,0 +1,112 @@
+package com.dji.sample.media.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.dji.sample.manage.model.dto.DeviceDictionaryDTO;
+import com.dji.sample.manage.service.IDeviceDictionaryService;
+import com.dji.sample.media.dao.IFileMapper;
+import com.dji.sample.media.model.FileUploadDTO;
+import com.dji.sample.media.model.MediaFileDTO;
+import com.dji.sample.media.model.MediaFileEntity;
+import com.dji.sample.media.service.IFileService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/12/9
+ */
+@Service
+@Transactional
+public class FileServiceImpl implements IFileService {
+
+
+    @Autowired
+    private IFileMapper mapper;
+
+    @Autowired
+    private IDeviceDictionaryService deviceDictionaryService;
+
+    @Override
+    public Boolean checkExist(String workspaceId, String fingerprint) {
+        MediaFileEntity fileEntity = mapper.selectOne(new LambdaQueryWrapper<MediaFileEntity>()
+                .eq(MediaFileEntity::getWorkspaceId, workspaceId)
+                .eq(MediaFileEntity::getFingerprint, fingerprint));
+        return fileEntity != null;
+    }
+
+    @Override
+    public Integer saveFile(String workspaceId, FileUploadDTO file) {
+        MediaFileEntity fileEntity = this.fileUploadConvertToEntity(file);
+        fileEntity.setWorkspaceId(workspaceId);
+        return mapper.insert(fileEntity);
+    }
+
+    @Override
+    public List<MediaFileDTO> getAllFilesByWorkspaceId(String workspaceId) {
+        return mapper.selectList(new LambdaQueryWrapper<MediaFileEntity>()
+                .eq(MediaFileEntity::getWorkspaceId, workspaceId))
+                .stream()
+                .map(this::entityConvertToDto)
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * Convert the received file object into a database entity object.
+     * @param file
+     * @return
+     */
+    private MediaFileEntity fileUploadConvertToEntity(FileUploadDTO file) {
+        MediaFileEntity.MediaFileEntityBuilder builder = MediaFileEntity.builder();
+
+        if (file != null) {
+            builder.fileName(file.getName())
+                    .filePath(file.getPath())
+                    .fingerprint(file.getFingerprint())
+                    .objectKey(file.getObjectKey())
+                    .subFileType(file.getSubFileType())
+                    .isOriginal(file.getExt().getIsOriginal())
+                    .drone(file.getExt().getSn())
+                    .tinnyFingerprint(file.getExt().getTinnyFingerprint());
+
+            // domain-type-subType
+            int[] payloadModel = Arrays.stream(file.getExt().getPayloadModelKey().split("-"))
+                    .map(Integer::valueOf)
+                    .mapToInt(Integer::intValue)
+                    .toArray();
+            Optional<DeviceDictionaryDTO> payloadDict = deviceDictionaryService
+                    .getOneDictionaryInfoByDomainTypeSubType(payloadModel[0], payloadModel[1], payloadModel[2]);
+            payloadDict.ifPresent(payload -> builder.payload(payload.getDeviceName()));
+        }
+        return builder.build();
+    }
+
+    /**
+     * Convert database entity objects into file data transfer object.
+     * @param entity
+     * @return
+     */
+    private MediaFileDTO entityConvertToDto(MediaFileEntity entity) {
+        MediaFileDTO.MediaFileDTOBuilder builder = MediaFileDTO.builder();
+
+        if (entity != null) {
+            builder.fileName(entity.getFileName())
+                    .filePath(entity.getFilePath())
+                    .isOriginal(entity.getIsOriginal())
+                    .objectKey(entity.getObjectKey())
+                    .tinnyFingerprint(entity.getTinnyFingerprint())
+                    .payload(entity.getPayload())
+                    .drone(entity.getDrone());
+
+        }
+
+        return builder.build();
+    }
+
+}
diff --git a/src/main/java/com/dji/sample/media/service/impl/MediaServiceImpl.java b/src/main/java/com/dji/sample/media/service/impl/MediaServiceImpl.java
new file mode 100644
index 0000000..f7758d8
--- /dev/null
+++ b/src/main/java/com/dji/sample/media/service/impl/MediaServiceImpl.java
@@ -0,0 +1,41 @@
+package com.dji.sample.media.service.impl;
+
+import com.dji.sample.media.model.FileUploadDTO;
+import com.dji.sample.media.model.MediaFileDTO;
+import com.dji.sample.media.service.IFileService;
+import com.dji.sample.media.service.IMediaService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * @author sean
+ * @version 0.2
+ * @date 2021/12/9
+ */
+@Service
+public class MediaServiceImpl implements IMediaService {
+
+    @Autowired
+    private IFileService fileService;
+
+    @Override
+    public Boolean fastUpload(String workspaceId, String fingerprint) {
+        return fileService.checkExist(workspaceId, fingerprint);
+    }
+
+    @Override
+    public Integer saveMediaFile(String workspaceId, FileUploadDTO file) {
+        return fileService.saveFile(workspaceId, file);
+    }
+
+    @Override
+    public List<String> getAllTinyFingerprintsByWorkspaceId(String workspaceId) {
+        return fileService.getAllFilesByWorkspaceId(workspaceId)
+                .stream()
+                .map(MediaFileDTO::getTinnyFingerprint)
+                .collect(Collectors.toList());
+    }
+}
diff --git a/src/main/java/com/dji/sample/storage/controller/StorageController.java b/src/main/java/com/dji/sample/storage/controller/StorageController.java
new file mode 100644
index 0000000..801146c
--- /dev/null
+++ b/src/main/java/com/dji/sample/storage/controller/StorageController.java
@@ -0,0 +1,51 @@
+package com.dji.sample.storage.controller;
+
+import com.dji.sample.common.model.ResponseResult;
+import com.dji.sample.component.oss.model.AliyunOSSConfiguration;
+import com.dji.sample.component.oss.model.MinIOConfiguration;
+import com.dji.sample.media.model.StsCredentialsDTO;
+import com.dji.sample.storage.service.IStorageService;
+import com.dji.sample.storage.service.impl.AliyunStorageServiceImpl;
+import com.dji.sample.storage.service.impl.MinIOStorageServiceImpl;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * @author sean
+ * @version 0.3
+ * @date 2021/12/29
+ */
+@RestController
+@RequestMapping("${url.storage.prefix}${url.storage.version}/workspaces/")
+public class StorageController {
+
+    private IStorageService storageService;
+
+    @Autowired
+    private void setOssService(@Autowired AliyunStorageServiceImpl aliyunStorageService,
+                              @Autowired MinIOStorageServiceImpl minIOStorageService) {
+        if (AliyunOSSConfiguration.enable) {
+            this.storageService = aliyunStorageService;
+            return;
+        }
+        if (MinIOConfiguration.enable) {
+            this.storageService = minIOStorageService;
+            return;
+        }
+        throw new NullPointerException("storageService is null.");
+    }
+    /**
+     * Get temporary credentials for uploading the media and wayline in DJI Pilot.
+     * @param workspaceId
+     * @return
+     */
+    @PostMapping("/{workspace_id}/sts")
+    public ResponseResult<StsCredentialsDTO> getSTSCredentials(@PathVariable(name = "workspace_id") String workspaceId) {
+
+        StsCredentialsDTO stsCredentials = storageService.getSTSCredentials();
+        return ResponseResult.success(stsCredentials);
+    }
+}
diff --git a/src/main/java/com/dji/sample/storage/service/IStorageService.java b/src/main/java/com/dji/sample/storage/service/IStorageService.java
new file mode 100644
index 0000000..0dc98c9
--- /dev/null
+++ b/src/main/java/com/dji/sample/storage/service/IStorageService.java
@@ -0,0 +1,17 @@
+package com.dji.sample.storage.service;
+
+import com.dji.sample.media.model.StsCredentialsDTO;
+
+/**
+ * @author sean
+ * @version 0.3
+ * @date 2021/12/29
+ */
+public interface IStorageService {
+
+    /**
+     * Get custom temporary credentials object for uploading the media and wayline.
+     * @return temporary credentials object
+     */
+    StsCredentialsDTO getSTSCredentials();
+}
diff --git a/src/main/java/com/dji/sample/storage/service/impl/AliyunStorageServiceImpl.java b/src/main/java/com/dji/sample/storage/service/impl/AliyunStorageServiceImpl.java
new file mode 100644
index 0000000..46366a5
--- /dev/null
+++ b/src/main/java/com/dji/sample/storage/service/impl/AliyunStorageServiceImpl.java
@@ -0,0 +1,32 @@
+package com.dji.sample.storage.service.impl;
+
+import com.dji.sample.component.oss.model.AliyunOSSConfiguration;
+import com.dji.sample.component.oss.service.impl.AliyunOssServiceImpl;
+import com.dji.sample.media.model.StsCredentialsDTO;
+import com.dji.sample.storage.service.IStorageService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+/**
+ * @author sean
+ * @version 0.3
+ * @date 2022/3/9
+ */
+@Service
+public class AliyunStorageServiceImpl implements IStorageService {
+
+    @Autowired
+    private AliyunOssServiceImpl ossService;
+
+    @Override
+    public StsCredentialsDTO getSTSCredentials() {
+        return StsCredentialsDTO.builder()
+                .endpoint(AliyunOSSConfiguration.endpoint)
+                .bucket(AliyunOSSConfiguration.bucket)
+                .credentials(ossService.getCredentials())
+                .provider(AliyunOSSConfiguration.PROVIDER)
+                .objectKeyPrefix(AliyunOSSConfiguration.objectDirPrefix)
+                .region(AliyunOSSConfiguration.region)
+                .build();
+    }
+}
diff --git a/src/main/java/com/dji/sample/storage/service/impl/MinIOStorageServiceImpl.java b/src/main/java/com/dji/sample/storage/service/impl/MinIOStorageServiceImpl.java
new file mode 100644
index 0000000..b03e856
--- /dev/null
+++ b/src/main/java/com/dji/sample/storage/service/impl/MinIOStorageServiceImpl.java
@@ -0,0 +1,34 @@
+package com.dji.sample.storage.service.impl;
+
+import com.dji.sample.component.oss.model.MinIOConfiguration;
+import com.dji.sample.component.oss.service.impl.MinIOServiceImpl;
+import com.dji.sample.media.model.StsCredentialsDTO;
+import com.dji.sample.storage.service.IStorageService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+/**
+ * @author sean
+ * @version 0.3
+ * @date 2021/12/31
+ */
+@Service
+public class MinIOStorageServiceImpl implements IStorageService {
+
+    @Autowired
+    private MinIOServiceImpl ossService;
+
+    @Override
+    public StsCredentialsDTO getSTSCredentials() {
+        return StsCredentialsDTO.builder()
+                .endpoint(MinIOConfiguration.endpoint)
+                .bucket(MinIOConfiguration.bucket)
+                .credentials(ossService.getCredentials())
+                .provider(MinIOConfiguration.PROVIDER)
+                .objectKeyPrefix(MinIOConfiguration.objectDirPrefix)
+                .region(MinIOConfiguration.region)
+                .build();
+    }
+
+
+}
diff --git a/src/main/java/com/dji/sample/wayline/controller/WaylineFileController.java b/src/main/java/com/dji/sample/wayline/controller/WaylineFileController.java
new file mode 100644
index 0000000..dbd9025
--- /dev/null
+++ b/src/main/java/com/dji/sample/wayline/controller/WaylineFileController.java
@@ -0,0 +1,150 @@
+package com.dji.sample.wayline.controller;
+
+import com.dji.sample.common.model.CustomClaim;
+import com.dji.sample.common.model.PaginationData;
+import com.dji.sample.common.model.ResponseResult;
+import com.dji.sample.component.oss.model.AliyunOSSConfiguration;
+import com.dji.sample.wayline.model.WaylineFileDTO;
+import com.dji.sample.wayline.model.WaylineFileUploadDTO;
+import com.dji.sample.wayline.model.WaylineQueryParam;
+import com.dji.sample.wayline.service.IWaylineFileService;
+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.io.IOException;
+import java.net.URL;
+import java.util.List;
+
+import static com.dji.sample.component.AuthInterceptor.TOKEN_CLAIM;
+
+/**
+ * @author sean
+ * @version 0.3
+ * @date 2021/12/22
+ */
+@RestController
+@RequestMapping("${url.wayline.prefix}${url.wayline.version}/workspaces")
+public class WaylineFileController {
+
+    @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.
+     * @param orderBy   Sorted fields. Spliced at the end of the sql statement.
+     * @param favorited Whether the wayline file is favorited or not.
+     * @param page
+     * @param pageSize
+     * @param templateType
+     * @param workspaceId
+     * @return
+     */
+    @GetMapping("/{workspace_id}/waylines")
+    public ResponseResult<PaginationData<WaylineFileDTO>> getWaylinesPagination(@RequestParam(name = "order_by") String orderBy,
+                                      @RequestParam(required = false) boolean favorited, @RequestParam Integer page,
+                                      @RequestParam(name = "page_size", defaultValue = "10") Integer pageSize,
+                                      @RequestParam(name = "template_type", required = false) Integer[] templateType,
+                                      @PathVariable(name = "workspace_id") String workspaceId) {
+        WaylineQueryParam param = WaylineQueryParam.builder()
+                .favorited(favorited)
+                .page(page)
+                .pageSize(pageSize)
+                .orderBy(orderBy)
+                .templateType(templateType)
+                .build();
+        PaginationData<WaylineFileDTO> data = waylineFileService.getWaylinesByParam(workspaceId, param);
+        return ResponseResult.success(data);
+    }
+
+    /**
+     * Query the download address of the file according to the wayline file id,
+     * and redirect to this address directly for download.
+     * @param workspaceId
+     * @param waylineId
+     * @param response
+     */
+    @GetMapping("/{workspace_id}/waylines/{wayline_id}/url")
+    public void getFileUrl(@PathVariable(name = "workspace_id") String workspaceId,
+                                @PathVariable(name = "wayline_id") String waylineId, HttpServletResponse response) {
+
+        WaylineFileDTO wayline = waylineFileService.getWaylineByWaylineId(workspaceId, waylineId);
+        URL url = waylineFileService.getObjectUrl(AliyunOSSConfiguration.bucket, wayline.getObjectKey());
+
+        try {
+            response.sendRedirect(url.toString());
+        } catch (IOException 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 request
+     * @param workspaceId
+     * @param uploadFile
+     * @return
+     */
+    @PostMapping("/{workspace_id}/upload-callback")
+    public ResponseResult uploadCallBack(HttpServletRequest request,
+                                         @PathVariable(name = "workspace_id") String workspaceId,
+                                         @RequestBody WaylineFileUploadDTO uploadFile) {
+
+        CustomClaim customClaim = (CustomClaim)request.getAttribute(TOKEN_CLAIM);
+
+        WaylineFileDTO metadata = uploadFile.getMetadata();
+        metadata.setUsername(customClaim.getUsername());
+        metadata.setObjectKey(uploadFile.getObjectKey());
+        metadata.setName(uploadFile.getName());
+
+        int id = waylineFileService.saveWaylineFile(workspaceId, metadata);
+
+        return id <= 0 ? ResponseResult.error() : ResponseResult.success();
+    }
+
+    /**
+     * Favorite the wayline file according to the wayline file id.
+     * @param workspaceId
+     * @param ids   wayline file id
+     * @return
+     */
+    @PostMapping("/{workspace_id}/favorites")
+    public ResponseResult markFavorite(@PathVariable(name = "workspace_id") String workspaceId,
+                             @RequestParam(name = "id") List<String> ids) {
+        boolean isMark = waylineFileService.markFavorite(workspaceId, ids, true);
+
+        return isMark ? ResponseResult.success() : ResponseResult.error();
+    }
+
+    /**
+     * Delete the favorites of this wayline file based on the wayline file id.
+     * @param workspaceId
+     * @param ids wayline file id
+     * @return
+     */
+    @DeleteMapping("/{workspace_id}/favorites")
+    public ResponseResult unmarkFavorite(@PathVariable(name = "workspace_id") String workspaceId,
+                             @RequestParam(name = "id") List<String> ids) {
+        boolean isMark = waylineFileService.markFavorite(workspaceId, 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 workspaceId
+     * @param names
+     * @return
+     */
+    @GetMapping("/{workspace_id}/waylines/duplicate-names")
+    public ResponseResult checkDuplicateNames(@PathVariable(name = "workspace_id") String workspaceId,
+                                              @RequestParam(name = "name") List<String> names) {
+        List<String> existNamesList = waylineFileService.getDuplicateNames(workspaceId, names);
+
+        return ResponseResult.success(existNamesList);
+    }
+}
diff --git a/src/main/java/com/dji/sample/wayline/dao/IWaylineFileMapper.java b/src/main/java/com/dji/sample/wayline/dao/IWaylineFileMapper.java
new file mode 100644
index 0000000..10a0520
--- /dev/null
+++ b/src/main/java/com/dji/sample/wayline/dao/IWaylineFileMapper.java
@@ -0,0 +1,12 @@
+package com.dji.sample.wayline.dao;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.dji.sample.wayline.model.WaylineFileEntity;
+
+/**
+ * @author sean
+ * @version 0.3
+ * @date 2021/12/22
+ */
+public interface IWaylineFileMapper extends BaseMapper<WaylineFileEntity> {
+}
diff --git a/src/main/java/com/dji/sample/wayline/model/WaylineFileDTO.java b/src/main/java/com/dji/sample/wayline/model/WaylineFileDTO.java
new file mode 100644
index 0000000..9b044db
--- /dev/null
+++ b/src/main/java/com/dji/sample/wayline/model/WaylineFileDTO.java
@@ -0,0 +1,41 @@
+package com.dji.sample.wayline.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+/**
+ * @author sean
+ * @version 0.3
+ * @date 2021/12/22
+ */
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class WaylineFileDTO {
+
+    private String name;
+
+    @JsonProperty("id")
+    private String waylineId;
+
+    private String droneModelKey;
+
+    private List<String> payloadModelKeys;
+
+    private Boolean favorited;
+
+    private List<Integer> templateTypes;
+
+    private String objectKey;
+
+    @JsonProperty("user_name")
+    private String username;
+
+    private Long updateTime;
+}
diff --git a/src/main/java/com/dji/sample/wayline/model/WaylineFileEntity.java b/src/main/java/com/dji/sample/wayline/model/WaylineFileEntity.java
new file mode 100644
index 0000000..34295b3
--- /dev/null
+++ b/src/main/java/com/dji/sample/wayline/model/WaylineFileEntity.java
@@ -0,0 +1,57 @@
+package com.dji.sample.wayline.model;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * @author sean
+ * @version 0.3
+ * @date 2021/12/22
+ */
+@Data
+@TableName("wayline_file")
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class WaylineFileEntity {
+
+    @TableId(type = IdType.AUTO)
+    private Integer id;
+
+    @TableField("name")
+    private String name;
+
+    @TableField("wayline_id")
+    private String waylineId;
+
+    @TableField("drone_model_key")
+    private String droneModelKey;
+
+    @TableField("payload_model_keys")
+    private String payloadModelKeys;
+
+    @TableField("workspace_id")
+    private String workspaceId;
+
+    @TableField("favorited")
+    private Boolean favorited;
+
+    @TableField("template_types")
+    private String templateTypes;
+
+    @TableField("object_key")
+    private String objectKey;
+
+    @TableField("user_name")
+    private String username;
+
+    @TableField(value = "create_time", fill = FieldFill.INSERT)
+    private Long createTime;
+
+    @TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
+    private Long updateTime;
+
+}
diff --git a/src/main/java/com/dji/sample/wayline/model/WaylineFileUploadDTO.java b/src/main/java/com/dji/sample/wayline/model/WaylineFileUploadDTO.java
new file mode 100644
index 0000000..53b7996
--- /dev/null
+++ b/src/main/java/com/dji/sample/wayline/model/WaylineFileUploadDTO.java
@@ -0,0 +1,18 @@
+package com.dji.sample.wayline.model;
+
+import lombok.Data;
+
+/**
+ * @author sean
+ * @version 0.3
+ * @date 2021/12/23
+ */
+@Data
+public class WaylineFileUploadDTO {
+
+    private String objectKey;
+
+    private String name;
+
+    private WaylineFileDTO metadata;
+}
diff --git a/src/main/java/com/dji/sample/wayline/model/WaylineQueryParam.java b/src/main/java/com/dji/sample/wayline/model/WaylineQueryParam.java
new file mode 100644
index 0000000..21db9fe
--- /dev/null
+++ b/src/main/java/com/dji/sample/wayline/model/WaylineQueryParam.java
@@ -0,0 +1,30 @@
+package com.dji.sample.wayline.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * @author sean
+ * @version 0.3
+ * @date 2021/12/22
+ */
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class WaylineQueryParam {
+
+    private boolean favorited;
+
+    @Builder.Default
+    private int page = 1;
+
+    @Builder.Default
+    private int pageSize = 10;
+
+    private String orderBy;
+
+    private Integer[] templateType;
+}
diff --git a/src/main/java/com/dji/sample/wayline/service/IWaylineFileService.java b/src/main/java/com/dji/sample/wayline/service/IWaylineFileService.java
new file mode 100644
index 0000000..941890d
--- /dev/null
+++ b/src/main/java/com/dji/sample/wayline/service/IWaylineFileService.java
@@ -0,0 +1,65 @@
+package com.dji.sample.wayline.service;
+
+import com.dji.sample.common.model.PaginationData;
+import com.dji.sample.wayline.model.WaylineFileDTO;
+import com.dji.sample.wayline.model.WaylineQueryParam;
+
+import java.net.URL;
+import java.util.List;
+
+/**
+ * @author sean
+ * @version 0.3
+ * @date 2021/12/22
+ */
+public interface IWaylineFileService {
+
+    /**
+     * Perform paging queries based on query parameters.
+     * @param workspaceId
+     * @param param
+     * @return
+     */
+    PaginationData<WaylineFileDTO> getWaylinesByParam(String workspaceId, WaylineQueryParam param);
+
+    /**
+     * Query the information of this wayline file according to the wayline file id.
+     * @param workspaceId
+     * @param waylineId
+     * @return
+     */
+    WaylineFileDTO getWaylineByWaylineId(String workspaceId, String waylineId);
+
+    /**
+     * Get the download address of the file object.
+     * @param bucket    bucket name
+     * @param objectKey object name
+     * @return
+     */
+    URL getObjectUrl(String bucket, String objectKey);
+
+    /**
+     * Save the basic information of the wayline file.
+     * @param workspaceId
+     * @param metadata
+     * @return
+     */
+    Integer saveWaylineFile(String workspaceId, WaylineFileDTO metadata);
+
+    /**
+     * Updates whether the file is collected or not based on the passed parameters.
+     * @param workspaceId
+     * @param ids          wayline id
+     * @param isFavorite   Whether the wayline file is favorited or not.
+     * @return
+     */
+    Boolean markFavorite(String workspaceId, List<String> ids, Boolean isFavorite);
+
+    /**
+     * Batch query for duplicate file names in workspace.
+     * @param workspaceId
+     * @param names
+     * @return
+     */
+    List<String> getDuplicateNames(String workspaceId, List<String> names);
+}
diff --git a/src/main/java/com/dji/sample/wayline/service/impl/WaylineFileServiceImpl.java b/src/main/java/com/dji/sample/wayline/service/impl/WaylineFileServiceImpl.java
new file mode 100644
index 0000000..eb6bc5e
--- /dev/null
+++ b/src/main/java/com/dji/sample/wayline/service/impl/WaylineFileServiceImpl.java
@@ -0,0 +1,179 @@
+package com.dji.sample.wayline.service.impl;
+
+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.dji.sample.common.model.Pagination;
+import com.dji.sample.common.model.PaginationData;
+import com.dji.sample.component.oss.model.AliyunOSSConfiguration;
+import com.dji.sample.component.oss.model.MinIOConfiguration;
+import com.dji.sample.component.oss.service.IOssService;
+import com.dji.sample.component.oss.service.impl.AliyunOssServiceImpl;
+import com.dji.sample.component.oss.service.impl.MinIOServiceImpl;
+import com.dji.sample.wayline.dao.IWaylineFileMapper;
+import com.dji.sample.wayline.model.WaylineFileDTO;
+import com.dji.sample.wayline.model.WaylineFileEntity;
+import com.dji.sample.wayline.model.WaylineQueryParam;
+import com.dji.sample.wayline.service.IWaylineFileService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+
+import java.net.URL;
+import java.util.Arrays;
+import java.util.List;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+/**
+ * @author sean
+ * @version 0.3
+ * @date 2021/12/22
+ */
+@Service
+@Transactional
+public class WaylineFileServiceImpl implements IWaylineFileService {
+
+    @Autowired
+    private IWaylineFileMapper mapper;
+
+    private IOssService ossService;
+
+    @Autowired
+    private void setOssService(@Autowired AliyunOssServiceImpl aliyunOssService, @Autowired MinIOServiceImpl minIOService) {
+        if (AliyunOSSConfiguration.enable) {
+            this.ossService = aliyunOssService;
+            return;
+        }
+        if (MinIOConfiguration.enable) {
+            this.ossService = minIOService;
+            return;
+        }
+        throw new NullPointerException("ossService is null.");
+    }
+
+    @Override
+    public PaginationData<WaylineFileDTO> getWaylinesByParam(String workspaceId, WaylineQueryParam param) {
+        // Paging Query
+        Page<WaylineFileEntity> page = mapper.selectPage(
+                new Page<WaylineFileEntity>(param.getPage(), param.getPageSize()),
+                new LambdaQueryWrapper<WaylineFileEntity>()
+                        .eq(WaylineFileEntity::getWorkspaceId, workspaceId)
+                        .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 WaylineFileDTO getWaylineByWaylineId(String workspaceId, String waylineId) {
+        return this.entityConvertToDTO(
+                mapper.selectOne(
+                        new LambdaQueryWrapper<WaylineFileEntity>()
+                                .eq(WaylineFileEntity::getWorkspaceId, workspaceId)
+                                .eq(WaylineFileEntity::getWaylineId, waylineId)));
+    }
+
+    @Override
+    public URL getObjectUrl(String bucket, String objectKey) {
+        return ossService.getObjectUrl(bucket, objectKey);
+    }
+
+    @Override
+    public Integer saveWaylineFile(String workspaceId, WaylineFileDTO metadata) {
+        WaylineFileEntity file = this.dtoConvertToEntity(metadata);
+        file.setWaylineId(UUID.randomUUID().toString());
+        file.setWorkspaceId(workspaceId);
+
+        int insertId = mapper.insert(file);
+        return insertId > 0 ? file.getId() : insertId;
+    }
+
+    @Override
+    public Boolean markFavorite(String workspaceId, 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)
+                .eq(WaylineFileEntity::getWorkspaceId, workspaceId)
+                .in(WaylineFileEntity::getWaylineId, waylineIds)) > 0;
+    }
+
+    @Override
+    public List<String> getDuplicateNames(String workspaceId, List<String> names) {
+        return mapper.selectList(new LambdaQueryWrapper<WaylineFileEntity>()
+                .eq(WaylineFileEntity::getWorkspaceId, workspaceId)
+                .in(WaylineFileEntity::getName, names))
+                .stream()
+                .map(WaylineFileEntity::getName)
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * Convert database entity objects into wayline data transfer object.
+     * @param entity
+     * @return
+     */
+    private WaylineFileDTO entityConvertToDTO(WaylineFileEntity entity) {
+        WaylineFileDTO.WaylineFileDTOBuilder builder = WaylineFileDTO.builder();
+
+        if (entity != null) {
+            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())
+                    .updateTime(entity.getUpdateTime())
+                    .waylineId(entity.getWaylineId());
+        }
+
+        return builder.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())
+                    .build();
+        }
+
+        return builder.build();
+    }
+}
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
new file mode 100644
index 0000000..57357e6
--- /dev/null
+++ b/src/main/resources/application.yml
@@ -0,0 +1,86 @@
+server:
+  port: 6789
+spring:
+  application:
+    name: cloud-api-sample
+  datasource:
+    druid:
+      type: com.alibaba.druid.pool.DruidDataSource
+      driver-class-name: com.mysql.cj.jdbc.Driver
+      url: jdbc:mysql://localhost:3306/cloud_sample?useSSL=false
+      username: root
+      password: root
+      initial-size: 10
+      min-idle: 10
+      max-active: 20
+      max-wait: 60000
+
+  jackson:
+    property-naming-strategy: SNAKE_CASE
+    date-format: yyyy-MM-dd HH:mm:ss
+    default-property-inclusion: NON_NULL
+    deserialization:
+      FAIL_ON_UNKNOWN_PROPERTIES: false
+
+jwt:
+  issuer: DJI
+  subject: CloudApiSample
+  secret: CloudApiSample
+  age: 86400
+
+mqtt:
+  protocol: tcp
+  host: Please enter your IP.
+  port: 1883
+  username: JavaServer
+  password: 123456
+  client-id: 123456
+  # Topics that need to be subscribed when initially connecting to mqtt, multiple topics are divided by ",".
+  inbound-topic: sys/product/+/status
+
+url:
+  manage:
+    prefix: /manage
+    version: /api/v1
+  map:
+    prefix: /map
+    version: /api/v1
+  media:
+    prefix: /media
+    version: /api/v1
+  wayline:
+    prefix: /wayline
+    version: /api/v1
+  storage:
+    prefix: /storage
+    version: /api/v1
+
+aliyun:
+  oss:
+    enable: true
+    endpoint: Please enter your endpoint. #Example: https://oss-cn-shenzhen.aliyuncs.com
+    access-key: Please enter your access key.
+    secret-key: Please enter your secret key.
+    expire: 3600
+    region: Please enter oss region.
+    role-session-name: Please enter session name.
+    role-arn: Please enter role arn.
+    bucket: Please enter bucket name.
+    object-dir-prefix: Please enter object prefix.
+
+# MinIO is temporarily unavailable.
+minio:
+  enable: false
+  endpoint: Please enter your endpoint. #http://192.168.1.1:9000/
+  access-key: minioadmin
+  secret-key: minioadmin
+  bucket: Please enter bucket name.
+  expire: 3600
+  region: Please enter minio region.
+  object-dir-prefix: Please enter object prefix.
+
+logging:
+  level:
+    com.dji.sample: debug
+  file:
+    name: logs/cloud-api-sample.log

--
Gitblit v1.9.3