diff --git a/admin/pom.xml b/admin/pom.xml
new file mode 100644
index 0000000..69fe99a
--- /dev/null
+++ b/admin/pom.xml
@@ -0,0 +1,151 @@
+
+
+ com.multictrl
+ Dock-MultiCtrl
+ 5.5.0
+
+ 4.0.0
+ admin
+ jar
+ admin
+
+
+ 2.5.2
+ 2.1.0
+ 1.6.2
+ 3.2.1
+ 8.6.0
+ 4.9.2
+ 6.3.1
+ 7.2.0
+ 1.6.1
+ 2.19.0
+ 0.8
+
+
+
+
+ com.multictrl
+ common
+ 5.5.0
+
+
+ com.multictrl
+ dynamic-datasource
+ 5.5.0
+
+
+ org.springframework.boot
+ spring-boot-starter-quartz
+
+
+ org.apache.shiro
+ shiro-spring
+ jakarta
+ ${shiro.version}
+
+
+
+ org.apache.shiro
+ shiro-core
+
+
+ org.apache.shiro
+ shiro-web
+
+
+
+
+
+ org.apache.shiro
+ shiro-core
+ jakarta
+ ${shiro.version}
+
+
+ org.apache.shiro
+ shiro-web
+ jakarta
+ ${shiro.version}
+
+
+ org.apache.shiro
+ shiro-core
+
+
+
+
+ com.github.whvcse
+ easy-captcha
+ ${captcha.version}
+
+
+ com.alibaba
+ easyexcel
+ ${easyexcel.version}
+
+
+ io.minio
+ minio
+ ${minio.version}
+
+
+ com.squareup.okhttp3
+ okhttp
+ ${okhttp.version}
+
+
+
+ org.springframework.integration
+ spring-integration-mqtt
+ ${mqtt.version}
+
+
+
+ com.influxdb
+ influxdb-client-java
+ ${influxdb.version}
+
+
+
+ dom4j
+ dom4j
+ ${dom4j.version}
+
+
+
+ com.drewnoakes
+ metadata-extractor
+ ${metadata.version}
+
+
+
+ org.locationtech.spatial4j
+ spatial4j
+ ${spatial4j.version}
+
+
+
+ org.springframework.cloud
+ spring-cloud-starter-openfeign
+
+
+
+
+ dj-multictrl-admin
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+ true
+
+
+
+
+
diff --git a/admin/src/main/java/com/multictrl/AdminApplication.java b/admin/src/main/java/com/multictrl/AdminApplication.java
new file mode 100644
index 0000000..14d098d
--- /dev/null
+++ b/admin/src/main/java/com/multictrl/AdminApplication.java
@@ -0,0 +1,45 @@
+package com.multictrl;
+
+import com.multictrl.common.utils.DateUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.builder.SpringApplicationBuilder;
+import org.springframework.boot.context.event.ApplicationReadyEvent;
+import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
+import org.springframework.cloud.openfeign.EnableFeignClients;
+import org.springframework.context.event.EventListener;
+import org.springframework.scheduling.annotation.EnableScheduling;
+
+import java.util.Date;
+
+
+/**
+ * DJ-MultiCtrl
+ *
+ * @author Sdy
+ */
+@Slf4j
+@EnableScheduling
+@EnableFeignClients
+@SpringBootApplication
+public class AdminApplication extends SpringBootServletInitializer {
+
+ public static void main(String[] args) {
+ SpringApplication.run(AdminApplication.class, args);
+ }
+
+ @Override
+ protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
+ return application.sources(AdminApplication.class);
+ }
+
+ @EventListener(ApplicationReadyEvent.class)
+ public void onApplicationReady(ApplicationReadyEvent event) {
+ log.info("\n" +
+ "====================================================================================================================\n" +
+ " [DJ-MultiCtrl] 系统启动完成!\n" +
+ " 当前时间: " + DateUtils.format(new Date(), DateUtils.DATE_TIME_PATTERN) + "\n" +
+ "====================================================================================================================\n");
+ }
+}
\ No newline at end of file
diff --git a/admin/src/main/java/com/multictrl/common/annotation/ApiOrder.java b/admin/src/main/java/com/multictrl/common/annotation/ApiOrder.java
new file mode 100644
index 0000000..560172a
--- /dev/null
+++ b/admin/src/main/java/com/multictrl/common/annotation/ApiOrder.java
@@ -0,0 +1,29 @@
+package com.multictrl.common.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Swagger文档目录排序注解
+ *
+ * 用法:在Controller类上与@Tag一起使用,value值越小越靠前
+ *
{@code
+ * @Tag(name = "机库信息表")
+ * @ApiOrder(1)
+ * }
+ *
+ * @author Sdy
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface ApiOrder {
+
+ /**
+ * 排序序号,数值越小显示越靠前
+ */
+ int value() default 9999;
+}
diff --git a/admin/src/main/java/com/multictrl/common/annotation/DataFilter.java b/admin/src/main/java/com/multictrl/common/annotation/DataFilter.java
new file mode 100644
index 0000000..8747c73
--- /dev/null
+++ b/admin/src/main/java/com/multictrl/common/annotation/DataFilter.java
@@ -0,0 +1,29 @@
+package com.multictrl.common.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 数据过滤注解
+ *
+ * @author Sdy
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface DataFilter {
+ /**
+ * 表的别名
+ */
+ String tableAlias() default "";
+
+ /**
+ * 用户ID
+ */
+ String userId() default "creator";
+
+ /**
+ * 部门ID
+ */
+ String deptId() default "dept_id";
+
+}
\ No newline at end of file
diff --git a/admin/src/main/java/com/multictrl/common/annotation/LogOperation.java b/admin/src/main/java/com/multictrl/common/annotation/LogOperation.java
new file mode 100644
index 0000000..4a5b9a9
--- /dev/null
+++ b/admin/src/main/java/com/multictrl/common/annotation/LogOperation.java
@@ -0,0 +1,20 @@
+package com.multictrl.common.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 操作日志注解
+ *
+ * @author Sdy
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface LogOperation {
+
+ String value() default "";
+}
diff --git a/admin/src/main/java/com/multictrl/common/aspect/DataFilterAspect.java b/admin/src/main/java/com/multictrl/common/aspect/DataFilterAspect.java
new file mode 100644
index 0000000..bcd2862
--- /dev/null
+++ b/admin/src/main/java/com/multictrl/common/aspect/DataFilterAspect.java
@@ -0,0 +1,99 @@
+package com.multictrl.common.aspect;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.StrUtil;
+import com.multictrl.common.annotation.DataFilter;
+import com.multictrl.common.constant.Constant;
+import com.multictrl.common.exception.ErrorCode;
+import com.multictrl.common.exception.RenException;
+import com.multictrl.common.interceptor.DataScope;
+import com.multictrl.modules.security.user.SecurityUser;
+import com.multictrl.modules.security.user.UserDetail;
+import com.multictrl.modules.sys.enums.SuperAdminEnum;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Before;
+import org.aspectj.lang.annotation.Pointcut;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.stereotype.Component;
+
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 数据过滤,切面处理类
+ *
+ * @author Sdy
+ */
+@Aspect
+@Component
+public class DataFilterAspect {
+
+ @Pointcut("@annotation(com.multictrl.common.annotation.DataFilter)")
+ public void dataFilterCut() {
+
+ }
+
+ @Before("dataFilterCut()")
+ public void dataFilter(JoinPoint point) {
+ Object params = point.getArgs()[0];
+ if (params instanceof Map) {
+ UserDetail user = SecurityUser.getUser();
+
+ //如果是超级管理员,则不进行数据过滤
+ if (user.getSuperAdmin() == SuperAdminEnum.YES.value()) {
+ return;
+ }
+
+ try {
+ //否则进行数据过滤
+ Map map = (Map) params;
+ String sqlFilter = getSqlFilter(user, point);
+ map.put(Constant.SQL_FILTER, new DataScope(sqlFilter));
+ } catch (Exception ignored) {
+
+ }
+
+ return;
+ }
+
+ throw new RenException(ErrorCode.DATA_SCOPE_PARAMS_ERROR);
+ }
+
+ /**
+ * 获取数据过滤的SQL
+ */
+ private String getSqlFilter(UserDetail user, JoinPoint point) throws Exception {
+ MethodSignature signature = (MethodSignature) point.getSignature();
+ Method method = point.getTarget().getClass().getDeclaredMethod(signature.getName(), signature.getParameterTypes());
+ DataFilter dataFilter = method.getAnnotation(DataFilter.class);
+
+ //获取表的别名
+ String tableAlias = dataFilter.tableAlias();
+ if (StrUtil.isNotBlank(tableAlias)) {
+ tableAlias += ".";
+ }
+
+ StringBuilder sqlFilter = new StringBuilder();
+ sqlFilter.append(" (");
+
+ //部门ID列表
+ List deptIdList = user.getDeptIdList();
+ if (CollUtil.isNotEmpty(deptIdList)) {
+ sqlFilter.append(tableAlias).append(dataFilter.deptId());
+
+ sqlFilter.append(" in(").append(CollUtil.join(deptIdList, ",")).append(")");
+ }
+
+ //查询本人数据
+ if (CollUtil.isNotEmpty(deptIdList)) {
+ sqlFilter.append(" or ");
+ }
+ sqlFilter.append(tableAlias).append(dataFilter.userId()).append("=").append(user.getId());
+
+ sqlFilter.append(")");
+
+ return sqlFilter.toString();
+ }
+}
\ No newline at end of file
diff --git a/admin/src/main/java/com/multictrl/common/aspect/LogOperationAspect.java b/admin/src/main/java/com/multictrl/common/aspect/LogOperationAspect.java
new file mode 100644
index 0000000..bbbaaaa
--- /dev/null
+++ b/admin/src/main/java/com/multictrl/common/aspect/LogOperationAspect.java
@@ -0,0 +1,102 @@
+package com.multictrl.common.aspect;
+
+import com.multictrl.common.annotation.LogOperation;
+import com.multictrl.common.utils.HttpContextUtils;
+import com.multictrl.common.utils.IpUtils;
+import com.multictrl.common.utils.JsonUtils;
+import com.multictrl.modules.log.entity.SysLogOperationEntity;
+import com.multictrl.modules.log.enums.OperationStatusEnum;
+import com.multictrl.modules.log.service.SysLogOperationService;
+import com.multictrl.modules.security.user.SecurityUser;
+import com.multictrl.modules.security.user.UserDetail;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.AllArgsConstructor;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Pointcut;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.http.HttpHeaders;
+import org.springframework.stereotype.Component;
+
+import java.lang.reflect.Method;
+
+/**
+ * 操作日志,切面处理类
+ *
+ * @author Sdy
+ */
+@Aspect
+@Component
+@AllArgsConstructor
+public class LogOperationAspect {
+ private final SysLogOperationService sysLogOperationService;
+
+ @Pointcut("@annotation(com.multictrl.common.annotation.LogOperation)")
+ public void logPointCut() {
+
+ }
+
+ @Around("logPointCut()")
+ public Object around(ProceedingJoinPoint point) throws Throwable {
+ long beginTime = System.currentTimeMillis();
+ try {
+ //执行方法
+ Object result = point.proceed();
+
+ //执行时长(毫秒)
+ long time = System.currentTimeMillis() - beginTime;
+ //保存日志
+ saveLog(point, time, OperationStatusEnum.SUCCESS.value());
+
+ return result;
+ } catch (Exception e) {
+ //执行时长(毫秒)
+ long time = System.currentTimeMillis() - beginTime;
+ //保存日志
+ saveLog(point, time, OperationStatusEnum.FAIL.value());
+
+ throw e;
+ }
+ }
+
+ private void saveLog(ProceedingJoinPoint joinPoint, long time, Integer status) throws Exception {
+ MethodSignature signature = (MethodSignature) joinPoint.getSignature();
+ Method method = joinPoint.getTarget().getClass().getDeclaredMethod(signature.getName(), signature.getParameterTypes());
+ LogOperation annotation = method.getAnnotation(LogOperation.class);
+
+ SysLogOperationEntity log = new SysLogOperationEntity();
+ if (annotation != null) {
+ //注解上的描述
+ log.setOperation(annotation.value());
+ }
+
+ //登录用户信息
+ UserDetail user = SecurityUser.getUser();
+ log.setCreatorName(user.getUsername());
+
+ log.setStatus(status);
+ log.setRequestTime((int) time);
+
+ //请求相关信息
+ HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
+ if (request != null) {
+ log.setIp(IpUtils.getIpAddr(request));
+ log.setUserAgent(request.getHeader(HttpHeaders.USER_AGENT));
+ log.setRequestUri(request.getRequestURI());
+ log.setRequestMethod(request.getMethod());
+ }
+
+ //请求参数
+ Object[] args = joinPoint.getArgs();
+ try {
+ String params = JsonUtils.toJsonString(args[0]);
+ log.setRequestParams(params);
+ } catch (Exception ignored) {
+
+ }
+
+ //保存到DB
+ sysLogOperationService.save(log);
+ }
+}
diff --git a/admin/src/main/java/com/multictrl/common/config/DJIConfig.java b/admin/src/main/java/com/multictrl/common/config/DJIConfig.java
new file mode 100644
index 0000000..2f8cf2d
--- /dev/null
+++ b/admin/src/main/java/com/multictrl/common/config/DJIConfig.java
@@ -0,0 +1,25 @@
+package com.multictrl.common.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 大疆机场属性配置
+ *
+ * @author Sdy
+ * @since 1.0.0 2026/4/22
+ */
+@Data
+@Configuration
+@ConfigurationProperties(prefix = "dji")
+public class DJIConfig {
+ private String appId;
+ private String appKey;
+ private String appLicense;
+ private String ntpServerHost;
+ private Integer ntpServerPort;
+ private String organizationName = "dji";
+ private String organizationId = "djiId";
+ private String deviceBindCode = "djiCode";
+}
diff --git a/admin/src/main/java/com/multictrl/common/config/InfluxConfig.java b/admin/src/main/java/com/multictrl/common/config/InfluxConfig.java
new file mode 100644
index 0000000..923d4a2
--- /dev/null
+++ b/admin/src/main/java/com/multictrl/common/config/InfluxConfig.java
@@ -0,0 +1,69 @@
+package com.multictrl.common.config;
+
+import com.influxdb.LogLevel;
+import com.influxdb.client.InfluxDBClient;
+import com.influxdb.client.InfluxDBClientFactory;
+import com.influxdb.client.InfluxDBClientOptions;
+import lombok.Data;
+import okhttp3.ConnectionPool;
+import okhttp3.ConnectionSpec;
+import okhttp3.OkHttpClient;
+import okhttp3.logging.HttpLoggingInterceptor;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.time.Duration;
+import java.util.Collections;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * influxdb配置
+ *
+ * @author Sdy
+ * @since 1.0.0 2026/5/3
+ */
+@Data
+@Configuration
+@ConfigurationProperties(prefix = "influx")
+public class InfluxConfig {
+
+ private Boolean saveOpen;
+ private String url;
+ private String token;
+ private String org;
+ private String bucket;
+
+ @Bean
+ public InfluxDBClient influxDBClient() {
+ // 1. 构建高性能 OkHttpClient(连接池+重试)
+ OkHttpClient.Builder okHttpClient = new OkHttpClient.Builder()
+ .connectionSpecs(Collections.singletonList(ConnectionSpec.CLEARTEXT))
+ .connectTimeout(Duration.ofSeconds(15))
+ .readTimeout(Duration.ofSeconds(300))
+ .writeTimeout(Duration.ofSeconds(120))
+ .connectionPool(new ConnectionPool(
+ 20,
+ 300,
+ TimeUnit.SECONDS
+ ))
+ .retryOnConnectionFailure(true)
+ .addInterceptor(new HttpLoggingInterceptor(message ->
+ LoggerFactory.getLogger("InfluxDB").debug("HTTP: {}", message)
+ ).setLevel(HttpLoggingInterceptor.Level.NONE));
+
+ // 2. 通过 Options 精确配置
+ InfluxDBClientOptions options = InfluxDBClientOptions.builder()
+ .url(url)
+ .authenticateToken(token.toCharArray())
+ .org(org)
+ .bucket(bucket)
+ .okHttpClient(okHttpClient)
+ .build();
+
+ InfluxDBClient client = InfluxDBClientFactory.create(options);
+ client.setLogLevel(LogLevel.NONE);
+ return client;
+ }
+}
diff --git a/admin/src/main/java/com/multictrl/common/config/MinioConfig.java b/admin/src/main/java/com/multictrl/common/config/MinioConfig.java
new file mode 100644
index 0000000..6843263
--- /dev/null
+++ b/admin/src/main/java/com/multictrl/common/config/MinioConfig.java
@@ -0,0 +1,36 @@
+package com.multictrl.common.config;
+
+import io.minio.MinioClient;
+import lombok.*;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * minio配置类
+ *
+ * @author Sdy
+ * @since 1.0.0 2026/4/16
+ */
+@Data
+@Configuration
+@ConfigurationProperties(prefix = "minio")
+public class MinioConfig {
+ private String endpoint;
+ private String accessKey;
+ private String secretKey;
+ private Other other = new Other();
+
+ @Data
+ public static class Other {
+ private String dataPath;
+ }
+
+ @Bean
+ public MinioClient minioClient() {
+ return MinioClient.builder()
+ .endpoint(endpoint)
+ .credentials(accessKey, secretKey)
+ .build();
+ }
+}
diff --git a/admin/src/main/java/com/multictrl/common/config/MqttConfig.java b/admin/src/main/java/com/multictrl/common/config/MqttConfig.java
new file mode 100644
index 0000000..6ff9381
--- /dev/null
+++ b/admin/src/main/java/com/multictrl/common/config/MqttConfig.java
@@ -0,0 +1,154 @@
+package com.multictrl.common.config;
+
+import com.multictrl.common.handler.Mqtt1MessageHandler;
+import com.multictrl.common.handler.Mqtt2MessageHandler;
+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.annotation.ServiceActivator;
+import org.springframework.integration.channel.DirectChannel;
+import org.springframework.integration.config.EnableIntegration;
+import org.springframework.integration.dsl.IntegrationFlow;
+import org.springframework.integration.mqtt.core.DefaultMqttPahoClientFactory;
+import org.springframework.integration.mqtt.inbound.MqttPahoMessageDrivenChannelAdapter;
+import org.springframework.integration.mqtt.outbound.MqttPahoMessageHandler;
+import org.springframework.integration.mqtt.support.DefaultPahoMessageConverter;
+import org.springframework.messaging.MessageChannel;
+import org.springframework.messaging.MessageHandler;
+
+/**
+ * MQTT双客户端配置
+ */
+@Data
+@Configuration
+@EnableIntegration
+@ConfigurationProperties(prefix = "mqtt")
+public class MqttConfig {
+
+ private Client client1 = new Client();
+ private Client client2 = new Client();
+
+ @Data
+ public static class Client {
+ private String url;
+ private String username;
+ private String password;
+ private String subClientId;
+ private String subTopic;
+ private String pubClientId;
+ }
+
+ // ==================== 通用工厂方法 ====================
+
+ private DefaultMqttPahoClientFactory createFactory(Client c) {
+ MqttConnectOptions opt = new MqttConnectOptions();
+ opt.setCleanSession(true);
+ opt.setServerURIs(new String[]{c.getUrl()});
+ opt.setUserName(c.getUsername());
+ opt.setPassword(c.getPassword().toCharArray());
+ opt.setConnectionTimeout(10);
+ opt.setKeepAliveInterval(60);
+ opt.setAutomaticReconnect(true);
+ DefaultMqttPahoClientFactory factory = new DefaultMqttPahoClientFactory();
+ factory.setConnectionOptions(opt);
+ return factory;
+ }
+
+ private MqttPahoMessageDrivenChannelAdapter createInbound(Client c, MessageChannel channel) {
+ MqttPahoMessageDrivenChannelAdapter adapter = new MqttPahoMessageDrivenChannelAdapter(
+ c.getSubClientId(), createFactory(c), splitTopics(c.getSubTopic()));
+ adapter.setQos(2);
+ adapter.setConverter(new DefaultPahoMessageConverter());
+ adapter.setOutputChannel(channel);
+ adapter.setCompletionTimeout(5000);
+ return adapter;
+ }
+
+ private MessageHandler createOutbound(Client c) {
+ MqttPahoMessageHandler handler = new MqttPahoMessageHandler(c.getPubClientId(), createFactory(c));
+ handler.setDefaultQos(2);
+ handler.setDefaultTopic("/default/topic");
+ handler.setAsync(true);
+ handler.setCompletionTimeout(5000);
+ return handler;
+ }
+
+ private String[] splitTopics(String topics) {
+ if (topics == null || topics.isBlank()) return new String[0];
+ return java.util.Arrays.stream(topics.split(",")).map(String::trim).filter(t -> !t.isEmpty()).toArray(String[]::new);
+ }
+
+ // ==================== 客户端1 ====================
+
+ @Bean("mqttInputChannel1")
+ public MessageChannel mqttInputChannel1() {
+ return new DirectChannel();
+ }
+
+ @Bean("mqttInboundAdapter1")
+ public MqttPahoMessageDrivenChannelAdapter mqttInboundAdapter1() {
+ return createInbound(client1, mqttInputChannel1());
+ }
+
+ @Bean
+ @ServiceActivator(inputChannel = "mqttInputChannel1")
+ public MessageHandler messageHandler1(Mqtt1MessageHandler receiverMessageHandler) {
+ return receiverMessageHandler;
+ }
+
+ @Bean("mqttOutboundChannel1")
+ public MessageChannel mqttOutboundChannel1() {
+ return new DirectChannel();
+ }
+
+ @Bean("mqttOutboundHandler1")
+ @ServiceActivator(inputChannel = "mqttOutboundChannel1")
+ public MessageHandler mqttOutboundHandler1() {
+ return createOutbound(client1);
+ }
+
+ /*@Bean("mqttOutboundFlow1")
+ public IntegrationFlow mqttOutboundFlow1() {
+ return IntegrationFlow.from(mqttOutboundChannel1())
+ .handle(mqttOutboundHandler1())
+ .get();
+ }*/
+
+ // ==================== 客户端2 ====================
+
+ @Bean("mqttInputChannel2")
+ public MessageChannel mqttInputChannel2() {
+ return new DirectChannel();
+ }
+
+ @Bean("mqttInboundAdapter2")
+ public MqttPahoMessageDrivenChannelAdapter mqttInboundAdapter2() {
+ return createInbound(client2, mqttInputChannel2());
+ }
+
+ @Bean
+ @ServiceActivator(inputChannel = "mqttInputChannel2")
+ public MessageHandler messageHandler2(Mqtt2MessageHandler receiverMessageHandler) {
+ return receiverMessageHandler;
+ }
+
+ @Bean("mqttOutboundChannel2")
+ public MessageChannel mqttOutboundChannel2() {
+ return new DirectChannel();
+ }
+
+ @Bean("mqttOutboundHandler2")
+ @ServiceActivator(inputChannel = "mqttOutboundChannel2")
+ public MessageHandler mqttOutboundHandler2() {
+ return createOutbound(client2);
+ }
+
+ /*@Bean("mqttOutboundFlow2")
+ public IntegrationFlow mqttOutboundFlow2() {
+ return IntegrationFlow.from(mqttOutboundChannel2())
+ .handle(mqttOutboundHandler2())
+ .get();
+ }*/
+}
diff --git a/admin/src/main/java/com/multictrl/common/config/MqttThreadConfig.java b/admin/src/main/java/com/multictrl/common/config/MqttThreadConfig.java
new file mode 100644
index 0000000..0578ee0
--- /dev/null
+++ b/admin/src/main/java/com/multictrl/common/config/MqttThreadConfig.java
@@ -0,0 +1,31 @@
+package com.multictrl.common.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.concurrent.*;
+
+/**
+ * MQTT线程池配置 + Channel绑定
+ */
+@Configuration
+public class MqttThreadConfig {
+
+ @Bean("mqttExecutor1")
+ public ExecutorService mqttExecutor1() {
+ ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 20, 60L, TimeUnit.SECONDS,
+ new LinkedBlockingQueue<>(200), Executors.defaultThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy());
+ // JVM关闭时优雅关闭线程池
+ Runtime.getRuntime().addShutdownHook(new Thread(executor::shutdown));
+ return executor;
+ }
+
+ @Bean("mqttExecutor2")
+ public ExecutorService mqttExecutor2() {
+ ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 20, 60L, TimeUnit.SECONDS,
+ new LinkedBlockingQueue<>(200), Executors.defaultThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy());
+ // JVM关闭时优雅关闭线程池
+ Runtime.getRuntime().addShutdownHook(new Thread(executor::shutdown));
+ return executor;
+ }
+}
diff --git a/admin/src/main/java/com/multictrl/common/config/MybatisPlusConfig.java b/admin/src/main/java/com/multictrl/common/config/MybatisPlusConfig.java
new file mode 100644
index 0000000..6d2566c
--- /dev/null
+++ b/admin/src/main/java/com/multictrl/common/config/MybatisPlusConfig.java
@@ -0,0 +1,35 @@
+package com.multictrl.common.config;
+
+import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
+import com.multictrl.common.interceptor.DataFilterInterceptor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * mybatis-plus配置
+ *
+ * @author Sdy
+ * @since 1.0.0
+ */
+@Configuration
+public class MybatisPlusConfig {
+
+ @Bean
+ public MybatisPlusInterceptor mybatisPlusInterceptor() {
+ MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
+ // 数据权限
+ mybatisPlusInterceptor.addInnerInterceptor(new DataFilterInterceptor());
+ // 分页插件
+ mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
+ // 乐观锁
+ mybatisPlusInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
+ // 防止全表更新与删除
+ mybatisPlusInterceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
+
+ return mybatisPlusInterceptor;
+ }
+
+}
\ No newline at end of file
diff --git a/admin/src/main/java/com/multictrl/common/config/SrsConfig.java b/admin/src/main/java/com/multictrl/common/config/SrsConfig.java
new file mode 100644
index 0000000..d7c9fc4
--- /dev/null
+++ b/admin/src/main/java/com/multictrl/common/config/SrsConfig.java
@@ -0,0 +1,29 @@
+package com.multictrl.common.config;
+
+import feign.auth.BasicAuthRequestInterceptor;
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 直播服务器配置
+ *
+ * @author Sdy
+ * @since 1.0.0 2026/5/7
+ */
+@Data
+@Configuration
+@ConfigurationProperties(prefix = "srs")
+public class SrsConfig {
+ private String ip;
+ private String rtmpPort;
+ private String rtmpToken;
+ private String username;
+ private String password;
+
+ @Bean
+ public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
+ return new BasicAuthRequestInterceptor(username, password);
+ }
+}
diff --git a/admin/src/main/java/com/multictrl/common/config/SwaggerConfig.java b/admin/src/main/java/com/multictrl/common/config/SwaggerConfig.java
new file mode 100644
index 0000000..e9bb934
--- /dev/null
+++ b/admin/src/main/java/com/multictrl/common/config/SwaggerConfig.java
@@ -0,0 +1,131 @@
+package com.multictrl.common.config;
+
+import com.multictrl.common.annotation.ApiOrder;
+import com.multictrl.common.constant.Constant;
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.Operation;
+import io.swagger.v3.oas.models.PathItem;
+import io.swagger.v3.oas.models.info.Info;
+import io.swagger.v3.oas.models.security.SecurityRequirement;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.extern.slf4j.Slf4j;
+import org.springdoc.core.customizers.OpenApiCustomizer;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+
+/**
+ * Swagger配置
+ *
+ * @author Sdy
+ */
+@Slf4j
+@Configuration
+public class SwaggerConfig {
+
+ @Bean
+ public OpenAPI createRestApi() {
+ return new OpenAPI()
+ .info(apiInfo())
+ .security(security());
+ }
+
+ /**
+ * 按Controller上的@ApiOrder注解对Swagger Tag排序
+ */
+ @Bean
+ public OpenApiCustomizer openApiCustomizer(@Lazy ApplicationContext ctx) {
+ return openApi -> {
+ Map tagOrderMap = buildTagOrderMap(ctx);
+ log.info("=== Swagger Tag Order Map: {} ===", tagOrderMap);
+
+ // springdoc 3.x: openApi.getTags() 为空,需要从Paths中收集Tag
+ List allTags = collectTagsFromPaths(openApi);
+ log.info("=== Collected Tags: {} ===",
+ allTags.stream().map(io.swagger.v3.oas.models.tags.Tag::getName).collect(Collectors.toList()));
+
+ // 按@ApiOrder排序
+ allTags.sort(Comparator.comparingInt(
+ t -> tagOrderMap.getOrDefault(t.getName(), 9999)
+ ));
+
+ openApi.tags(allTags);
+ };
+ }
+
+ /**
+ * 从OpenAPI的Paths中收集所有用到的Tag(去重)
+ */
+ private List collectTagsFromPaths(OpenAPI openApi) {
+ Set tagNameSet = new LinkedHashSet<>();
+ if (openApi.getPaths() != null) {
+ for (PathItem pathItem : openApi.getPaths().values()) {
+ for (Operation op : pathItem.readOperations()) {
+ if (op.getTags() != null) {
+ tagNameSet.addAll(op.getTags());
+ }
+ }
+ }
+ }
+ return tagNameSet.stream()
+ .map(name -> new io.swagger.v3.oas.models.tags.Tag().name(name))
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * 扫描所有带@RestController和@Tag的类,读取@ApiOrder值
+ */
+ private Map buildTagOrderMap(ApplicationContext ctx) {
+ Map orderMap = new ConcurrentHashMap<>();
+ try {
+ String[] beanNames = ctx.getBeanNamesForAnnotation(RestController.class);
+ for (String beanName : beanNames) {
+ Object bean = ctx.getBean(beanName);
+ Class> clazz = bean.getClass();
+ // 处理CGLIB/JDK代理,获取原始类
+ while (clazz.getName().contains("$$") || clazz.getName().contains("Proxy")) {
+ Class> superClazz = clazz.getSuperclass();
+ if (superClazz != null && !superClazz.equals(Object.class)) {
+ clazz = superClazz;
+ } else {
+ break;
+ }
+ }
+
+ ApiOrder apiOrder = AnnotationUtils.findAnnotation(clazz, ApiOrder.class);
+ Tag tag = AnnotationUtils.findAnnotation(clazz, Tag.class);
+
+ if (tag != null && apiOrder != null) {
+ orderMap.put(tag.name(), apiOrder.value());
+ log.info("Found ApiOrder: {} -> {}", tag.name(), apiOrder.value());
+ }
+ }
+ } catch (Exception e) {
+ log.warn("Failed to build tag order map: {}", e.getMessage());
+ }
+ return orderMap;
+ }
+
+ private Info apiInfo() {
+ return new Info()
+ .title("大疆机场操控系统")
+ .description("api文档")
+ .version("5.x");
+ }
+
+ private List security() {
+ SecurityRequirement key = new SecurityRequirement();
+ key.addList(Constant.TOKEN_HEADER, Constant.TOKEN_HEADER);
+
+ List list = new ArrayList<>();
+ list.add(key);
+ return list;
+ }
+}
diff --git a/admin/src/main/java/com/multictrl/common/config/SysConfig.java b/admin/src/main/java/com/multictrl/common/config/SysConfig.java
new file mode 100644
index 0000000..102a9de
--- /dev/null
+++ b/admin/src/main/java/com/multictrl/common/config/SysConfig.java
@@ -0,0 +1,23 @@
+package com.multictrl.common.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 系统配置信息
+ *
+ * @author Sdy
+ * @since 1.0.0 2026/4/26
+ */
+@Data
+@Configuration
+@ConfigurationProperties(prefix = "host")
+public class SysConfig {
+
+ private String ip;
+
+ private String port;
+
+ private Boolean isSsl;
+}
diff --git a/admin/src/main/java/com/multictrl/common/constant/BusinessConstant.java b/admin/src/main/java/com/multictrl/common/constant/BusinessConstant.java
new file mode 100644
index 0000000..9a21763
--- /dev/null
+++ b/admin/src/main/java/com/multictrl/common/constant/BusinessConstant.java
@@ -0,0 +1,80 @@
+package com.multictrl.common.constant;
+
+/**
+ * 业务常量类
+ *
+ * @author Sdy
+ * @since 1.0.0 2026/4/16
+ */
+public interface BusinessConstant {
+
+ //********************************* minio *********************************//
+ String ROUTE_IMG_BUCKET = "route-images";//航线图片桶
+ String ROUTE_KMZ_BUCKET = "route-kmz";//航线桶
+ String DOCK_MEDIA_BUCKET = "dock-media";//机库回传媒体桶
+ String REMOTE_LOG_BUCKET = "remote-log";//机库回传媒体桶
+ String DEVICE_FIRMWARE_BUCKET = "device-firmware";
+
+ //********************************* route action *********************************//
+ String DEFAULT_ACTION_TRIGGER_TYPE = "reachPoint";//默认动作触发器类型 到达航点执行
+ String START_VIDEO_FLAG = "startRecord";//开始录像
+ String STOP_VIDEO_FLAG = "stopRecord";//停止录像
+ String EQUAL_TIME_TAKE_PHOTO_FLAG = "multipleTiming";//等时拍照
+ String EQUAL_DISTANCE_TAKE_PHOTO_FLAG = "multipleDistance";//等距拍照
+ String FINISH_TAKE_PHOTO_FLAG = "takePhotoFinish";//结束间隔拍照
+ String TAKE_PHOTO_FLAG = "takePhoto";//拍照
+
+ //********************************* dj topic *********************************//
+ String REQUESTS = "requests";
+ String STATUS = "status";
+ String OSD = "osd";
+ String STATE = "state";
+ String EVENTS = "events";
+ String SERVICES_REPLY = "services_reply";
+ String STORAGE_CONFIG_GET = "storage_config_get";
+ String _REPLY = "_reply";
+ //********************************* dj payload key *********************************//
+ String METHOD = "method";
+ String GATEWAY = "gateway";
+ String DATA = "data";
+ String NEED_REPLY = "need_reply";
+ //********************************* dj requests topic method *********************************//
+ String CONFIG = "config";
+ String AIRPORT_BIND_STATUS = "airport_bind_status";
+ String AIRPORT_ORGANIZATION_GET = "airport_organization_get";
+ String AIRPORT_ORGANIZATION_BIND = "airport_organization_bind";
+ String FLIGHT_TASK_RESOURCE_GET = "flighttask_resource_get";
+ //********************************* dj events topic method *********************************//
+ String FLIGHTTASK_PROGRESS = "flighttask_progress";
+ String TAKEOFF_TO_POINT_PROGRESS = "takeoff_to_point_progress";
+ String FLY_TO_POINT_PROGRESS = "fly_to_point_progress";
+ String FILE_UPLOAD_CALLBACK = "file_upload_callback";
+ String FILEUPLOAD_PROGRESS = "fileupload_progress";
+ String OTA_PROGRESS = "ota_progress";
+ String HMS = "hms";
+ //********************************* dj status topic method *********************************//
+ String UPDATE_TOPO = "update_topo";
+ //********************************* dj cache key *********************************//
+ String DOCK_VIDEO_ID = "dock_video_id_";
+ String UAV_VIDEO_ID = "uav_video_id_";
+ String UAV_CAMERA_INDEX = "uav_camera_index_";
+ String DOCK_OSD = "dock_osd_";
+ String UAV_OSD = "uav_osd_";
+ String WORKING_TASK_ID = "working_task_id_";
+ String IN_FLIGHT_WORKING_TASK_ID = "in_flight_working_task_id_";
+ String UAV_VIDEO_TYPE = "uav_video_type_";
+ String FLIGHT_TASK_PROGRESS = "flight_task_progress_";
+ String FLIGHT_TASK_PROGRESS_STATUS = "flight_task_progress_status_";
+ String FLIGHT_TASK_PROGRESS_FLIGHT_ID = "flight_task_progress_flight_id_";
+ String DRC_HEART_BEAT_SN = "DRC_HEART_BEAT_SN";
+ String DOCK_IN_WORK = "dock_in_work_";
+ String FLIGHT_TASK_KMZ_FILE_INFO = "flight_task_kmz_file_info_";
+ String UAV_SPEAKER_INDEX = "uav_speaker_index_";
+
+ //********************************* other *********************************//
+ String HTTP_PROTOCOL = "http://";
+ String HTTPS_PROTOCOL = "https://";
+ String TCP_PROTOCOL = "tcp://";
+ String DRC = "drc_";
+ String FILE_PATH = "file/";
+}
diff --git a/admin/src/main/java/com/multictrl/common/constant/DeviceType.java b/admin/src/main/java/com/multictrl/common/constant/DeviceType.java
new file mode 100644
index 0000000..a1a46cc
--- /dev/null
+++ b/admin/src/main/java/com/multictrl/common/constant/DeviceType.java
@@ -0,0 +1,14 @@
+package com.multictrl.common.constant;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@AllArgsConstructor
+@Getter
+public enum DeviceType {
+ DOCK("dock", "机场"),
+ UAV("uav", "飞机");
+
+ private final String code;
+ private final String desc;
+}
diff --git a/admin/src/main/java/com/multictrl/common/constant/DockMode.java b/admin/src/main/java/com/multictrl/common/constant/DockMode.java
new file mode 100644
index 0000000..fe7a9b9
--- /dev/null
+++ b/admin/src/main/java/com/multictrl/common/constant/DockMode.java
@@ -0,0 +1,38 @@
+package com.multictrl.common.constant;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@AllArgsConstructor
+@Getter
+public enum DockMode {
+ IDLE(0, "空闲中"),
+ FIELD_DEBUG(1, "现场调试"),
+ REMOTE_DEBUG(2, "远程调试"),
+ FIRMWARE_UPGRADING(3, "固件升级中"),
+ WORKING(4, "作业中"),
+ PENDING_CALIBRATION(5, "待标定");
+
+ private final Integer code;
+ private final String desc;
+
+ // 根据code获取DockMode
+ public static DockMode getByCode(Integer code) {
+ for (DockMode dockMode : DockMode.values()) {
+ if (dockMode.getCode().equals(code)) {
+ return dockMode;
+ }
+ }
+ return null;
+ }
+
+ //根据code获取desc
+ public static String getDescByCode(Integer code) {
+ for (DockMode dockMode : DockMode.values()) {
+ if (dockMode.getCode().equals(code)) {
+ return dockMode.getDesc();
+ }
+ }
+ return null;
+ }
+}
diff --git a/admin/src/main/java/com/multictrl/common/constant/FlightTaskProgressStatus.java b/admin/src/main/java/com/multictrl/common/constant/FlightTaskProgressStatus.java
new file mode 100644
index 0000000..ee1c86a
--- /dev/null
+++ b/admin/src/main/java/com/multictrl/common/constant/FlightTaskProgressStatus.java
@@ -0,0 +1,21 @@
+package com.multictrl.common.constant;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@AllArgsConstructor
+@Getter
+public enum FlightTaskProgressStatus {
+ CANCELED("canceled", "取消或终止"),
+ FAILED("failed", "失败"),
+ IN_PROGRESS("in_progress", "执行中"),
+ OK("ok", "执行成功"),
+ PARTIALLY_DONE("partially_done", "部分完成"),
+ PAUSED("paused", "暂停"),
+ REJECTED("rejected", "拒绝"),
+ SENT("sent", "已下发"),
+ TIMEOUT("timeout", "超时");
+
+ private final String code;
+ private final String desc;
+}
diff --git a/admin/src/main/java/com/multictrl/common/constant/FlightTaskStatus.java b/admin/src/main/java/com/multictrl/common/constant/FlightTaskStatus.java
new file mode 100644
index 0000000..b05bb50
--- /dev/null
+++ b/admin/src/main/java/com/multictrl/common/constant/FlightTaskStatus.java
@@ -0,0 +1,16 @@
+package com.multictrl.common.constant;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@AllArgsConstructor
+@Getter
+public enum FlightTaskStatus {
+ FAILED(-1, "任务失败"),
+ IN_PROGRESS(0, "任务进行中"),
+ FINISHED(1, "任务完成"),
+ PREVENTED(2, "任务被阻止");
+
+ private final Integer code;
+ private final String desc;
+}
diff --git a/admin/src/main/java/com/multictrl/common/constant/SetActionParams.java b/admin/src/main/java/com/multictrl/common/constant/SetActionParams.java
new file mode 100644
index 0000000..b0bb6c4
--- /dev/null
+++ b/admin/src/main/java/com/multictrl/common/constant/SetActionParams.java
@@ -0,0 +1,212 @@
+package com.multictrl.common.constant;
+
+import com.multictrl.modules.business.dto.WaypointActionDTO;
+import lombok.extern.slf4j.Slf4j;
+import org.dom4j.Element;
+
+/**
+ * 动作类型
+ *
+ * @author Sdy
+ * @since 1.0.0
+ */
+@Slf4j
+public enum SetActionParams {
+ takePhoto {//拍照
+
+ @Override
+ public void setActionParam(WaypointActionDTO actionDTO, Element actionActuatorFuncParam) {
+ actionActuatorFuncParam.addElement("wpml:payloadPositionIndex").setText("0");
+ actionActuatorFuncParam.addElement("wpml:fileSuffix").setText("航点" + actionDTO.getWaypointOrder());
+ actionActuatorFuncParam.addElement("wpml:payloadLensIndex").setText("wide,zoom,ir,visable");
+ actionActuatorFuncParam.addElement("wpml:useGlobalPayloadLensIndex").setText("1");
+ }
+
+ @Override
+ public void getActionParam(WaypointActionDTO actionDTO, Element actionActuatorFuncParam) {
+// String payloadLensIndex = actionActuatorFuncParam.elementText("payloadLensIndex");
+
+ }
+ }, startRecord {//开始录像
+
+ @Override
+ public void setActionParam(WaypointActionDTO actionDTO, Element actionActuatorFuncParam) {
+ takePhoto.setActionParam(actionDTO, actionActuatorFuncParam);
+
+ }
+
+ @Override
+ public void getActionParam(WaypointActionDTO actionDTO, Element actionActuatorFuncParam) {
+
+ }
+ }, stopRecord {//结束录像
+
+ @Override
+ public void setActionParam(WaypointActionDTO actionDTO, Element actionActuatorFuncParam) {
+ actionActuatorFuncParam.addElement("wpml:payloadPositionIndex").setText("0");
+ }
+
+ @Override
+ public void getActionParam(WaypointActionDTO actionDTO, Element actionActuatorFuncParam) {
+
+ }
+ }, zoom {//变焦
+
+ @Override
+ public void setActionParam(WaypointActionDTO actionDTO, Element actionActuatorFuncParam) {
+ actionActuatorFuncParam.addElement("wpml:payloadPositionIndex").setText("0");
+ actionActuatorFuncParam.addElement("wpml:focalLength").setText(
+ Math.round(Double.parseDouble(actionDTO.getActionValue()) * 23.75f) + "");
+ actionActuatorFuncParam.addElement("wpml:isUseFocalFactor").setText("0");
+ }
+
+ @Override
+ public void getActionParam(WaypointActionDTO actionDTO, Element actionActuatorFuncParam) {
+ String focalLength = actionActuatorFuncParam.elementText("focalLength");
+ actionDTO.setActionValue(Math.round(Double.parseDouble(focalLength) / 23.75f) + "");
+ }
+ }, hover {//悬停
+
+ @Override
+ public void setActionParam(WaypointActionDTO actionDTO, Element actionActuatorFuncParam) {
+ actionActuatorFuncParam.addElement("wpml:hoverTime").setText(actionDTO.getActionValue());
+ }
+
+ @Override
+ public void getActionParam(WaypointActionDTO actionDTO, Element actionActuatorFuncParam) {
+ String hoverTime = actionActuatorFuncParam.elementText("hoverTime");
+ actionDTO.setActionValue(hoverTime);
+ }
+ }, rotateYaw {//飞行器偏航角
+
+ @Override
+ public void setActionParam(WaypointActionDTO actionDTO, Element actionActuatorFuncParam) {
+ actionActuatorFuncParam.addElement("wpml:aircraftHeading").setText(actionDTO.getActionValue());
+ actionActuatorFuncParam.addElement("wpml:aircraftPathMode").setText("counterClockwise");
+ }
+
+ @Override
+ public void getActionParam(WaypointActionDTO actionDTO, Element actionActuatorFuncParam) {
+ String aircraftHeading = actionActuatorFuncParam.elementText("aircraftHeading");
+ actionDTO.setActionValue(aircraftHeading);
+ }
+ }, gimbalRotate {//云台俯仰角
+
+ @Override
+ public void setActionParam(WaypointActionDTO actionDTO, Element actionActuatorFuncParam) {
+ setGimbalParam(actionActuatorFuncParam);
+ actionActuatorFuncParam.addElement("wpml:gimbalYawRotateEnable").setText("0");
+ actionActuatorFuncParam.addElement("wpml:gimbalYawRotateAngle").setText("0");
+ actionActuatorFuncParam.addElement("wpml:gimbalPitchRotateEnable").setText("1");
+ actionActuatorFuncParam.addElement("wpml:gimbalPitchRotateAngle").setText(actionDTO.getActionValue());
+ }
+
+ @Override
+ public void getActionParam(WaypointActionDTO actionDTO, Element actionActuatorFuncParam) {
+ String gimbalPitchRotateAngle = actionActuatorFuncParam.elementText("gimbalPitchRotateAngle");
+ actionDTO.setActionValue(gimbalPitchRotateAngle);
+ }
+ }, gimbalRotate_yaw {//云台偏航角
+
+ @Override
+ public void setActionParam(WaypointActionDTO actionDTO, Element actionActuatorFuncParam) {
+ setGimbalParam(actionActuatorFuncParam);
+ actionActuatorFuncParam.addElement("wpml:gimbalPitchRotateEnable").setText("0");
+ actionActuatorFuncParam.addElement("wpml:gimbalPitchRotateAngle").setText("0");
+ actionActuatorFuncParam.addElement("wpml:gimbalYawRotateEnable").setText("1");
+ actionActuatorFuncParam.addElement("wpml:gimbalYawRotateAngle").setText(actionDTO.getActionValue());
+ }
+
+ @Override
+ public void getActionParam(WaypointActionDTO actionDTO, Element actionActuatorFuncParam) {
+ String gimbalYawRotateAngle = actionActuatorFuncParam.elementText("gimbalYawRotateAngle");
+ actionDTO.setActionValue(gimbalYawRotateAngle);
+ }
+ }, panoShot {//全景拍照
+
+ @Override
+ public void setActionParam(WaypointActionDTO actionDTO, Element actionActuatorFuncParam) {
+ takePhoto.setActionParam(actionDTO, actionActuatorFuncParam);
+ actionActuatorFuncParam.addElement("wpml:panoShotSubMode").setText("panoShot_360");
+ }
+
+ @Override
+ public void getActionParam(WaypointActionDTO actionDTO, Element actionActuatorFuncParam) {
+
+ }
+ }, multipleTiming {//等时拍照
+
+ @Override
+ public void setActionParam(WaypointActionDTO actionDTO, Element actionActuatorFuncParam) {
+ takePhoto.setActionParam(actionDTO, actionActuatorFuncParam);
+ }
+
+ @Override
+ public void getActionParam(WaypointActionDTO actionDTO, Element actionActuatorFuncParam) {
+
+ }
+ }, multipleDistance {//等距拍照
+
+ @Override
+ public void setActionParam(WaypointActionDTO actionDTO, Element actionActuatorFuncParam) {
+ takePhoto.setActionParam(actionDTO, actionActuatorFuncParam);
+ }
+
+ @Override
+ public void getActionParam(WaypointActionDTO actionDTO, Element actionActuatorFuncParam) {
+
+ }
+ };
+
+ /**
+ * 设置参数
+ */
+ public abstract void setActionParam(WaypointActionDTO actionDTO, Element actionActuatorFuncParam);
+
+ /**
+ * 获取值
+ */
+ public abstract void getActionParam(WaypointActionDTO actionDTO, Element actionActuatorFuncParam);
+
+ /**
+ * 根据传入的动作类型执行对应的方法
+ *
+ * @param actionDTO 动作
+ * @param actionActuatorFuncParam 参数元素
+ */
+ public static void executeAction(WaypointActionDTO actionDTO, Element actionActuatorFuncParam) {
+ try {
+ SetActionParams.valueOf(actionDTO.getActionType()).setActionParam(actionDTO, actionActuatorFuncParam);
+ } catch (IllegalArgumentException e) {
+ // 处理未知动作类型的异常情况
+ System.err.println("未知的动作类型: " + actionDTO.getActionType());
+ }
+ }
+
+ /**
+ * 根据传入的动作类型执行对应的方法
+ *
+ * @param actionDTO 动作
+ * @param actionActuatorFuncParam 参数元素
+ */
+ public static void getAction(WaypointActionDTO actionDTO, Element actionActuatorFuncParam) {
+ try {
+ SetActionParams.valueOf(actionDTO.getActionType()).getActionParam(actionDTO, actionActuatorFuncParam);
+ } catch (IllegalArgumentException e) {
+ // 处理未知动作类型的异常情况
+ log.error("未知的动作类型: {}", actionDTO.getActionType());
+ }
+ }
+
+ /**
+ * 云台参数
+ */
+ private static void setGimbalParam(Element actionActuatorFuncParam) {
+ actionActuatorFuncParam.addElement("wpml:gimbalRotateMode").setText("absoluteAngle");
+ actionActuatorFuncParam.addElement("wpml:gimbalRollRotateEnable").setText("0");
+ actionActuatorFuncParam.addElement("wpml:gimbalRollRotateAngle").setText("0");
+ actionActuatorFuncParam.addElement("wpml:gimbalRotateTimeEnable").setText("0");
+ actionActuatorFuncParam.addElement("wpml:gimbalRotateTime").setText("0");
+ actionActuatorFuncParam.addElement("wpml:payloadPositionIndex").setText("0");
+ }
+}
diff --git a/admin/src/main/java/com/multictrl/common/exception/RenExceptionHandler.java b/admin/src/main/java/com/multictrl/common/exception/RenExceptionHandler.java
new file mode 100644
index 0000000..9cb9fe8
--- /dev/null
+++ b/admin/src/main/java/com/multictrl/common/exception/RenExceptionHandler.java
@@ -0,0 +1,84 @@
+package com.multictrl.common.exception;
+
+import cn.hutool.core.map.MapUtil;
+import com.multictrl.common.utils.HttpContextUtils;
+import com.multictrl.common.utils.IpUtils;
+import com.multictrl.common.utils.JsonUtils;
+import com.multictrl.common.utils.Result;
+import com.multictrl.modules.log.entity.SysLogErrorEntity;
+import com.multictrl.modules.log.service.SysLogErrorService;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.dao.DuplicateKeyException;
+import org.springframework.http.HttpHeaders;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+import java.util.Map;
+
+/**
+ * 异常处理器
+ *
+ * @author Sdy
+ * @since 1.0.0
+ */
+@Slf4j
+@RestControllerAdvice
+@AllArgsConstructor
+public class RenExceptionHandler {
+ private final SysLogErrorService sysLogErrorService;
+
+ /**
+ * 处理自定义异常
+ */
+ @ExceptionHandler(RenException.class)
+ public Result