Browse Source

1、20250226 异音服务后台逻辑首次备份,基本完成开发需求。

pc147123 7 months ago
commit
a33e239c17
78 changed files with 7227 additions and 0 deletions
  1. 33 0
      .gitignore
  2. 227 0
      pom.xml
  3. 29 0
      src/main/java/com/example/unusualsounds/UnusualSoundsApplication.java
  4. 92 0
      src/main/java/com/example/unusualsounds/common/constant/HttpStatus.java
  5. 21 0
      src/main/java/com/example/unusualsounds/common/exception/UtilException.java
  6. 76 0
      src/main/java/com/example/unusualsounds/common/utils/BaiDuUtils.java
  7. 464 0
      src/main/java/com/example/unusualsounds/common/utils/DateUtils.java
  8. 60 0
      src/main/java/com/example/unusualsounds/common/utils/FastJson2RedisSerializer.java
  9. 68 0
      src/main/java/com/example/unusualsounds/common/utils/FileToMultipartFile.java
  10. 84 0
      src/main/java/com/example/unusualsounds/common/utils/RedisUtils.java
  11. 491 0
      src/main/java/com/example/unusualsounds/common/utils/StringUtilsEx.java
  12. 529 0
      src/main/java/com/example/unusualsounds/common/utils/UUID.java
  13. 30 0
      src/main/java/com/example/unusualsounds/framework/config/CorsConfig.java
  14. 21 0
      src/main/java/com/example/unusualsounds/framework/config/MybatisPlusConfig.java
  15. 26 0
      src/main/java/com/example/unusualsounds/framework/config/SwaggerConfig.java
  16. 30 0
      src/main/java/com/example/unusualsounds/framework/minio/MinioConfig.java
  17. 209 0
      src/main/java/com/example/unusualsounds/framework/minio/MinioUtil.java
  18. 148 0
      src/main/java/com/example/unusualsounds/project/device/controller/SensorsController.java
  19. 250 0
      src/main/java/com/example/unusualsounds/project/device/entity/DevicesSoundSensors.java
  20. 83 0
      src/main/java/com/example/unusualsounds/project/device/entity/vo/RegisterSoundSensors.java
  21. 63 0
      src/main/java/com/example/unusualsounds/project/device/entity/vo/SoundSensorsQueryVo.java
  22. 38 0
      src/main/java/com/example/unusualsounds/project/device/entity/vo/SoundSensorsVo.java
  23. 23 0
      src/main/java/com/example/unusualsounds/project/device/mapper/DevicesSoundSensorsMapper.java
  24. 16 0
      src/main/java/com/example/unusualsounds/project/device/service/DeviceVoiceService.java
  25. 59 0
      src/main/java/com/example/unusualsounds/project/device/service/DevicesSoundSensorsService.java
  26. 29 0
      src/main/java/com/example/unusualsounds/project/device/service/impl/DeviceVoiceServiceImpl.java
  27. 265 0
      src/main/java/com/example/unusualsounds/project/device/service/impl/DevicesSoundSensorsServiceImpl.java
  28. 68 0
      src/main/java/com/example/unusualsounds/project/vox/config/AsyncConfig.java
  29. 48 0
      src/main/java/com/example/unusualsounds/project/vox/config/SegmentRepairService.java
  30. 289 0
      src/main/java/com/example/unusualsounds/project/vox/config/VideoTaskManager.java
  31. 110 0
      src/main/java/com/example/unusualsounds/project/vox/controller/CheckVoxController.java
  32. 52 0
      src/main/java/com/example/unusualsounds/project/vox/controller/OrganizationController.java
  33. 96 0
      src/main/java/com/example/unusualsounds/project/vox/entity/AlarmReports.java
  34. 527 0
      src/main/java/com/example/unusualsounds/project/vox/entity/Alarms.java
  35. 63 0
      src/main/java/com/example/unusualsounds/project/vox/entity/EdgeOrganization.java
  36. 304 0
      src/main/java/com/example/unusualsounds/project/vox/entity/Skills.java
  37. 20 0
      src/main/java/com/example/unusualsounds/project/vox/entity/VideoStatus.java
  38. 45 0
      src/main/java/com/example/unusualsounds/project/vox/entity/VideoTask.java
  39. 81 0
      src/main/java/com/example/unusualsounds/project/vox/entity/VoiceTicketLogs.java
  40. 20 0
      src/main/java/com/example/unusualsounds/project/vox/mapper/AlarmReportsMapper.java
  41. 20 0
      src/main/java/com/example/unusualsounds/project/vox/mapper/AlarmsMapper.java
  42. 21 0
      src/main/java/com/example/unusualsounds/project/vox/mapper/EdgeOrganizationMapper.java
  43. 20 0
      src/main/java/com/example/unusualsounds/project/vox/mapper/SkillsMapper.java
  44. 20 0
      src/main/java/com/example/unusualsounds/project/vox/mapper/VoiceTicketLogsMapper.java
  45. 20 0
      src/main/java/com/example/unusualsounds/project/vox/service/AlarmReportsService.java
  46. 17 0
      src/main/java/com/example/unusualsounds/project/vox/service/AlarmsService.java
  47. 29 0
      src/main/java/com/example/unusualsounds/project/vox/service/EdgeOrganizationService.java
  48. 18 0
      src/main/java/com/example/unusualsounds/project/vox/service/SkillsService.java
  49. 36 0
      src/main/java/com/example/unusualsounds/project/vox/service/VideoService.java
  50. 29 0
      src/main/java/com/example/unusualsounds/project/vox/service/VoiceTicketLogsService.java
  51. 19 0
      src/main/java/com/example/unusualsounds/project/vox/service/VoxService.java
  52. 43 0
      src/main/java/com/example/unusualsounds/project/vox/service/impl/AlarmReportsServiceImpl.java
  53. 73 0
      src/main/java/com/example/unusualsounds/project/vox/service/impl/AlarmsServiceImpl.java
  54. 109 0
      src/main/java/com/example/unusualsounds/project/vox/service/impl/EdgeOrganizationServiceImpl.java
  55. 52 0
      src/main/java/com/example/unusualsounds/project/vox/service/impl/SkillsServiceImpl.java
  56. 133 0
      src/main/java/com/example/unusualsounds/project/vox/service/impl/VideoServiceImpl.java
  57. 71 0
      src/main/java/com/example/unusualsounds/project/vox/service/impl/VoiceTicketLogsServiceImpl.java
  58. 93 0
      src/main/java/com/example/unusualsounds/project/vox/service/impl/VoxServiceImpl.java
  59. 86 0
      src/main/java/com/example/unusualsounds/project/vox/task/TicketsTask.java
  60. 197 0
      src/main/java/com/example/unusualsounds/web/entity/AjaxResult.java
  61. 45 0
      src/main/java/com/example/unusualsounds/web/jsonobj/CameraDTO.java
  62. 10 0
      src/main/java/com/example/unusualsounds/web/jsonobj/Department.java
  63. 17 0
      src/main/java/com/example/unusualsounds/web/jsonobj/Device.java
  64. 10 0
      src/main/java/com/example/unusualsounds/web/jsonobj/Fps.java
  65. 17 0
      src/main/java/com/example/unusualsounds/web/jsonobj/Skill.java
  66. 10 0
      src/main/java/com/example/unusualsounds/web/jsonobj/TimeFrame.java
  67. 53 0
      src/main/resources/application-dev.yml
  68. 51 0
      src/main/resources/application-prod.yml
  69. 102 0
      src/main/resources/application.yml
  70. 94 0
      src/main/resources/logback.xml
  71. 27 0
      src/main/resources/mybatis/mapper/AlarmReportsMapper.xml
  72. 78 0
      src/main/resources/mybatis/mapper/AlarmsMapper.xml
  73. 61 0
      src/main/resources/mybatis/mapper/DevicesSoundSensorsMapper.xml
  74. 22 0
      src/main/resources/mybatis/mapper/EdgeOrganizationMapper.xml
  75. 50 0
      src/main/resources/mybatis/mapper/SkillsMapper.xml
  76. 24 0
      src/main/resources/mybatis/mapper/VoiceTicketLogsMapper.xml
  77. 20 0
      src/main/resources/mybatis/mybatis-config.xml
  78. 243 0
      src/test/java/com/example/unusualsounds/UnusualSoundsApplicationTests.java

+ 33 - 0
.gitignore

@@ -0,0 +1,33 @@
+README.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/

+ 227 - 0
pom.xml

@@ -0,0 +1,227 @@
+<?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>
+    <groupId>com.example</groupId>
+    <artifactId>unusualSounds</artifactId>
+    <version>0.0.1-SNAPSHOT</version>
+    <name>unusualSounds</name>
+    <description>unusualSounds</description>
+
+    <properties>
+        <java.version>17</java.version>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+        <spring-boot.version>3.0.2</spring-boot.version>
+        <knife4j.version>4.4.0</knife4j.version>
+        <mysql.version>8.0.29</mysql.version>
+        <mybatis-plus.version>3.5.3.2</mybatis-plus.version>
+        <druid.version>1.2.16</druid.version>
+        <commons.version>2.6</commons.version>
+        <minio.version>8.5.2</minio.version>
+        <javacv.version>1.5.9</javacv.version>
+        <dynamic.version>3.6.1</dynamic.version>
+        <fastjson.version>2.0.28</fastjson.version>
+        <mybatis-plus-extension>3.3.0</mybatis-plus-extension>
+    </properties>
+    <dependencies>
+
+<!--        <dependency>-->
+<!--            <groupId>com.baomidou</groupId>-->
+<!--            <artifactId>mybatis-plus-extension</artifactId>-->
+<!--            <version>${mybatis-plus-extension}</version>-->
+<!--        </dependency>-->
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-validation</artifactId>
+        </dependency>
+
+        <!-- 集成redis依赖  -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-redis</artifactId>
+        </dependency>
+        <!-- redis 连接池  -->
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-pool2</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.alibaba.fastjson2</groupId>
+            <artifactId>fastjson2</artifactId>
+            <version>${fastjson.version}</version>
+        </dependency>
+
+<!--        百度一见平台sdk-->
+        <dependency>
+            <groupId>com.baidubce</groupId>
+            <artifactId>bce-java-sdk</artifactId>
+            <version>0.10.322</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>ch.qos.logback</groupId>
+                    <artifactId>logback-classic</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>ch.qos.logback</groupId>
+                    <artifactId>logback-core</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.slf4j</groupId>
+                    <artifactId>jcl-over-slf4j</artifactId>
+                </exclusion>
+                <exclusion>
+                    <artifactId>slf4j-reload4j</artifactId>
+                    <groupId>org.slf4j</groupId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+
+<!--        排除jdk17下使用bce-java-sdk 中tools.jar问题-->
+        <dependency>
+            <groupId>org.apache.hadoop </groupId>
+            <artifactId>hadoop-mapreduce-client-core </artifactId>
+            <version>2.5.1 </version>
+            <exclusions>
+                <exclusion>
+                    <groupId>jdk.tools </groupId>
+                    <artifactId>jdk.tools </artifactId>
+                </exclusion>
+                <exclusion>
+                    <artifactId>slf4j-reload4j</artifactId>
+                    <groupId>org.slf4j</groupId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-configuration-processor</artifactId>
+            <optional>true</optional>
+        </dependency>
+
+        <dependency>
+            <groupId>org.bytedeco</groupId>
+            <artifactId>javacv-platform</artifactId>
+            <version>${javacv.version}</version>
+        </dependency>
+
+
+        <dependency>
+            <groupId>io.minio</groupId>
+            <artifactId>minio</artifactId>
+            <version>${minio.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>druid-spring-boot-starter</artifactId>
+            <version>${druid.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-redis</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>commons-io</groupId>
+            <artifactId>commons-io</artifactId>
+            <version>${commons.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
+            <version>${dynamic.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>mysql</groupId>
+            <artifactId>mysql-connector-java</artifactId>
+            <version>${mysql.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-boot-starter</artifactId>
+            <version>${mybatis-plus.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.github.xiaoymin</groupId>
+            <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
+            <version>${knife4j.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-devtools</artifactId>
+            <scope>runtime</scope>
+            <optional>true</optional>
+        </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>
+    </dependencies>
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-dependencies</artifactId>
+                <version>${spring-boot.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <version>3.8.1</version>
+                <configuration>
+                    <source>17</source>
+                    <target>17</target>
+                    <encoding>UTF-8</encoding>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <version>${spring-boot.version}</version>
+                <configuration>
+                    <mainClass>com.example.unusualsounds.UnusualSoundsApplication</mainClass>
+                    <skip>true</skip>
+                </configuration>
+                <executions>
+                    <execution>
+                        <id>repackage</id>
+                        <goals>
+                            <goal>repackage</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>

+ 29 - 0
src/main/java/com/example/unusualsounds/UnusualSoundsApplication.java

@@ -0,0 +1,29 @@
+package com.example.unusualsounds;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.boot.web.client.RestTemplateBuilder;
+import org.springframework.context.annotation.Bean;
+import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.web.client.RestTemplate;
+
+
+@SpringBootApplication(exclude={DataSourceAutoConfiguration.class})
+@EnableScheduling
+public class UnusualSoundsApplication {
+
+    @Autowired
+    //RestTemplateBuilder
+    private RestTemplateBuilder builder;
+    // 使用RestTemplateBuilder来实例化RestTemplate对象,spring默认已经注入了RestTemplateBuilder实例
+    @Bean
+    public RestTemplate restTemplate() {
+        return builder.build();
+    }
+
+    public static void main(String[] args) {
+        SpringApplication.run(UnusualSoundsApplication.class, args);
+    }
+}

+ 92 - 0
src/main/java/com/example/unusualsounds/common/constant/HttpStatus.java

@@ -0,0 +1,92 @@
+package com.example.unusualsounds.common.constant;
+
+/**
+ * 消息状态码集合
+ *
+ */
+public class HttpStatus {
+    /**
+     * 操作成功
+     */
+    public static final int SUCCESS = 200;
+
+    /**
+     * 对象创建成功
+     */
+    public static final int CREATED = 201;
+
+    /**
+     * 请求已经被接受
+     */
+    public static final int ACCEPTED = 202;
+
+    /**
+     * 操作已经执行成功,但是没有返回数据
+     */
+    public static final int NO_CONTENT = 204;
+
+    /**
+     * 资源已被移除
+     */
+    public static final int MOVED_PERM = 301;
+
+    /**
+     * 重定向
+     */
+    public static final int SEE_OTHER = 303;
+
+    /**
+     * 资源没有被修改
+     */
+    public static final int NOT_MODIFIED = 304;
+
+    /**
+     * 参数列表错误(缺少,格式不匹配)
+     */
+    public static final int BAD_REQUEST = 400;
+
+    /**
+     * 未授权
+     */
+    public static final int UNAUTHORIZED = 401;
+
+    /**
+     * 访问受限,授权过期
+     */
+    public static final int FORBIDDEN = 403;
+
+    /**
+     * 资源,服务未找到
+     */
+    public static final int NOT_FOUND = 404;
+
+    /**
+     * 不允许的http方法
+     */
+    public static final int BAD_METHOD = 405;
+
+    /**
+     * 资源冲突,或者资源被锁
+     */
+    public static final int CONFLICT = 409;
+
+    /**
+     * 不支持的数据,媒体类型
+     */
+    public static final int UNSUPPORTED_TYPE = 415;
+
+    /**
+     * 系统内部错误
+     */
+    public static final int ERROR = 500;
+
+    /**
+     * 接口未实现
+     */
+    public static final int NOT_IMPLEMENTED = 501;
+
+    /**
+     * 系统警告消息
+     */
+    public static final int WARN = 601;
+}

+ 21 - 0
src/main/java/com/example/unusualsounds/common/exception/UtilException.java

@@ -0,0 +1,21 @@
+package com.example.unusualsounds.common.exception;
+
+/**
+ * 工具类异常
+ *
+ */
+public class UtilException extends RuntimeException {
+    private static final long serialVersionUID = 8247610319171014183L;
+
+    public UtilException(Throwable e) {
+        super(e.getMessage(), e);
+    }
+
+    public UtilException(String message) {
+        super(message);
+    }
+
+    public UtilException(String message, Throwable throwable) {
+        super(message, throwable);
+    }
+}

+ 76 - 0
src/main/java/com/example/unusualsounds/common/utils/BaiDuUtils.java

@@ -0,0 +1,76 @@
+package com.example.unusualsounds.common.utils;
+
+import static com.baidubce.auth.SignOptions.DEFAULT;
+import com.baidubce.auth.BceCredentials;
+import com.baidubce.auth.BceV1Signer;
+import com.baidubce.auth.DefaultBceCredentials;
+import com.baidubce.http.HttpMethodName;
+import com.baidubce.internal.InternalRequest;
+import com.baidubce.util.JsonUtils;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.client.RestTemplate;
+
+import java.net.URI;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+public final class  BaiDuUtils {
+    /**
+     * 发送请求
+     *
+     * @param uriStr         uri路径
+     * @param httpMethodName 请求方式
+     * @param requestBody    请求体
+     * @param requestParams  请求参数
+     * @param t              响应体类型
+     * @param <T>            响应
+     * @return
+     */
+    public static <T> ResponseEntity<T> sendRequest(String uriStr, HttpMethodName httpMethodName, Object requestBody,
+                                                    Map<String, String> requestParams, Class<T> t, String ak, String sk,String endpoint, String uriPathPrefix) {
+        String signUrl = String.format("%s%s", endpoint, uriStr);
+        URI uri = URI.create(signUrl);
+        InternalRequest request = new InternalRequest(httpMethodName, uri);
+
+        // 构建请求 url
+        String url = String.format("%s%s%s", endpoint, uriPathPrefix, uriStr);
+        // 构建查询参数
+        if (requestParams != null && !requestParams.isEmpty()) {
+            url = url.concat("?");
+            for (Map.Entry<String, String> entry : requestParams.entrySet()) {
+                request.addParameter(entry.getKey(), entry.getValue());
+                url = url.concat(entry.getKey()).concat("=").concat(entry.getValue()).concat("&");
+            }
+            url = url.substring(0, url.length() - 1);
+        }
+
+        // 构建签名
+        BceCredentials credentials = new DefaultBceCredentials(ak, sk);
+        BceV1Signer bce = new BceV1Signer();
+        Set<String> setString = new HashSet<>();
+        setString.add("Host");
+        DEFAULT.setHeadersToSign(setString);
+        bce.sign(request, credentials, DEFAULT);
+
+        RestTemplate restTemplate = new RestTemplate();
+        HttpHeaders headers = new HttpHeaders();
+        headers.add("Authorization", request.getHeaders().get("Authorization"));
+        headers.add("Host", request.getHeaders().get("Host"));
+
+        HttpEntity<?> requestEntity;
+        if (requestBody != null) {
+            String requestJson = JsonUtils.toJsonString(requestBody);
+            requestEntity = new HttpEntity<>(requestJson, headers);
+        } else {
+            requestEntity = new HttpEntity<>(headers);
+        }
+        //使用headers方法设置GET请求的头信息, 返回
+        return restTemplate.exchange(url, HttpMethod.valueOf(httpMethodName.name()), requestEntity, t);
+    }
+
+
+}

+ 464 - 0
src/main/java/com/example/unusualsounds/common/utils/DateUtils.java

@@ -0,0 +1,464 @@
+package com.example.unusualsounds.common.utils;
+
+import org.apache.commons.lang3.time.DateFormatUtils;
+
+import java.lang.management.ManagementFactory;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.time.*;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.TemporalAdjusters;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 时间工具类
+ *
+ * @author alibct
+ */
+public class DateUtils extends org.apache.commons.lang3.time.DateUtils {
+    public static String YYYY = "yyyy";
+
+    public static String YYYY_MM = "yyyy-MM";
+
+    public static String YYYY_MM_DD = "yyyy-MM-dd";
+
+    public static String YYYYMMDDHHMMSS = "yyyyMMddHHmmss";
+
+    public static String YYYY_MM_DD_HH_MM_SS = "yyyy-MM-dd HH:mm:ss";
+
+    private static String[] parsePatterns = {
+            "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy-MM",
+            "yyyy/MM/dd", "yyyy/MM/dd HH:mm:ss", "yyyy/MM/dd HH:mm", "yyyy/MM",
+            "yyyy.MM.dd", "yyyy.MM.dd HH:mm:ss", "yyyy.MM.dd HH:mm", "yyyy.MM"};
+
+    /**
+     * 获取当前Date型日期
+     *
+     * @return Date() 当前日期
+     */
+    public static Date getNowDate() {
+        return new Date();
+    }
+
+    /**
+     * 获取当前日期, 默认格式为yyyy-MM-dd
+     *
+     * @return String
+     */
+    public static String getDate() {
+        return dateTimeNow(YYYY_MM_DD);
+    }
+
+    public static final String getTime() {
+        return dateTimeNow(YYYY_MM_DD_HH_MM_SS);
+    }
+
+    public static final String dateTimeNow() {
+        return dateTimeNow(YYYYMMDDHHMMSS);
+    }
+
+    public static final String dateTimeNow(final String format) {
+        return parseDateToStr(format, new Date());
+    }
+
+    public static final String dateTime(final Date date) {
+        return parseDateToStr(YYYY_MM_DD, date);
+    }
+
+    public static final String parseDateToStr(final String format, final Date date) {
+        return new SimpleDateFormat(format).format(date);
+    }
+
+    public static final Date dateTime(final String format, final String ts) {
+        try {
+            return new SimpleDateFormat(format).parse(ts);
+        } catch (ParseException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * 日期路径 即年/月/日 如2018/08/08
+     */
+    public static final String datePath() {
+        Date now = new Date();
+        return DateFormatUtils.format(now, "yyyy/MM/dd");
+    }
+
+    /**
+     * 日期路径 即年/月/日 如20180808
+     */
+    public static final String dateTime() {
+        Date now = new Date();
+        return DateFormatUtils.format(now, "yyyyMMdd");
+    }
+
+    /**
+     * 判断一个localdatetime是否是当月最后一天的23:59
+     */
+    public static boolean endMonthDateTime( LocalDateTime dateTime) {
+        // 获取该日期所在月的最后一天
+        LocalDateTime lastDayOfMonth = dateTime.with(TemporalAdjusters.lastDayOfMonth());
+        // 设置时间为23:59:00
+        LocalDateTime endOfDay = LocalDateTime.of(lastDayOfMonth.getYear(), lastDayOfMonth.getMonth(), lastDayOfMonth.getDayOfMonth(), 23, 59, 0);
+        // 判断给定的LocalDateTime是否是月末最后一天的23:59:00
+        return dateTime.equals(endOfDay);
+    }
+
+
+    /**
+     * 传入开始开始时间,返回到当前年的年份list
+     * @return
+     */
+    public static  List<Integer> generateYearsUntilNow(int startYear) {
+        LocalDate now = LocalDate.now();
+        int currentYear = now.getYear();
+
+        List<Integer> yearsList = new ArrayList<>();
+        for (int year = startYear; year <= currentYear; year++) {
+            yearsList.add(year);
+        }
+
+        return yearsList;
+    }
+
+    /**
+     * 日期型字符串转化为日期 格式
+     */
+    public static Date parseDate(Object str) {
+        if (str == null) {
+            return null;
+        }
+        try {
+            return parseDate(str.toString(), parsePatterns);
+        } catch (ParseException e) {
+            return null;
+        }
+    }
+
+    /**
+     * 获取服务器启动时间
+     */
+    public static Date getServerStartDate() {
+        long time = ManagementFactory.getRuntimeMXBean().getStartTime();
+        return new Date(time);
+    }
+
+    /**
+     * 计算相差天数
+     */
+    public static int differentDaysByMillisecond(Date date1, Date date2) {
+        return Math.abs((int) ((date2.getTime() - date1.getTime()) / (1000 * 3600 * 24)));
+    }
+
+    /**
+     * 计算时间差
+     *
+     * @param endDate   最后时间
+     * @param startTime 开始时间
+     * @return 时间差(天/小时/分钟)
+     */
+    public static String timeDistance(Date endDate, Date startTime) {
+        long nd = 1000 * 24 * 60 * 60;
+        long nh = 1000 * 60 * 60;
+        long nm = 1000 * 60;
+        // long ns = 1000;
+        // 获得两个时间的毫秒时间差异
+        long diff = endDate.getTime() - startTime.getTime();
+        // 计算差多少天
+        long day = diff / nd;
+        // 计算差多少小时
+        long hour = diff % nd / nh;
+        // 计算差多少分钟
+        long min = diff % nd % nh / nm;
+        // 计算差多少秒//输出结果
+        // long sec = diff % nd % nh % nm / ns;
+        return day + "天" + hour + "小时" + min + "分钟";
+    }
+
+    /**
+     * 增加 LocalDateTime ==> Date
+     */
+    public static Date toDate(LocalDateTime temporalAccessor) {
+        ZonedDateTime zdt = temporalAccessor.atZone(ZoneId.systemDefault());
+        return Date.from(zdt.toInstant());
+    }
+
+    /**
+     * 增加 LocalDate ==> Date
+     */
+    public static Date toDate(LocalDate temporalAccessor) {
+        LocalDateTime localDateTime = LocalDateTime.of(temporalAccessor, LocalTime.of(0, 0, 0));
+        ZonedDateTime zdt = localDateTime.atZone(ZoneId.systemDefault());
+        return Date.from(zdt.toInstant());
+    }
+
+    /**
+     * 增加 Date ==> LocalDateTime
+     */
+    public static LocalDateTime datatToLocalTimeDate(Date date) {
+        ZoneId zoneId = ZoneId.systemDefault();
+        LocalDateTime localDateTime = date.toInstant().atZone(zoneId).toLocalDateTime();
+        return localDateTime;
+    }
+
+    /**
+     * 增加 Date ==> LocalDate
+     */
+    public static LocalDate dataToLocalDate(Date date) {
+        ZoneId zoneId = ZoneId.systemDefault();
+        return date.toInstant().atZone(zoneId).toLocalDate();
+    }
+
+    /**
+     * localTime日期向前减去seconds秒
+     * @param localDateTime
+     * @param seconds
+     * @return
+     */
+    public static  LocalDateTime minusSecond(LocalDateTime localDateTime,Long seconds) {
+        return localDateTime.minusSeconds(seconds);
+    }
+
+    /**
+     * localTime日期向前减去day天
+     * @param localDate
+     * @param day
+     * @return
+     */
+    public static  LocalDate minusDay(LocalDate localDate,Long day) {
+        return localDate.minusDays(day);
+    }
+
+
+
+    /**
+     * 当前时间往后减一天   接受时间类型:yyyy-mm-dd的字符串
+     *
+     * @return
+     */
+    public static String minusOneDay(String time) throws ParseException {
+        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-mm-dd");
+        //将接收的time中的年月日截取成String数组
+        String[] timeStr = time.split("-");
+        Calendar calendar = Calendar.getInstance();
+        int year = Integer.valueOf(timeStr[0]);
+        int month = Integer.valueOf(timeStr[1]);
+        int day = Integer.valueOf(timeStr[2]);
+        //判断time中的日期是否是该年该月的一号,如果不是就往前减一天;如果是就看情况减月份和年份
+        if (day <= 1) {
+            String date = null;
+            //如果月份不是1月,就对月份减一;如果月份是1月,就对年份减一;
+            if (month > 1) {
+                month--;
+                Calendar c = Calendar.getInstance();
+                c.set(year, month, 0);
+                Date parse = dateFormat.parse(year + "-" + month + "-" + c.get(Calendar.DAY_OF_MONTH));
+                date = dateFormat.format(parse);
+            } else if (month == 1) {
+                year--;
+                date = year + "-12-31";
+            }
+            return date;
+        }
+        //time中的日期不是该年该月的一号,直接往前减一天
+        Date date = dateFormat.parse(time);
+        calendar.setTime(date);
+        calendar.add(Calendar.DATE, -1);
+        return dateFormat.format(calendar.getTime());
+    }
+
+    /**
+     * 传入两个时间区间,返回每天的日期
+     * @param startDateStr
+     * @param endDateStr
+     * @return
+     */
+    public static List<String> dateZone(String startDateStr,String endDateStr){
+
+        List<String> dateList = new ArrayList<>();
+        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+        DateTimeFormatter formatter1 = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+
+        LocalDateTime startDate = LocalDateTime.parse(startDateStr, formatter);
+        LocalDateTime endDate = LocalDateTime.parse(endDateStr, formatter);
+
+        while (!startDate.isAfter(endDate)) {
+            dateList.add(startDate.format(formatter1));
+            startDate = startDate.plusDays(1);
+        }
+        return dateList;
+    }
+
+    /**
+     * 当前时间往前加一天  接受时间类型:yyyy-mm-dd的字符串
+     *
+     * @return
+     */
+    public static String addOneDay(String time) throws ParseException {
+        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-mm-dd");
+        //将接收的time中的年月日截取成String数组
+        String[] timeStr = time.split("-");
+        //确定time中的那一年的那个月份有多少天
+        Calendar calendar = Calendar.getInstance();
+        int year = Integer.valueOf(timeStr[0]);
+        int month = Integer.valueOf(timeStr[1]);
+        int day = Integer.valueOf(timeStr[2]);
+        calendar.set(year, month, 0);
+        int dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH);
+        //判断time中的日期是否超过了该年该月的最后一天,如果超过就进行以下处理
+        if (day >= dayOfMonth) {
+            String date = null;
+            //判断月份是否是12月,不是就往后加一个月;是的话就把年份加一年
+            if (month < 12) {
+                month++;
+                Date parse = dateFormat.parse(year + "-" + month + "-01");
+                date = dateFormat.format(parse);
+            } else if (month == 12) {
+                year++;
+                date = year + "-01-01";
+            }
+            return date;
+        }
+        //time中的日期没有超过该年该月的最后一天,则天数往后加一天
+        calendar.setTime(dateFormat.parse(time));
+        calendar.add(Calendar.DATE, 1);
+        return dateFormat.format(calendar.getTime());
+    }
+
+    /**
+     * @param date 日期
+     * @return 获取指定日期的最后时分秒
+     */
+    public static Date getLastDate(Date date) {
+        Calendar calendar = Calendar.getInstance();
+        calendar.setTime(date);
+        // 将时分秒,毫秒域清零
+        calendar.set(Calendar.HOUR_OF_DAY, 23);
+        calendar.set(Calendar.MINUTE, 59);
+        calendar.set(Calendar.SECOND, 59);
+        return calendar.getTime();
+    }
+
+    /**
+     * 获取时间(年月)
+     *
+     * @return
+     */
+    public static final String getSystemDateYM() {
+        Calendar cal = Calendar.getInstance();
+        SimpleDateFormat formatter = new SimpleDateFormat("yyyyMM");
+        String mDateTime = formatter.format(cal.getTime());
+        return mDateTime;
+    }
+    /**
+     * 获取时间(日)
+     *
+     * @return
+     */
+    public static String getSystemDateDay() {
+        Calendar cal = Calendar.getInstance();
+        SimpleDateFormat formatter = new SimpleDateFormat("dd");
+        String mDateTime = formatter.format(cal.getTime());
+        return mDateTime;
+    }
+    /**
+     * 获取时间(小时)
+     *
+     * @return
+     */
+    public static final String getSystemDateHour() {
+        Calendar cal = Calendar.getInstance();
+        SimpleDateFormat formatter = new SimpleDateFormat("HH");
+        String mDateTime = formatter.format(cal.getTime());
+        return mDateTime;
+    }
+    /**
+     * 获取时间(分钟)
+     *
+     * @return
+     */
+    public static final String getSystemDateMin() {
+        Calendar cal = Calendar.getInstance();
+        SimpleDateFormat formatter = new SimpleDateFormat("mm");
+        String mDateTime = formatter.format(cal.getTime());
+        mDateTime = mDateTime.substring(0, 1);
+        int end = Integer.parseInt(mDateTime);
+        mDateTime = mDateTime + "0-" + end + "9";
+        return mDateTime;
+    }
+
+    /**
+     * 获取日期所在年份
+     *
+     * @param date
+     * @return
+     */
+    public static int getYear(Date date) {
+        LocalDate localDate = date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
+        return localDate.getYear();
+    }
+
+    /**
+     * 获取日期所在季度
+     *
+     * @param date
+     * @return
+     */
+    public static int getQuarter(Date date) {
+        LocalDate localDate = date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
+        int month = localDate.getMonthValue();
+        if (month <= 3) {
+            return 1;
+        } else if (month <= 6) {
+            return 2;
+        } else if (month <= 9) {
+            return 3;
+        } else {
+            return 4;
+        }
+    }
+
+    /**
+     * 获取两个日期之间的 年份_季度 如传 2022-05-01 10:00:00 2023-05-01 10:00:00 返回
+     * ["2022_2","2022_3","2022_4","2023_1","2023_2"]
+     *
+     * @param startDate
+     * @param endDate
+     * @return
+     */
+    public static List<String> getYearQuarterList(LocalDate startDate, LocalDate endDate) {
+        List<String> yearQuarterList = new ArrayList<>();
+
+        LocalDate currentDate = startDate;
+        while (!currentDate.isAfter(endDate)) {
+            int year = currentDate.getYear();
+            int quarter = (currentDate.getMonthValue() - 1) / 3 + 1;
+            String yearQuarter = year + "_" + quarter;
+            yearQuarterList.add(yearQuarter);
+
+            currentDate = currentDate.plusMonths(3);
+        }
+
+        return yearQuarterList;
+    }
+
+    /**
+     * @param date 日期
+     * @return 获取指定日期的最后时分秒
+     */
+    public static Date getStartDate(Date date) {
+        Calendar calendar = Calendar.getInstance();
+        calendar.setTime(date);
+        // 将时分秒,毫秒域清零
+        calendar.set(Calendar.HOUR_OF_DAY, 0);
+        calendar.set(Calendar.MINUTE, 0);
+        calendar.set(Calendar.SECOND, 0);
+        return calendar.getTime();
+    }
+
+
+}

+ 60 - 0
src/main/java/com/example/unusualsounds/common/utils/FastJson2RedisSerializer.java

@@ -0,0 +1,60 @@
+package com.example.unusualsounds.common.utils;
+
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONReader;
+import com.alibaba.fastjson2.filter.Filter;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.ArrayUtils;
+import org.springframework.data.redis.serializer.RedisSerializer;
+import org.springframework.data.redis.serializer.SerializationException;
+
+import java.util.Objects;
+
+/**
+ * 自定义fastjson2序列化器
+ * @param <T>
+ */
+@Slf4j
+public class FastJson2RedisSerializer<T> implements RedisSerializer<T> {
+
+    // 自动类型过滤器
+    static final Filter AUTO_TYPE_FILTER = JSONReader.autoTypeFilter(
+            "***.***.***"
+    );
+
+    private final Class<T> clazz;
+
+    public FastJson2RedisSerializer(Class<T> clazz) {
+        super();
+        this.clazz = clazz;
+    }
+
+    @Override
+    public byte[] serialize(T t) throws SerializationException {
+        if (Objects.isNull(t)) {
+            return new byte[0];
+        }
+        try {
+            return JSON.toJSONBytes(t);
+        } catch (Exception e) {
+            log.error("Fastjson2 序列化错误:{}", e.getMessage());
+            throw new SerializationException("无法序列化: " + e.getMessage(), e);
+        }
+
+    }
+
+    @Override
+    public T deserialize(byte[] bytes) throws SerializationException {
+        if (ArrayUtils.isEmpty(bytes)) {
+            return null;
+        }
+        try {
+            return JSON.parseObject(bytes, clazz, AUTO_TYPE_FILTER);
+        } catch (Exception e) {
+            log.error("Fastjson2 反序列化错误:{}", e.getMessage());
+            throw new SerializationException("无法反序列化: " + e.getMessage(), e);
+        }
+
+    }
+}
+

+ 68 - 0
src/main/java/com/example/unusualsounds/common/utils/FileToMultipartFile.java

@@ -0,0 +1,68 @@
+package com.example.unusualsounds.common.utils;
+
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.*;
+
+public class FileToMultipartFile implements MultipartFile {
+
+    private final File file;
+
+    public FileToMultipartFile(File file) {
+        this.file = file;
+    }
+
+    @Override
+    public String getName() {
+        return file.getName();
+    }
+
+    @Override
+    public String getOriginalFilename() {
+        return file.getName();
+    }
+
+    @Override
+    public String getContentType() {
+        return "application/octet-stream";
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return file.length() == 0;
+    }
+
+    @Override
+    public long getSize() {
+        return file.length();
+    }
+
+    @Override
+    public byte[] getBytes() throws IOException {
+        InputStream is = new FileInputStream(file);
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        byte[] buffer = new byte[1024];
+        int len;
+        while ((len = is.read(buffer)) != -1) {
+            baos.write(buffer, 0, len);
+        }
+        return baos.toByteArray();
+    }
+
+    @Override
+    public InputStream getInputStream() throws IOException {
+        return new FileInputStream(file);
+    }
+
+    @Override
+    public void transferTo(File dest) throws IOException, IllegalStateException {
+        try (InputStream is = new FileInputStream(file);
+             OutputStream os = new FileOutputStream(dest)) {
+            byte[] buffer = new byte[1024];
+            int len;
+            while ((len = is.read(buffer)) != -1) {
+                os.write(buffer, 0, len);
+            }
+        }
+    }
+}

+ 84 - 0
src/main/java/com/example/unusualsounds/common/utils/RedisUtils.java

@@ -0,0 +1,84 @@
+package com.example.unusualsounds.common.utils;
+
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+import org.springframework.stereotype.Component;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * redis工具类
+ * 使用已添加的fastjson2来实现对象的序列化和反序列化操作
+ *
+ */
+@Component
+@RequiredArgsConstructor
+public class RedisUtils {
+    private static RedisTemplate<String, Object> redisTemplate;
+
+    public static RedisTemplate<String, Object> getRedisTemplate() {
+        return redisTemplate;
+    }
+
+    @Autowired
+    private RedisUtils(RedisConnectionFactory redisConnectionFactory) {
+        redisTemplate = new RedisTemplate<>();
+        FastJson2RedisSerializer<Object> fastJson2RedisSerializer = new FastJson2RedisSerializer<>(Object.class);
+        StringRedisSerializer serializer = new StringRedisSerializer();
+        redisTemplate.setConnectionFactory(redisConnectionFactory);
+        redisTemplate.setKeySerializer(serializer);
+        redisTemplate.setHashKeySerializer(serializer);
+        redisTemplate.setValueSerializer(fastJson2RedisSerializer);
+        redisTemplate.setHashValueSerializer(fastJson2RedisSerializer);
+        redisTemplate.afterPropertiesSet();
+    }
+
+
+    public static boolean set(String key, Object value,long timeout, TimeUnit timeUnit) {
+        try {
+            redisTemplate.opsForValue().set(key, value,timeout,timeUnit);
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    public static boolean setForeverKey(String key, Object value) {
+        try {
+            redisTemplate.opsForValue().set(key, value);
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    public static Object get(String key) {
+        return key == null ? null : redisTemplate.opsForValue().get(key);
+    }
+
+    public static boolean delete(String key) {
+        return  key ==null ? false :redisTemplate.delete(key);
+    }
+
+    public static boolean expire(String key, long timeout, TimeUnit timeUnit) {
+
+        try {
+            redisTemplate.expire("key", timeout, timeUnit);
+            return true;
+        }catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    public static boolean exists(String key) {
+        return key == null ? false : redisTemplate.hasKey(key);
+    }
+
+}

+ 491 - 0
src/main/java/com/example/unusualsounds/common/utils/StringUtilsEx.java

@@ -0,0 +1,491 @@
+package com.example.unusualsounds.common.utils;
+
+
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.util.AntPathMatcher;
+
+import java.util.*;
+
+/**
+ * 字符串工具
+ */
+public class StringUtilsEx extends StringUtils {
+    /**
+     * 空字符串
+     */
+    private static final String NULLSTR = "";
+
+    /**
+     * 下划线
+     */
+    private static final char SEPARATOR = '_';
+
+    /**
+     * 获取参数不为空值
+     *
+     * @param value defaultValue 要判断的value
+     * @return value 返回值
+     */
+    public static <T> T nvl(T value, T defaultValue) {
+        return value != null ? value : defaultValue;
+    }
+
+    /**
+     * * 判断一个Collection是否为空, 包含List,Set,Queue
+     *
+     * @param coll 要判断的Collection
+     * @return true:为空 false:非空
+     */
+    public static boolean isEmpty(Collection<?> coll) {
+        return isNull(coll) || coll.isEmpty();
+    }
+
+    /**
+     * * 判断一个Collection是否非空,包含List,Set,Queue
+     *
+     * @param coll 要判断的Collection
+     * @return true:非空 false:空
+     */
+    public static boolean isNotEmpty(Collection<?> coll) {
+        return !isEmpty(coll);
+    }
+
+    /**
+     * * 判断一个对象数组是否为空
+     *
+     * @param objects 要判断的对象数组
+     *                * @return true:为空 false:非空
+     */
+    public static boolean isEmpty(Object[] objects) {
+        return isNull(objects) || (objects.length == 0);
+    }
+
+    /**
+     * * 判断一个对象数组是否非空
+     *
+     * @param objects 要判断的对象数组
+     * @return true:非空 false:空
+     */
+    public static boolean isNotEmpty(Object[] objects) {
+        return !isEmpty(objects);
+    }
+
+    /**
+     * * 判断一个Map是否为空
+     *
+     * @param map 要判断的Map
+     * @return true:为空 false:非空
+     */
+    public static boolean isEmpty(Map<?, ?> map) {
+        return isNull(map) || map.isEmpty();
+    }
+
+    /**
+     * * 判断一个Map是否为空
+     *
+     * @param map 要判断的Map
+     * @return true:非空 false:空
+     */
+    public static boolean isNotEmpty(Map<?, ?> map) {
+        return !isEmpty(map);
+    }
+
+    /**
+     * * 判断一个字符串是否为空串
+     *
+     * @param str String
+     * @return true:为空 false:非空
+     */
+    public static boolean isEmpty(String str) {
+        return isNull(str) || NULLSTR.equals(str.trim());
+    }
+
+    /**
+     * * 判断一个字符串是否为非空串
+     *
+     * @param str String
+     * @return true:非空串 false:空串
+     */
+    public static boolean isNotEmpty(String str) {
+        return !isEmpty(str);
+    }
+
+    /**
+     * * 判断一个对象是否为空
+     *
+     * @param object Object
+     * @return true:为空 false:非空
+     */
+    public static boolean isNull(Object object) {
+        return object == null;
+    }
+
+    /**
+     * * 判断一个对象是否非空
+     *
+     * @param object Object
+     * @return true:非空 false:空
+     */
+    public static boolean isNotNull(Object object) {
+        return !isNull(object);
+    }
+
+    /**
+     * * 判断一个对象是否是数组类型(Java基本型别的数组)
+     *
+     * @param object 对象
+     * @return true:是数组 false:不是数组
+     */
+    public static boolean isArray(Object object) {
+        return isNotNull(object) && object.getClass().isArray();
+    }
+
+    /**
+     * 去空格
+     */
+    public static String trim(String str) {
+        return (str == null ? "" : str.trim());
+    }
+
+    /**
+     * 截取字符串
+     *
+     * @param str   字符串
+     * @param start 开始
+     * @return 结果
+     */
+    public static String substring(final String str, int start) {
+        if (str == null) {
+            return NULLSTR;
+        }
+
+        if (start < 0) {
+            start = str.length() + start;
+        }
+
+        if (start < 0) {
+            start = 0;
+        }
+        if (start > str.length()) {
+            return NULLSTR;
+        }
+
+        return str.substring(start);
+    }
+
+    /**
+     * 截取字符串
+     *
+     * @param str   字符串
+     * @param start 开始
+     * @param end   结束
+     * @return 结果
+     */
+    public static String substring(final String str, int start, int end) {
+        if (str == null) {
+            return NULLSTR;
+        }
+
+        if (end < 0) {
+            end = str.length() + end;
+        }
+        if (start < 0) {
+            start = str.length() + start;
+        }
+
+        if (end > str.length()) {
+            end = str.length();
+        }
+
+        if (start > end) {
+            return NULLSTR;
+        }
+
+        if (start < 0) {
+            start = 0;
+        }
+        if (end < 0) {
+            end = 0;
+        }
+
+        return str.substring(start, end);
+    }
+
+
+    /**
+     * 字符串转set
+     *
+     * @param str 字符串
+     * @param sep 分隔符
+     * @return set集合
+     */
+    public static final Set<String> str2Set(String str, String sep) {
+        return new HashSet<String>(str2List(str, sep, true, false));
+    }
+
+    /**
+     * 字符串转list
+     *
+     * @param str         字符串
+     * @param sep         分隔符
+     * @param filterBlank 过滤纯空白
+     * @param trim        去掉首尾空白
+     * @return list集合
+     */
+    public static final List<String> str2List(String str, String sep, boolean filterBlank, boolean trim) {
+        List<String> list = new ArrayList<String>();
+        if (StringUtils.isEmpty(str)) {
+            return list;
+        }
+
+        // 过滤空白字符串
+        if (filterBlank && StringUtils.isBlank(str)) {
+            return list;
+        }
+        String[] split = str.split(sep);
+        for (String string : split) {
+            if (filterBlank && StringUtils.isBlank(string)) {
+                continue;
+            }
+            if (trim) {
+                string = string.trim();
+            }
+            list.add(string);
+        }
+
+        return list;
+    }
+
+    /**
+     * 判断给定的set列表中是否包含数组array 判断给定的数组array中是否包含给定的元素value
+     *
+     * @param collection 给定的集合
+     * @param array      给定的数组
+     * @return boolean 结果
+     */
+    public static boolean containsAny(Collection<String> collection, String... array) {
+        if (isEmpty(collection) || isEmpty(array)) {
+            return false;
+        } else {
+            for (String str : array) {
+                if (collection.contains(str)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+    }
+
+    /**
+     * 查找指定字符串是否包含指定字符串列表中的任意一个字符串同时串忽略大小写
+     *
+     * @param cs                  指定字符串
+     * @param searchCharSequences 需要检查的字符串数组
+     * @return 是否包含任意一个字符串
+     */
+    public static boolean containsAnyIgnoreCase(CharSequence cs, CharSequence... searchCharSequences) {
+        if (isEmpty(cs) || isEmpty(searchCharSequences)) {
+            return false;
+        }
+        for (CharSequence testStr : searchCharSequences) {
+            if (containsIgnoreCase(cs, testStr)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 驼峰转下划线命名
+     */
+    public static String toUnderScoreCase(String str) {
+        if (str == null) {
+            return null;
+        }
+        StringBuilder sb = new StringBuilder();
+        // 前置字符是否大写
+        boolean preCharIsUpperCase = true;
+        // 当前字符是否大写
+        boolean curreCharIsUpperCase = true;
+        // 下一字符是否大写
+        boolean nexteCharIsUpperCase = true;
+        for (int i = 0; i < str.length(); i++) {
+            char c = str.charAt(i);
+            if (i > 0) {
+                preCharIsUpperCase = Character.isUpperCase(str.charAt(i - 1));
+            } else {
+                preCharIsUpperCase = false;
+            }
+
+            curreCharIsUpperCase = Character.isUpperCase(c);
+
+            if (i < (str.length() - 1)) {
+                nexteCharIsUpperCase = Character.isUpperCase(str.charAt(i + 1));
+            }
+
+            if (preCharIsUpperCase && curreCharIsUpperCase && !nexteCharIsUpperCase) {
+                sb.append(SEPARATOR);
+            } else if ((i != 0 && !preCharIsUpperCase) && curreCharIsUpperCase) {
+                sb.append(SEPARATOR);
+            }
+            sb.append(Character.toLowerCase(c));
+        }
+
+        return sb.toString();
+    }
+
+    /**
+     * 是否包含字符串
+     *
+     * @param str  验证字符串
+     * @param strs 字符串组
+     * @return 包含返回true
+     */
+    public static boolean inStringIgnoreCase(String str, String... strs) {
+        if (str != null && strs != null) {
+            for (String s : strs) {
+                if (str.equalsIgnoreCase(trim(s))) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 将下划线大写方式命名的字符串转换为驼峰式。如果转换前的下划线大写方式命名的字符串为空,则返回空字符串。 例如:HELLO_WORLD->HelloWorld
+     *
+     * @param name 转换前的下划线大写方式命名的字符串
+     * @return 转换后的驼峰式命名的字符串
+     */
+    public static String convertToCamelCase(String name) {
+        StringBuilder result = new StringBuilder();
+        // 快速检查
+        if (name == null || name.isEmpty()) {
+            // 没必要转换
+            return "";
+        } else if (!name.contains("_")) {
+            // 不含下划线,仅将首字母大写
+            return name.substring(0, 1).toUpperCase() + name.substring(1);
+        }
+        // 用下划线将原始字符串分割
+        String[] camels = name.split("_");
+        for (String camel : camels) {
+            // 跳过原始字符串中开头、结尾的下换线或双重下划线
+            if (camel.isEmpty()) {
+                continue;
+            }
+            // 首字母大写
+            result.append(camel.substring(0, 1).toUpperCase());
+            result.append(camel.substring(1).toLowerCase());
+        }
+        return result.toString();
+    }
+
+    /**
+     * 驼峰式命名法
+     * 例如:user_name->userName
+     */
+    public static String toCamelCase(String s) {
+        if (s == null) {
+            return null;
+        }
+        if (s.indexOf(SEPARATOR) == -1) {
+            return s;
+        }
+        s = s.toLowerCase();
+        StringBuilder sb = new StringBuilder(s.length());
+        boolean upperCase = false;
+        for (int i = 0; i < s.length(); i++) {
+            char c = s.charAt(i);
+
+            if (c == SEPARATOR) {
+                upperCase = true;
+            } else if (upperCase) {
+                sb.append(Character.toUpperCase(c));
+                upperCase = false;
+            } else {
+                sb.append(c);
+            }
+        }
+        return sb.toString();
+    }
+
+    /**
+     * 查找指定字符串是否匹配指定字符串列表中的任意一个字符串
+     *
+     * @param str  指定字符串
+     * @param strs 需要检查的字符串数组
+     * @return 是否匹配
+     */
+    public static boolean matches(String str, List<String> strs) {
+        if (isEmpty(str) || isEmpty(strs)) {
+            return false;
+        }
+        for (String pattern : strs) {
+            if (isMatch(pattern, str)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 判断url是否与规则配置:
+     * ? 表示单个字符;
+     * * 表示一层路径内的任意字符串,不可跨层级;
+     * ** 表示任意层路径;
+     *
+     * @param pattern 匹配规则
+     * @param url     需要匹配的url
+     * @return
+     */
+    public static boolean isMatch(String pattern, String url) {
+        AntPathMatcher matcher = new AntPathMatcher();
+        return matcher.match(pattern, url);
+    }
+
+    @SuppressWarnings("unchecked")
+    public static <T> T cast(Object obj) {
+        return (T) obj;
+    }
+
+    /**
+     * 数字左边补齐0,使之达到指定长度。注意,如果数字转换为字符串后,长度大于size,则只保留 最后size个字符。
+     *
+     * @param num  数字对象
+     * @param size 字符串指定长度
+     * @return 返回数字的字符串格式,该字符串为指定长度。
+     */
+    public static final String padl(final Number num, final int size) {
+        return padl(num.toString(), size, '0');
+    }
+
+    /**
+     * 字符串左补齐。如果原始字符串s长度大于size,则只保留最后size个字符。
+     *
+     * @param s    原始字符串
+     * @param size 字符串指定长度
+     * @param c    用于补齐的字符
+     * @return 返回指定长度的字符串,由原字符串左补齐或截取得到。
+     */
+    public static final String padl(final String s, final int size, final char c) {
+        final StringBuilder sb = new StringBuilder(size);
+        if (s != null) {
+            final int len = s.length();
+            if (s.length() <= size) {
+                for (int i = size - len; i > 0; i--) {
+                    sb.append(c);
+                }
+                sb.append(s);
+            } else {
+                return s.substring(len - size, len);
+            }
+        } else {
+            for (int i = size; i > 0; i--) {
+                sb.append(c);
+            }
+        }
+        return sb.toString();
+    }
+}

+ 529 - 0
src/main/java/com/example/unusualsounds/common/utils/UUID.java

@@ -0,0 +1,529 @@
+package com.example.unusualsounds.common.utils;
+
+import com.example.unusualsounds.common.exception.UtilException;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Random;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.regex.Pattern;
+
+/**
+ * 提供通用唯一识别码(universally unique identifier)(UUID)实现
+ */
+public final class UUID implements java.io.Serializable, Comparable<UUID> {
+    private static final long serialVersionUID = -1185015143654744140L;
+
+    /**
+     * SecureRandom 的单例
+     */
+    private static class Holder {
+        static final SecureRandom numberGenerator = getSecureRandom();
+    }
+
+    /**
+     * 此UUID的最高64有效位
+     */
+    private final long mostSigBits;
+
+    /**
+     * 此UUID的最低64有效位
+     */
+    private final long leastSigBits;
+
+    /**
+     * 私有构造
+     *
+     * @param data 数据
+     */
+    private UUID(byte[] data) {
+        long msb = 0;
+        long lsb = 0;
+        assert data.length == 16 : "data must be 16 bytes in length";
+        for (int i = 0; i < 8; i++) {
+            msb = (msb << 8) | (data[i] & 0xff);
+        }
+        for (int i = 8; i < 16; i++) {
+            lsb = (lsb << 8) | (data[i] & 0xff);
+        }
+        this.mostSigBits = msb;
+        this.leastSigBits = lsb;
+    }
+
+    /**
+     * 使用指定的数据构造新的 UUID。
+     *
+     * @param mostSigBits  用于 {@code UUID} 的最高有效 64 位
+     * @param leastSigBits 用于 {@code UUID} 的最低有效 64 位
+     */
+    public UUID(long mostSigBits, long leastSigBits) {
+        this.mostSigBits = mostSigBits;
+        this.leastSigBits = leastSigBits;
+    }
+
+    /**
+     * 获取类型 4(伪随机生成的)UUID 的静态工厂。 使用加密的本地线程伪随机数生成器生成该 UUID。
+     *
+     * @return 随机生成的 {@code UUID}
+     */
+    public static UUID fastUUID() {
+        return randomUUID(false);
+    }
+
+    /**
+     * 获取类型 4(伪随机生成的)UUID 的静态工厂。 使用加密的强伪随机数生成器生成该 UUID。
+     *
+     * @return 随机生成的 {@code UUID}
+     */
+    public static UUID randomUUID() {
+        return randomUUID(true);
+    }
+
+    /**
+     * 获取类型 4(伪随机生成的)UUID 的静态工厂。 使用加密的强伪随机数生成器生成该 UUID。
+     *
+     * @param isSecure 是否使用{@link SecureRandom}如果是可以获得更安全的随机码,否则可以得到更好的性能
+     * @return 随机生成的 {@code UUID}
+     */
+    public static UUID randomUUID(boolean isSecure) {
+        final Random ng = isSecure ? Holder.numberGenerator : getRandom();
+
+        byte[] randomBytes = new byte[16];
+        ng.nextBytes(randomBytes);
+        randomBytes[6] &= 0x0f; /* clear version */
+        randomBytes[6] |= 0x40; /* set to version 4 */
+        randomBytes[8] &= 0x3f; /* clear variant */
+        randomBytes[8] |= 0x80; /* set to IETF variant */
+        return new UUID(randomBytes);
+    }
+
+    /**
+     * 根据指定的字节数组获取类型 3(基于名称的)UUID 的静态工厂。
+     *
+     * @param name 用于构造 UUID 的字节数组。
+     * @return 根据指定数组生成的 {@code UUID}
+     */
+    public static UUID nameUUIDFromBytes(byte[] name) {
+        MessageDigest md;
+        try {
+            md = MessageDigest.getInstance("MD5");
+        } catch (NoSuchAlgorithmException nsae) {
+            throw new InternalError("MD5 not supported");
+        }
+        byte[] md5Bytes = md.digest(name);
+        md5Bytes[6] &= 0x0f; /* clear version */
+        md5Bytes[6] |= 0x30; /* set to version 3 */
+        md5Bytes[8] &= 0x3f; /* clear variant */
+        md5Bytes[8] |= 0x80; /* set to IETF variant */
+        return new UUID(md5Bytes);
+    }
+
+    /**
+     * 根据 {@link #toString()} 方法中描述的字符串标准表示形式创建{@code UUID}。
+     *
+     * @param name 指定 {@code UUID} 字符串
+     * @return 具有指定值的 {@code UUID}
+     * @throws IllegalArgumentException 如果 name 与 {@link #toString} 中描述的字符串表示形式不符抛出此异常
+     */
+    public static UUID fromString(String name) {
+        String[] components = name.split("-");
+        if (components.length != 5) {
+            throw new IllegalArgumentException("Invalid UUID string: " + name);
+        }
+        for (int i = 0; i < 5; i++) {
+            components[i] = "0x" + components[i];
+        }
+
+        long mostSigBits = Long.decode(components[0]).longValue();
+        mostSigBits <<= 16;
+        mostSigBits |= Long.decode(components[1]).longValue();
+        mostSigBits <<= 16;
+        mostSigBits |= Long.decode(components[2]).longValue();
+
+        long leastSigBits = Long.decode(components[3]).longValue();
+        leastSigBits <<= 48;
+        leastSigBits |= Long.decode(components[4]).longValue();
+
+        return new UUID(mostSigBits, leastSigBits);
+    }
+
+    /**
+     * 返回此 UUID 的 128 位值中的最低有效 64 位。
+     *
+     * @return 此 UUID 的 128 位值中的最低有效 64 位。
+     */
+    public long getLeastSignificantBits() {
+        return leastSigBits;
+    }
+
+    /**
+     * 返回此 UUID 的 128 位值中的最高有效 64 位。
+     *
+     * @return 此 UUID 的 128 位值中最高有效 64 位。
+     */
+    public long getMostSignificantBits() {
+        return mostSigBits;
+    }
+
+    /**
+     * 与此 {@code UUID} 相关联的版本号. 版本号描述此 {@code UUID} 是如何生成的。
+     * <p>
+     * 版本号具有以下含意:
+     * <ul>
+     * <li>1 基于时间的 UUID
+     * <li>2 DCE 安全 UUID
+     * <li>3 基于名称的 UUID
+     * <li>4 随机生成的 UUID
+     * </ul>
+     *
+     * @return 此 {@code UUID} 的版本号
+     */
+    public int version() {
+        // Version is bits masked by 0x000000000000F000 in MS long
+        return (int) ((mostSigBits >> 12) & 0x0f);
+    }
+
+    /**
+     * 与此 {@code UUID} 相关联的变体号。变体号描述 {@code UUID} 的布局。
+     * <p>
+     * 变体号具有以下含意:
+     * <ul>
+     * <li>0 为 NCS 向后兼容保留
+     * <li>2 <a href="http://www.ietf.org/rfc/rfc4122.txt">IETF&nbsp;RFC&nbsp;4122</a>(Leach-Salz), 用于此类
+     * <li>6 保留,微软向后兼容
+     * <li>7 保留供以后定义使用
+     * </ul>
+     *
+     * @return 此 {@code UUID} 相关联的变体号
+     */
+    public int variant() {
+        // This field is composed of a varying number of bits.
+        // 0 - - Reserved for NCS backward compatibility
+        // 1 0 - The IETF aka Leach-Salz variant (used by this class)
+        // 1 1 0 Reserved, Microsoft backward compatibility
+        // 1 1 1 Reserved for future definition.
+        return (int) ((leastSigBits >>> (64 - (leastSigBits >>> 62))) & (leastSigBits >> 63));
+    }
+
+    /**
+     * 与此 UUID 相关联的时间戳值。
+     *
+     * <p>
+     * 60 位的时间戳值根据此 {@code UUID} 的 time_low、time_mid 和 time_hi 字段构造。<br>
+     * 所得到的时间戳以 100 毫微秒为单位,从 UTC(通用协调时间) 1582 年 10 月 15 日零时开始。
+     *
+     * <p>
+     * 时间戳值仅在在基于时间的 UUID(其 version 类型为 1)中才有意义。<br>
+     * 如果此 {@code UUID} 不是基于时间的 UUID,则此方法抛出 UnsupportedOperationException。
+     *
+     * @throws UnsupportedOperationException 如果此 {@code UUID} 不是 version 为 1 的 UUID。
+     */
+    public long timestamp() throws UnsupportedOperationException {
+        checkTimeBase();
+        return (mostSigBits & 0x0FFFL) << 48//
+                | ((mostSigBits >> 16) & 0x0FFFFL) << 32//
+                | mostSigBits >>> 32;
+    }
+
+    /**
+     * 与此 UUID 相关联的时钟序列值。
+     *
+     * <p>
+     * 14 位的时钟序列值根据此 UUID 的 clock_seq 字段构造。clock_seq 字段用于保证在基于时间的 UUID 中的时间唯一性。
+     * <p>
+     * {@code clockSequence} 值仅在基于时间的 UUID(其 version 类型为 1)中才有意义。 如果此 UUID 不是基于时间的 UUID,则此方法抛出
+     * UnsupportedOperationException。
+     *
+     * @return 此 {@code UUID} 的时钟序列
+     * @throws UnsupportedOperationException 如果此 UUID 的 version 不为 1
+     */
+    public int clockSequence() throws UnsupportedOperationException {
+        checkTimeBase();
+        return (int) ((leastSigBits & 0x3FFF000000000000L) >>> 48);
+    }
+
+    /**
+     * 与此 UUID 相关的节点值。
+     *
+     * <p>
+     * 48 位的节点值根据此 UUID 的 node 字段构造。此字段旨在用于保存机器的 IEEE 802 地址,该地址用于生成此 UUID 以保证空间唯一性。
+     * <p>
+     * 节点值仅在基于时间的 UUID(其 version 类型为 1)中才有意义。<br>
+     * 如果此 UUID 不是基于时间的 UUID,则此方法抛出 UnsupportedOperationException。
+     *
+     * @return 此 {@code UUID} 的节点值
+     * @throws UnsupportedOperationException 如果此 UUID 的 version 不为 1
+     */
+    public long node() throws UnsupportedOperationException {
+        checkTimeBase();
+        return leastSigBits & 0x0000FFFFFFFFFFFFL;
+    }
+
+    /**
+     * 返回此{@code UUID} 的字符串表现形式。
+     *
+     * <p>
+     * UUID 的字符串表示形式由此 BNF 描述:
+     *
+     * <pre>
+     * {@code
+     * UUID                   = <time_low>-<time_mid>-<time_high_and_version>-<variant_and_sequence>-<node>
+     * time_low               = 4*<hexOctet>
+     * time_mid               = 2*<hexOctet>
+     * time_high_and_version  = 2*<hexOctet>
+     * variant_and_sequence   = 2*<hexOctet>
+     * node                   = 6*<hexOctet>
+     * hexOctet               = <hexDigit><hexDigit>
+     * hexDigit               = [0-9a-fA-F]
+     * }
+     * </pre>
+     *
+     * </blockquote>
+     *
+     * @return 此{@code UUID} 的字符串表现形式
+     * @see #toString(boolean)
+     */
+    @Override
+    public String toString() {
+        return toString(false);
+    }
+
+    /**
+     * 返回此{@code UUID} 的字符串表现形式。
+     *
+     * <p>
+     * UUID 的字符串表示形式由此 BNF 描述:
+     *
+     * <pre>
+     * {@code
+     * UUID                   = <time_low>-<time_mid>-<time_high_and_version>-<variant_and_sequence>-<node>
+     * time_low               = 4*<hexOctet>
+     * time_mid               = 2*<hexOctet>
+     * time_high_and_version  = 2*<hexOctet>
+     * variant_and_sequence   = 2*<hexOctet>
+     * node                   = 6*<hexOctet>
+     * hexOctet               = <hexDigit><hexDigit>
+     * hexDigit               = [0-9a-fA-F]
+     * }
+     * </pre>
+     *
+     * </blockquote>
+     *
+     * @param isSimple 是否简单模式,简单模式为不带'-'的UUID字符串
+     * @return 此{@code UUID} 的字符串表现形式
+     */
+    public String toString(boolean isSimple) {
+        final StringBuilder builder = new StringBuilder(isSimple ? 32 : 36);
+        // time_low
+        builder.append(digits(mostSigBits >> 32, 8));
+        if (false == isSimple) {
+            builder.append('-');
+        }
+        // time_mid
+        builder.append(digits(mostSigBits >> 16, 4));
+        if (false == isSimple) {
+            builder.append('-');
+        }
+        // time_high_and_version
+        builder.append(digits(mostSigBits, 4));
+        if (false == isSimple) {
+            builder.append('-');
+        }
+        // variant_and_sequence
+        builder.append(digits(leastSigBits >> 48, 4));
+        if (false == isSimple) {
+            builder.append('-');
+        }
+        // node
+        builder.append(digits(leastSigBits, 12));
+
+        return builder.toString();
+    }
+
+    /**
+     * 返回此 UUID 的哈希码。
+     *
+     * @return UUID 的哈希码值。
+     */
+    @Override
+    public int hashCode() {
+        long hilo = mostSigBits ^ leastSigBits;
+        return ((int) (hilo >> 32)) ^ (int) hilo;
+    }
+
+    /**
+     * 将此对象与指定对象比较。
+     * <p>
+     * 当且仅当参数不为 {@code null}、而是一个 UUID 对象、具有与此 UUID 相同的 varriant、包含相同的值(每一位均相同)时,结果才为 {@code true}。
+     *
+     * @param obj 要与之比较的对象
+     * @return 如果对象相同,则返回 {@code true};否则返回 {@code false}
+     */
+    @Override
+    public boolean equals(Object obj) {
+        if ((null == obj) || (obj.getClass() != UUID.class)) {
+            return false;
+        }
+        UUID id = (UUID) obj;
+        return (mostSigBits == id.mostSigBits && leastSigBits == id.leastSigBits);
+    }
+
+    // Comparison Operations
+
+    /**
+     * 将此 UUID 与指定的 UUID 比较。
+     *
+     * <p>
+     * 如果两个 UUID 不同,且第一个 UUID 的最高有效字段大于第二个 UUID 的对应字段,则第一个 UUID 大于第二个 UUID。
+     *
+     * @param val 与此 UUID 比较的 UUID
+     * @return 在此 UUID 小于、等于或大于 val 时,分别返回 -1、0 或 1。
+     */
+    @Override
+    public int compareTo(UUID val) {
+        // The ordering is intentionally set up so that the UUIDs
+        // can simply be numerically compared as two numbers
+        return (this.mostSigBits < val.mostSigBits ? -1 : //
+                (this.mostSigBits > val.mostSigBits ? 1 : //
+                        (this.leastSigBits < val.leastSigBits ? -1 : //
+                                (this.leastSigBits > val.leastSigBits ? 1 : //
+                                        0))));
+    }
+
+    // -------------------------------------------------------------------------------------------------------------------
+    // Private method start
+
+    /**
+     * 返回指定数字对应的hex值
+     *
+     * @param val    值
+     * @param digits 位
+     * @return 值
+     */
+    private static String digits(long val, int digits) {
+        long hi = 1L << (digits * 4);
+        return Long.toHexString(hi | (val & (hi - 1))).substring(1);
+    }
+
+    /**
+     * 检查是否为time-based版本UUID
+     */
+    private void checkTimeBase() {
+        if (version() != 1) {
+            throw new UnsupportedOperationException("Not a time-based UUID");
+        }
+    }
+
+    /**
+     * 获取{@link SecureRandom},类提供加密的强随机数生成器 (RNG)
+     *
+     * @return {@link SecureRandom}
+     */
+    public static SecureRandom getSecureRandom() {
+        try {
+            return SecureRandom.getInstance("SHA1PRNG");
+        } catch (NoSuchAlgorithmException e) {
+            throw new UtilException(e);
+        }
+    }
+
+    /**
+     * 获取随机数生成器对象<br>
+     * ThreadLocalRandom是JDK 7之后提供并发产生随机数,能够解决多个线程发生的竞争争夺。
+     *
+     * @return {@link ThreadLocalRandom}
+     */
+    public static ThreadLocalRandom getRandom() {
+        return ThreadLocalRandom.current();
+    }
+
+    /**
+     * UUID 格式的正则表达式
+     */
+    private static final Pattern UUID_PATTERN = Pattern.compile("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$");
+
+    /**
+     * 命名空间UUID(可以是任意UUID,这里用一个常见的命名空间UUID示例)
+     */
+    private static final UUID NAMESPACE_DNS = UUID.fromString("6ba7b810-9dad-11d1-80b4-00c04fd430c8");
+
+    /**
+     * 生成标准UUID(版本4)
+     * @return
+     */
+    public static String generateUUID() {
+        return UUID.randomUUID().toString();
+    }
+
+    /**
+     * 生成无连字符的UUID
+     * @return
+     */
+    public static String generateUUIDWithoutHyphens() {
+        return UUID.randomUUID().toString().replace("-", "");
+    }
+
+    /**
+     * 生成基于命名空间的UUID(版本3,基于MD5哈希)
+     * @param name
+     * @return
+     */
+    public static String generateUUIDv3(String name) {
+        return UUID.nameUUIDFromBytes(name.getBytes()).toString();
+    }
+
+    /**
+     * 生成基于命名空间的UUID(版本5,基于SHA-1哈希)
+     * @param name
+     * @return
+     */
+    public static String generateUUIDv5(String name) {
+        return generateUUIDv5(NAMESPACE_DNS, name);
+    }
+
+    /**
+     * 生成基于指定命名空间的UUID(版本5,基于SHA-1哈希)
+     * @param namespace
+     * @param name
+     * @return
+     */
+    public static String generateUUIDv5(UUID namespace, String name) {
+        return UUID.nameUUIDFromBytes((namespace.toString() + name).getBytes()).toString();
+    }
+
+    /**
+     * 校验是否是有效的UUID格式
+     * @param uuid
+     * @return
+     */
+    public static boolean isValidUUID(String uuid) {
+        return uuid != null && UUID_PATTERN.matcher(uuid).matches();
+    }
+
+    /**
+     * 去掉UUID中的连字符
+     * @param uuid
+     * @return
+     */
+    public static String removeHyphens(String uuid) {
+        return uuid != null ? uuid.replace("-", "") : null;
+    }
+
+    /**
+     * 格式化UUID,添加连字符
+     * @param uuid
+     * @return
+     */
+    public static String addHyphens(String uuid) {
+        if (uuid == null || uuid.length() != 32) {
+            throw new IllegalArgumentException("Invalid UUID format without hyphens");
+        }
+        return uuid.substring(0, 8) + "-" +
+                uuid.substring(8, 12) + "-" +
+                uuid.substring(12, 16) + "-" +
+                uuid.substring(16, 20) + "-" +
+                uuid.substring(20);
+    }
+
+}

+ 30 - 0
src/main/java/com/example/unusualsounds/framework/config/CorsConfig.java

@@ -0,0 +1,30 @@
+package com.example.unusualsounds.framework.config;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.Ordered;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
+import org.springframework.web.filter.CorsFilter;
+
+
+@Configuration
+public class CorsConfig {
+
+    @Bean
+    public FilterRegistrationBean<CorsFilter> corsFilter() {
+        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+        CorsConfiguration config = new CorsConfiguration();
+        // 允许所有域名(生产环境应指定具体域名)
+        config.addAllowedOriginPattern("*");
+        config.addAllowedHeader("*");
+        config.addAllowedMethod("*");
+        config.setAllowCredentials(true); // 允许携带凭证(如Cookie)
+        source.registerCorsConfiguration("/**", config);
+
+        FilterRegistrationBean<CorsFilter> bean =
+                new FilterRegistrationBean<>(new CorsFilter(source));
+        bean.setOrder(Ordered.HIGHEST_PRECEDENCE); // 最高优先级
+        return bean;
+    }
+}

+ 21 - 0
src/main/java/com/example/unusualsounds/framework/config/MybatisPlusConfig.java

@@ -0,0 +1,21 @@
+package com.example.unusualsounds.framework.config;
+
+
+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;
+
+@Configuration
+public class MybatisPlusConfig {
+
+    @Bean
+    public MybatisPlusInterceptor mybatisPlusInterceptor() {
+        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
+        // 设置数据库类型为MySQL, 根据实际情况选择DbType
+        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
+        return interceptor;
+    }
+}
+

+ 26 - 0
src/main/java/com/example/unusualsounds/framework/config/SwaggerConfig.java

@@ -0,0 +1,26 @@
+package com.example.unusualsounds.framework.config;
+
+
+import io.swagger.v3.oas.models.ExternalDocumentation;
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.info.Contact;
+import io.swagger.v3.oas.models.info.Info;
+import io.swagger.v3.oas.models.info.License;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class SwaggerConfig {
+
+    @Bean
+    public OpenAPI swaggerOpenAPI(){
+
+        return new OpenAPI()
+                .info(new Info().title("异音识别微服务")
+                        .contact(new Contact().name("礼云"))
+                        .description("API文档")
+                        .version("v1.0")
+                        .license(new License().name("Apache 2.0").url("http://springdoc.org")));
+    }
+
+}

+ 30 - 0
src/main/java/com/example/unusualsounds/framework/minio/MinioConfig.java

@@ -0,0 +1,30 @@
+package com.example.unusualsounds.framework.minio;
+
+
+import io.minio.MinioClient;
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+@Data
+@ConfigurationProperties(prefix = "minio")
+public class MinioConfig {
+
+
+    private String endpoint;
+    private String ak;
+    private String sk;
+    private String bucket;
+
+    @Bean
+    public MinioClient minioClient() {
+
+        return MinioClient.builder()
+                .endpoint(endpoint)
+                .credentials(ak, sk)
+                .build();
+    }
+
+}

+ 209 - 0
src/main/java/com/example/unusualsounds/framework/minio/MinioUtil.java

@@ -0,0 +1,209 @@
+package com.example.unusualsounds.framework.minio;
+
+import com.example.unusualsounds.common.utils.DateUtils;
+import com.example.unusualsounds.common.utils.UUID;
+import io.minio.*;
+import io.minio.http.Method;
+import io.minio.messages.Bucket;
+import io.minio.messages.Item;
+import jakarta.annotation.Resource;
+import jakarta.servlet.ServletOutputStream;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import org.springframework.util.FastByteArrayOutputStream;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Component
+@Slf4j
+public class MinioUtil {
+
+    @Autowired
+    private MinioConfig prop;
+
+    @Resource
+    private MinioClient minioClient;
+
+
+    /**
+     * 查看存储bucket是否存在
+     * @return boolean
+     */
+    public Boolean bucketExists(String bucketName) {
+        Boolean found;
+        try {
+            found = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+        return found;
+    }
+
+    /**
+     * 创建存储bucket
+     * @return Boolean
+     */
+    public Boolean makeBucket(String bucketName) {
+        try {
+            minioClient.makeBucket(MakeBucketArgs.builder()
+                    .bucket(bucketName)
+                    .build());
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+        return true;
+    }
+    /**
+     * 删除存储bucket
+     * @return Boolean
+     */
+    public Boolean removeBucket(String bucketName) {
+        try {
+            minioClient.removeBucket(RemoveBucketArgs.builder()
+                    .bucket(bucketName)
+                    .build());
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+        return true;
+    }
+    /**
+     * 获取全部bucket
+     */
+    public List<Bucket> getAllBuckets() {
+        try {
+            List<Bucket> buckets = minioClient.listBuckets();
+            return buckets;
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+
+
+    /**
+     * 文件上传
+     *
+     * @param file 文件
+     * @return Boolean
+     */
+    public String upload(MultipartFile file, String path) {
+        String originalFilename = file.getOriginalFilename();
+        if (StringUtils.isBlank(originalFilename)){
+            throw new RuntimeException();
+        }
+        String fileName = UUID.randomUUID().toString() + originalFilename.substring(originalFilename.lastIndexOf("."));
+
+        String objectName = path+ DateUtils.getDate() + "/" + fileName;
+
+
+        try {
+            PutObjectArgs objectArgs = PutObjectArgs.builder().bucket(prop.getBucket()).object(objectName)
+                    .stream(file.getInputStream(), file.getSize(), -1).contentType(file.getContentType()).build();
+            //文件名称相同会覆盖
+            minioClient.putObject(objectArgs);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return null;
+        }
+        return prop.getEndpoint()+"/"+prop.getBucket()+"/"+objectName;
+    }
+
+    /**
+     * 预览图片
+     * @param fileName
+     * @return
+     */
+    public String preview(String fileName)  {
+
+        // 查看文件地址
+        GetPresignedObjectUrlArgs build = new GetPresignedObjectUrlArgs().builder()
+                .bucket(prop.getBucket())
+                .object(fileName)
+                .method(Method.GET)
+                .build();
+        try {
+            String url = minioClient.getPresignedObjectUrl(build);
+            return url;
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    /**
+     * 文件下载
+     * @param fileName 文件名称
+     * @param res response
+     * @return Boolean
+     */
+    public void download(String fileName, HttpServletResponse res) {
+        GetObjectArgs objectArgs = GetObjectArgs.builder().bucket(prop.getBucket())
+                .object(fileName).build();
+        try (GetObjectResponse response = minioClient.getObject(objectArgs)){
+            byte[] buf = new byte[1024];
+            int len;
+            try (FastByteArrayOutputStream os = new FastByteArrayOutputStream()){
+                while ((len=response.read(buf))!=-1){
+                    os.write(buf,0,len);
+                }
+                os.flush();
+                byte[] bytes = os.toByteArray();
+                res.setCharacterEncoding("utf-8");
+                // 设置强制下载不打开
+                // res.setContentType("application/force-download");
+                res.addHeader("Content-Disposition", "attachment;fileName=" + fileName);
+                try (ServletOutputStream stream = res.getOutputStream()){
+                    stream.write(bytes);
+                    stream.flush();
+                }
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * 查看文件对象
+     * @return 存储bucket内文件对象信息
+     */
+    public List<Item> listObjects() {
+        Iterable<Result<Item>> results = minioClient.listObjects(
+                ListObjectsArgs.builder().bucket(prop.getBucket()).build());
+        List<Item> items = new ArrayList<>();
+        try {
+            for (Result<Item> result : results) {
+                items.add(result.get());
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            return null;
+        }
+        return items;
+    }
+
+    /**
+     * 删除
+     * @param fileName
+     * @return
+     * @throws Exception
+     */
+    public boolean remove(String fileName){
+        try {
+            minioClient.removeObject( RemoveObjectArgs.builder().bucket(prop.getBucket()).object(fileName).build());
+        }catch (Exception e){
+            return false;
+        }
+        return true;
+    }
+
+}

+ 148 - 0
src/main/java/com/example/unusualsounds/project/device/controller/SensorsController.java

@@ -0,0 +1,148 @@
+package com.example.unusualsounds.project.device.controller;
+
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.example.unusualsounds.common.utils.RedisUtils;
+import com.example.unusualsounds.common.utils.UUID;
+import com.example.unusualsounds.framework.minio.MinioUtil;
+import com.example.unusualsounds.project.device.entity.DevicesSoundSensors;
+import com.example.unusualsounds.project.device.entity.vo.RegisterSoundSensors;
+import com.example.unusualsounds.project.device.entity.vo.SoundSensorsQueryVo;
+import com.example.unusualsounds.project.device.service.impl.DeviceVoiceServiceImpl;
+import com.example.unusualsounds.project.device.service.impl.DevicesSoundSensorsServiceImpl;
+import com.example.unusualsounds.web.entity.AjaxResult;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.List;
+
+@RestController
+@Tag(name = "ser")
+@RequestMapping("/voiceSer")
+public class SensorsController {
+
+    @Value("${minio.dir}")
+    private String dir;
+
+    @Autowired
+    private MinioUtil minioUtil;
+
+    @Autowired
+    private DeviceVoiceServiceImpl deviceVoiceServiceImpl;
+
+    @Autowired
+    private DevicesSoundSensorsServiceImpl devicesSoundSensorsServiceImpl;
+
+
+    @Operation(summary = "上传文件,校验文件")
+    @PostMapping("/uploadVoiceFile")
+    public AjaxResult uploadFile(@RequestParam("file") MultipartFile file){
+        AjaxResult ajaxResult = new AjaxResult();
+
+        //简单约束一下文件大小和后缀
+        if(!deviceVoiceServiceImpl.checkVoiceFile(file.getOriginalFilename(), file.getSize())){
+            ajaxResult.put("code",10001);
+            ajaxResult.put("msg","文件上传失败,请检查文件大小和格式");
+            return ajaxResult;
+        }
+
+        String uuid = UUID.generateUUIDWithoutHyphens();
+        String upload = minioUtil.upload(file,dir);
+        boolean b = RedisUtils.setForeverKey(uuid, upload);
+        ajaxResult.put("code",200);
+        ajaxResult.put("msg","文件上传成功");
+        ajaxResult.put("fileUUID",uuid);
+        return ajaxResult;
+
+    }
+
+
+
+    @Operation(summary = "增加传感器,修改传感器技能信息")
+    @PostMapping("/uploadSensors")
+    public AjaxResult testMinio(HttpServletRequest request,@RequestBody RegisterSoundSensors soundSensors){
+        AjaxResult ajaxResult = new AjaxResult();
+
+        String remoteHost = request.getHeader("Ip");
+        if (remoteHost==null||"".equals(remoteHost)){
+            remoteHost = " ";
+        }
+        System.out.println("remoteHost:"+remoteHost);
+
+//        String remoteHost = "测试IP";
+
+        //修改传感器技能信息
+        if(soundSensors.getSoundSensorUuid()!=null&& soundSensors.getSoundSensorUuid()!=""){
+            boolean updateSensorSkill = devicesSoundSensorsServiceImpl.updateSensorSkill(soundSensors);
+            ajaxResult.put("code",200);
+            ajaxResult.put("msg","技能修改成功!");
+            return ajaxResult;
+        }
+        boolean checkSensorName = devicesSoundSensorsServiceImpl.checkSensorName(soundSensors.getSoundSensorName());
+        if(!checkSensorName){
+            ajaxResult.put("code",10001);
+            ajaxResult.put("msg","传感器名称重复!");
+            return ajaxResult;
+        }
+
+        switch (soundSensors.getSoundSensorType()){
+            case "file":
+                if (soundSensors.getSoundFileUUid()==null||"".equals(soundSensors.getSoundFileUUid())){
+                    ajaxResult.put("code",10001);
+                    ajaxResult.put("msg","文件UUID不能为空!");
+                    return ajaxResult;
+                }
+                break;
+            case "sensor":
+                if (soundSensors.getSoundSensorUrl()==null||"".equals(soundSensors.getSoundSensorUrl())){
+                    ajaxResult.put("code",10001);
+                    ajaxResult.put("msg","传感器链接不能为空!");
+                    return ajaxResult;
+                }
+                break;
+            default:
+                ajaxResult.put("code",10001);
+                ajaxResult.put("msg","传感器类型错误!");
+                return ajaxResult;
+        }
+        boolean saveSensor =devicesSoundSensorsServiceImpl.saveSensor(soundSensors,remoteHost);
+        ajaxResult.put("code",200);
+        ajaxResult.put("msg","传感器添加成功");
+        return ajaxResult;
+
+    }
+
+
+    @Operation(summary = "删除传感器")
+    @PostMapping("/delSensor")
+    public AjaxResult delSensor(@RequestBody List<String> sensorUuids){
+
+        if(sensorUuids.size()==0||sensorUuids==null){
+         return AjaxResult.error("请选择要删除的传感器!");
+        }
+       Integer delSensor =  devicesSoundSensorsServiceImpl.delSensor(sensorUuids);
+
+       if (delSensor>1){
+           return AjaxResult.success("删除传感器成功!");
+       }
+       return AjaxResult.error("删除传感器失败!");
+    }
+
+    @Operation(summary = "查询传感器列表")
+    @PostMapping("/getSensorList")
+    private AjaxResult getSensorList(HttpServletRequest request,@RequestBody SoundSensorsQueryVo soundSensorsQueryVo){
+        String remoteHost = request.getHeader("Ip");
+        IPage<DevicesSoundSensors>  sensorList = devicesSoundSensorsServiceImpl.getSensorList(soundSensorsQueryVo,remoteHost);
+        return AjaxResult.success(sensorList);
+
+    }
+
+
+
+}

+ 250 - 0
src/main/java/com/example/unusualsounds/project/device/entity/DevicesSoundSensors.java

@@ -0,0 +1,250 @@
+package com.example.unusualsounds.project.device.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 java.io.Serializable;
+import java.util.Date;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 设备信息表
+ * @TableName devices_sound_sensors
+ */
+@TableName(value ="devices_sound_sensors")
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class DevicesSoundSensors implements Serializable {
+    /**
+     * 自增id
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 设备id
+     */
+    @TableField(value = "uuid")
+    private String uuid;
+
+    /**
+     * 部门ID
+     */
+    @TableField(value = "dept_uuid")
+    private String deptUuid;
+
+    /**
+     * 设备名,代码逻辑限制其唯一性
+     */
+    @TableField(value = "name")
+    private String name;
+
+    /**
+     * 可播音频地址
+     */
+    @TableField(value = "stream_url")
+    private String streamUrl;
+
+    /**
+     * 分析后的标清流地址
+     */
+    @TableField(value = "analysis_sd_url")
+    private String analysisSdUrl;
+
+    /**
+     * 分析后的高清流地址
+     */
+    @TableField(value = "analysis_hd_url")
+    private String analysisHdUrl;
+
+    /**
+     * 原始视频流地址
+     */
+    @TableField(value = "src_url")
+    private String srcUrl;
+
+    /**
+     * 设备描述
+     */
+    @TableField(value = "description")
+    private String description;
+
+    /**
+     * 地址描述
+     */
+    @TableField(value = "ip")
+    private String ip;
+
+    /**
+     * 创建时间
+     */
+    @TableField(value = "created_at")
+    private Date createdAt;
+
+    /**
+     * 更新时间
+     */
+    @TableField(value = "updated_at")
+    private Date updatedAt;
+
+    /**
+     * 删除时间
+     */
+    @TableField(value = "deleted_at")
+    private Date deletedAt;
+
+    /**
+     * 设备状态 online/offline/error
+     */
+    @TableField(value = "status")
+    private String status;
+
+    /**
+     * bool, 是否在使用中
+     */
+    @TableField(value = "is_used")
+    private Integer isUsed;
+
+    /**
+     * 图片链接
+     */
+    @TableField(value = "image_url")
+    private String imageUrl;
+
+    /**
+     * 图片分辨率(dx,dy)
+     */
+    @TableField(value = "image_resolution_json")
+    private String imageResolutionJson;
+
+    /**
+     * 
+     */
+    @TableField(value = "user_uuid")
+    private String userUuid;
+
+    /**
+     * 组织ID
+     */
+    @TableField(value = "org_uuid")
+    private String orgUuid;
+
+    /**
+     * 
+     */
+    @TableField(value = "type")
+    private String type;
+
+    /**
+     * 
+     */
+    @TableField(value = "stream_id")
+    private String streamId;
+
+    /**
+     * 
+     */
+    @TableField(value = "transfer_url")
+    private String transferUrl;
+
+    /**
+     * 注册的流媒体服务地址
+     */
+    @TableField(value = "stream_host")
+    private String streamHost;
+
+    /**
+     * 机器人id
+     */
+    @TableField(value = "robot_id")
+    private String robotId;
+
+    /**
+     * 摄像头标定
+     */
+    @TableField(value = "camera_matrix_json")
+    private String cameraMatrixJson;
+
+    /**
+     * 数据来源 cloud/node/aiBox
+     */
+    @TableField(value = "data_source")
+    private String dataSource;
+
+    /**
+     * 国标ID
+     */
+    @TableField(value = "gb_id")
+    private String gbId;
+
+    /**
+     * 国标根节点ID
+     */
+    @TableField(value = "gb_root_id")
+    private String gbRootId;
+
+    /**
+     * 设备坐标
+     */
+    @TableField(value = "location")
+    private Double location;
+
+    /**
+     * bool, 是否轮巡
+     */
+    @TableField(value = "is_turninged")
+    private Integer isTurninged;
+
+    /**
+     * 点位id
+     */
+    @TableField(value = "spot_uuid")
+    private String spotUuid;
+
+    /**
+     * 部门
+     */
+    @TableField(value = "department")
+    private String department;
+
+    /**
+     * 部门负责人
+     */
+    @TableField(value = "director")
+    private String director;
+
+    /**
+     * file 上传文件 置空:流地址
+     */
+    @TableField(value = "video_type")
+    private String videoType;
+
+    /**
+     * 文件名uuid,流地址置空
+     */
+    @TableField(value = "file_uuid")
+    private String fileUuid;
+
+    /**
+     * 技能id
+     */
+    @TableField(value = "skill_uuid")
+    private String skillUuid;
+
+    /**
+     * 负责人姓名
+     */
+    @TableField(value = "responsible_person")
+    private String responsiblePerson;
+
+
+    @TableField(exist = false)
+    private static final long serialVersionUID = 1L;
+
+
+}

+ 83 - 0
src/main/java/com/example/unusualsounds/project/device/entity/vo/RegisterSoundSensors.java

@@ -0,0 +1,83 @@
+package com.example.unusualsounds.project.device.entity.vo;
+
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.hibernate.validator.constraints.Length;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@Schema(description = "传感器注册信息")
+public class RegisterSoundSensors {
+
+    /**
+     * 传感器名称
+     */
+    @Schema(description = "传感器名称")
+    @NotBlank
+    @Length(min = 1, max = 30)
+    private String soundSensorName;
+
+    /**
+     * 类型 ,传感器接入还是文件接入
+     */
+    @Schema(description = "类型(sensor or file)传感器接入还是文件接入")
+    @NotBlank
+    private String soundSensorType;
+
+    /**
+     * 传感器地址
+     */
+    @Schema(description = "传感器地址")
+    private String soundSensorUrl;
+
+    /**
+     * 文件uuid
+     */
+    @Schema(description = "文件uuid")
+    private String soundFileUUid;
+
+    /**
+     * 责任部门UUid
+     */
+    @Schema(description = "责任部门UUid")
+    @NotBlank
+    private String deptUuid;
+
+    /**
+     * 责任部门名称
+     */
+    @Schema(description = "责任部门名称")
+    @NotBlank
+    private String department;
+
+    /**
+     * 备注,描述
+     */
+    @Schema(description = "备注,描述")
+    private String description;
+
+    /**
+     * 技能UUid
+     */
+    @Schema(description = "技能Uuid")
+    @NotBlank
+    private String skillUuid;
+
+    /**
+     * 责任人
+     */
+    @Schema(description = "责任人")
+    private String responsiblePerson;
+
+
+    /**
+     * 甄别是插入还是删除
+     */
+    @Schema(description = "设备Uuid")
+    private String soundSensorUuid;
+}

+ 63 - 0
src/main/java/com/example/unusualsounds/project/device/entity/vo/SoundSensorsQueryVo.java

@@ -0,0 +1,63 @@
+package com.example.unusualsounds.project.device.entity.vo;
+
+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 io.swagger.v3.oas.annotations.media.Schema;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 设备信息查询视图对象
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@Schema(description = "传感器分页查询视图")
+public class SoundSensorsQueryVo implements Serializable {
+
+
+    /**
+     * 当前页码
+     */
+    @Schema(description = "当前页码")
+    private Integer pageNum;
+
+    /**
+     * 每页显示条数
+     */
+    @Schema(description = "每页显示条数")
+    private Integer pageSize;
+
+    /**
+     * 设备名,代码逻辑限制其唯一性
+     */
+    @Schema(description = "设备名")
+    private String name;
+
+
+
+    /**
+     * 设备状态 online/offline/error
+     */
+    @Schema(description = "设备状态")
+    private String status;
+
+
+
+    /**
+     * 部门id
+     */
+    @Schema(description = "部门id")
+    private String deptUuid;
+
+
+
+
+
+}

+ 38 - 0
src/main/java/com/example/unusualsounds/project/device/entity/vo/SoundSensorsVo.java

@@ -0,0 +1,38 @@
+package com.example.unusualsounds.project.device.entity.vo;
+
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 传感器视图vo
+ */
+@Schema(title = "传感器视图vo")
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class SoundSensorsVo {
+
+    /**
+     * 设备id
+     */
+    @TableField(value = "uuid")
+    private String uuid;
+
+    /**
+     * 部门ID
+     */
+    @TableField(value = "dept_uuid")
+    private String deptUuid;
+
+    /**
+     * 设备名,代码逻辑限制其唯一性
+     */
+    @TableField(value = "name")
+    private String name;
+
+}

+ 23 - 0
src/main/java/com/example/unusualsounds/project/device/mapper/DevicesSoundSensorsMapper.java

@@ -0,0 +1,23 @@
+package com.example.unusualsounds.project.device.mapper;
+
+import com.example.unusualsounds.project.device.entity.DevicesSoundSensors;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+* @author pengc
+* @description 针对表【devices_sound_sensors(设备信息表)】的数据库操作Mapper
+* @createDate 2025-02-21 13:23:08
+* @Entity com.example.unusualsounds.project.device.entity.DevicesSoundSensors
+*/
+@Mapper
+public interface DevicesSoundSensorsMapper extends BaseMapper<DevicesSoundSensors> {
+
+
+
+
+}
+
+
+
+

+ 16 - 0
src/main/java/com/example/unusualsounds/project/device/service/DeviceVoiceService.java

@@ -0,0 +1,16 @@
+package com.example.unusualsounds.project.device.service;
+
+
+
+public interface DeviceVoiceService {
+
+
+    /**
+     * 检查文件是否为语音文件和文件大小是否符合要求
+     * @param fileName
+     * @param fileSize
+     * @return
+     */
+    boolean checkVoiceFile(String fileName, Long fileSize);
+
+}

+ 59 - 0
src/main/java/com/example/unusualsounds/project/device/service/DevicesSoundSensorsService.java

@@ -0,0 +1,59 @@
+package com.example.unusualsounds.project.device.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.example.unusualsounds.project.device.entity.DevicesSoundSensors;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.example.unusualsounds.project.device.entity.vo.RegisterSoundSensors;
+import com.example.unusualsounds.project.device.entity.vo.SoundSensorsQueryVo;
+
+import java.util.List;
+
+/**
+* @author pengc
+* @description 针对表【devices_sound_sensors(设备信息表)】的数据库操作Service
+* @createDate 2025-02-21 13:23:08
+*/
+public interface DevicesSoundSensorsService extends IService<DevicesSoundSensors> {
+
+    /**
+     * 检查是否有重名的文件uuid
+     * @param originalFilename
+     * @return
+     */
+    String checkFileNameUUID(String originalFilename);
+
+    /**
+     * 检查是否有重名的传感器名称
+     * @param soundSensorName
+     * @return
+     */
+    boolean checkSensorName(String soundSensorName);
+
+    /**
+     * 保存传感器信息
+     * @param soundSensors
+     * @return
+     */
+    boolean saveSensor(RegisterSoundSensors soundSensors,String remoteHost);
+
+    /**
+     * 更新传感器技能信息
+     * @param soundSensors
+     * @return
+     */
+    boolean updateSensorSkill(RegisterSoundSensors soundSensors);
+
+    /**
+     * 批量删除传感器信息
+     * @param sensorUuids
+     * @return
+     */
+    Integer delSensor(List<String> sensorUuids);
+
+    /**
+     * 查询传感器列表
+     * @param soundSensorsQueryVo
+     * @return
+     */
+    IPage<DevicesSoundSensors> getSensorList(SoundSensorsQueryVo soundSensorsQueryVo,String remoteHost);
+}

+ 29 - 0
src/main/java/com/example/unusualsounds/project/device/service/impl/DeviceVoiceServiceImpl.java

@@ -0,0 +1,29 @@
+package com.example.unusualsounds.project.device.service.impl;
+
+
+import com.example.unusualsounds.project.device.service.DeviceVoiceService;
+import org.springframework.stereotype.Service;
+
+import java.util.Arrays;
+import java.util.List;
+
+@Service
+public class DeviceVoiceServiceImpl implements DeviceVoiceService {
+
+
+    private static final List<String> SUPPORTED_AUDIO_FORMATS = Arrays.asList("mp4",".mp3", ".wma", ".wav", ".aac", ".flac");
+
+
+    @Override
+    public boolean checkVoiceFile(String fileName, Long fileSize) {
+
+        if(fileSize > 1024 * 1024*50){
+            return false;
+        }
+        String s = fileName.split("\\.")[1].toLowerCase();
+        if(!SUPPORTED_AUDIO_FORMATS.contains(s)){
+            return false;
+        }
+        return true;
+    }
+}

+ 265 - 0
src/main/java/com/example/unusualsounds/project/device/service/impl/DevicesSoundSensorsServiceImpl.java

@@ -0,0 +1,265 @@
+package com.example.unusualsounds.project.device.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.example.unusualsounds.common.utils.BaiDuUtils;
+import com.example.unusualsounds.common.utils.RedisUtils;
+import com.example.unusualsounds.common.utils.UUID;
+import com.example.unusualsounds.project.device.entity.DevicesSoundSensors;
+import com.example.unusualsounds.project.device.entity.vo.RegisterSoundSensors;
+import com.example.unusualsounds.project.device.entity.vo.SoundSensorsQueryVo;
+import com.example.unusualsounds.project.device.service.DevicesSoundSensorsService;
+import com.example.unusualsounds.project.device.mapper.DevicesSoundSensorsMapper;
+import com.example.unusualsounds.project.vox.entity.Skills;
+import com.example.unusualsounds.project.vox.service.impl.EdgeOrganizationServiceImpl;
+import com.example.unusualsounds.project.vox.service.impl.SkillsServiceImpl;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.*;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+* @author pengc
+* @description 针对表【devices_sound_sensors(设备信息表)】的数据库操作Service实现
+* @createDate 2025-02-21 13:23:08
+*/
+@Service
+@Slf4j
+public class DevicesSoundSensorsServiceImpl extends ServiceImpl<DevicesSoundSensorsMapper, DevicesSoundSensors>
+    implements DevicesSoundSensorsService{
+
+
+    @Value("${vox.post-analysis-url}")
+    private String postAnalysisUrl;
+
+    @Autowired
+    private SkillsServiceImpl skillsServiceImpl;
+
+    @Autowired
+    private RestTemplate restTemplate;
+
+    @Autowired
+    private EdgeOrganizationServiceImpl edgeOrganizationServiceImpl;
+
+
+    @Override
+    public String checkFileNameUUID(String originalFilename) {
+
+        String s = UUID.generateUUIDv5(originalFilename);
+        String s1 = UUID.removeHyphens(s);
+
+        QueryWrapper<DevicesSoundSensors> queryWrapper = new QueryWrapper<>();
+        queryWrapper.eq("file_uuid",s1).last("limit 1");
+
+        DevicesSoundSensors devicesSoundSensors = baseMapper.selectOne(queryWrapper);
+        if (devicesSoundSensors != null){
+            return devicesSoundSensors.getFileUuid();
+        }
+        return s1;
+    }
+
+    @Override
+    public boolean checkSensorName(String soundSensorName) {
+        QueryWrapper<DevicesSoundSensors> queryWrapper = new QueryWrapper<>();
+        queryWrapper.eq("name",soundSensorName).last("limit 1");
+
+        DevicesSoundSensors devicesSoundSensors = baseMapper.selectOne(queryWrapper);
+
+        if (devicesSoundSensors != null){
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public boolean saveSensor(RegisterSoundSensors soundSensors,String remoteHost) {
+
+        DevicesSoundSensors devicesSoundSensors = initSoundSensors();
+
+        devicesSoundSensors.setName(soundSensors.getSoundSensorName());
+        devicesSoundSensors.setDeptUuid(soundSensors.getDeptUuid());
+
+        if("file".equals(soundSensors.getSoundSensorType())){
+            Object o = RedisUtils.get(soundSensors.getSoundFileUUid());
+            devicesSoundSensors.setStreamUrl(o.toString());
+            devicesSoundSensors.setSrcUrl(o.toString());
+            devicesSoundSensors.setStatus("online");
+            devicesSoundSensors.setVideoType("file");
+            devicesSoundSensors.setFileUuid(soundSensors.getSoundFileUUid());
+        }else {
+            devicesSoundSensors.setStreamUrl(soundSensors.getSoundSensorUrl());
+            devicesSoundSensors.setSrcUrl(soundSensors.getSoundSensorUrl());
+            devicesSoundSensors.setStatus("offline");
+            devicesSoundSensors.setVideoType("stream");
+        }
+        HashMap<String, String> organizations = edgeOrganizationServiceImpl.getOrganizations();
+
+        devicesSoundSensors.setOrgUuid(organizations.get("orgUuid"));
+        devicesSoundSensors.setDepartment("{\"id\":\""+soundSensors.getDeptUuid()+"\",\"name\":\""+soundSensors.getDepartment()+"\"}");
+        devicesSoundSensors.setSkillUuid(soundSensors.getSkillUuid());
+        devicesSoundSensors.setIp(remoteHost);
+        devicesSoundSensors.setResponsiblePerson(soundSensors.getResponsiblePerson());
+        devicesSoundSensors.setDescription(soundSensors.getDescription());
+
+        int insert = baseMapper.insert(devicesSoundSensors);
+        RedisUtils.delete(soundSensors.getSoundFileUUid());
+        //对文件进行立刻通知算法解析(如果是文件类型,通知算法解析)
+
+        if("file".equals(soundSensors.getSoundSensorType())){
+            QueryWrapper<Skills> skillsQueryWrapper = new QueryWrapper<>();
+            skillsQueryWrapper.eq("uuid",soundSensors.getSkillUuid()).last("limit 1");
+            Skills skills = skillsServiceImpl.getBaseMapper().selectOne(skillsQueryWrapper);
+
+            this.analysisSoundFile(devicesSoundSensors.getSrcUrl(),skills.getName(), devicesSoundSensors.getUuid(),
+                    devicesSoundSensors.getName(),soundSensors.getSkillUuid());
+        }
+
+
+        if (insert > 0){
+            return true;
+        }else {
+            return false;
+        }
+    }
+
+    @Override
+    public boolean updateSensorSkill(RegisterSoundSensors soundSensors) {
+
+        UpdateWrapper<DevicesSoundSensors> updateWrapper = new UpdateWrapper<>();
+        updateWrapper.eq("uuid",soundSensors.getSoundSensorUuid())
+                        .set("skill_uuid",soundSensors.getSkillUuid());
+        int update = baseMapper.update(null, updateWrapper);
+        if (update > 0){
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public Integer delSensor(List<String> sensorUuids) {
+
+        QueryWrapper<DevicesSoundSensors> queryWrapper = new QueryWrapper<>();
+        queryWrapper.in("uuid",sensorUuids);
+
+        int delete = baseMapper.delete(queryWrapper);
+
+        return delete;
+    }
+
+    @Override
+    public IPage<DevicesSoundSensors> getSensorList(SoundSensorsQueryVo soundSensorsQueryVo,String remoteHost) {
+
+        Page<DevicesSoundSensors> sensorsPage = new Page<>(soundSensorsQueryVo.getPageNum(), soundSensorsQueryVo.getPageSize());
+
+        QueryWrapper<DevicesSoundSensors> queryWrapper = new QueryWrapper<>();
+
+        if (soundSensorsQueryVo.getName() != null && !"".equals(soundSensorsQueryVo.getName())){
+            queryWrapper.like("name",soundSensorsQueryVo.getName());
+        }
+        if (soundSensorsQueryVo.getStatus() != null && !"".equals(soundSensorsQueryVo.getStatus())){
+            queryWrapper.eq("status",soundSensorsQueryVo.getStatus());
+        }
+        if(soundSensorsQueryVo.getDeptUuid() != null && !"".equals(soundSensorsQueryVo.getDeptUuid())){
+            queryWrapper.eq("dept_uuid",soundSensorsQueryVo.getDeptUuid());
+        }
+        if(remoteHost != null && !"".equals(remoteHost)){
+            queryWrapper.eq("ip",remoteHost);
+        }
+        IPage<DevicesSoundSensors> page = baseMapper.selectPage(sensorsPage, queryWrapper);
+        return page;
+    }
+
+
+    /**
+     * 初始化声音传感器部分基本信息
+     * @return
+     */
+    public DevicesSoundSensors initSoundSensors(){
+
+        DevicesSoundSensors devicesSoundSensors = new DevicesSoundSensors();
+        devicesSoundSensors.setUuid(UUID.generateUUIDWithoutHyphens());
+        devicesSoundSensors.setAnalysisHdUrl("");
+        devicesSoundSensors.setAnalysisSdUrl("");
+        devicesSoundSensors.setDescription("");
+        devicesSoundSensors.setIp("");
+        devicesSoundSensors.setCreatedAt(new Date());
+        devicesSoundSensors.setUpdatedAt(new Date());
+        devicesSoundSensors.setIsUsed(0);
+        devicesSoundSensors.setImageUrl("");
+        devicesSoundSensors.setType("normal");
+        devicesSoundSensors.setStreamHost("");
+        devicesSoundSensors.setRobotId("");
+        devicesSoundSensors.setDataSource("cloud");
+        devicesSoundSensors.setGbId("");
+        devicesSoundSensors.setGbRootId("");
+        devicesSoundSensors.setLocation(0.0);
+        devicesSoundSensors.setIsTurninged(0);
+        devicesSoundSensors.setDirector("[]");
+        devicesSoundSensors.setFileUuid("");
+        devicesSoundSensors.setSkillUuid("");
+        devicesSoundSensors.setVideoType("sensor");
+        return devicesSoundSensors;
+    }
+
+
+    /**
+     * 通知算法对声音文件进行解析
+     * @param
+     */
+    public void analysisSoundFile(String filePath,String skillName,String streamId,String deviceName,String skillId) {
+
+        //同时支持20个用户上传文件解析
+        ExecutorService executorService = Executors.newFixedThreadPool(20);
+
+        CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
+            log.info("通知算法解析用户自定义上传文件");
+
+            HashMap<String, Object> hashMap = new HashMap<>();
+            hashMap.put("filePath",filePath);
+            hashMap.put("skillName",skillName);
+            hashMap.put("streamId",streamId);
+            hashMap.put("deviceName",deviceName);
+            hashMap.put("skillId",skillId);
+
+            HttpHeaders httpHeaders = new HttpHeaders();
+            httpHeaders.setContentType(MediaType.APPLICATION_JSON);
+            HttpEntity<Map<String, Object>> mapHttpEntity = new HttpEntity<Map<String, Object>>(hashMap, httpHeaders);
+            ResponseEntity<String> stringResponseEntity = restTemplate.postForEntity(postAnalysisUrl, mapHttpEntity, String.class);
+            if (stringResponseEntity.getStatusCode() == HttpStatus.OK){
+                log.info("通知算法成功");
+            }
+
+        },executorService);
+        try {
+            future.get();
+
+            executorService.shutdown();
+        } catch (InterruptedException e) {
+            throw new RuntimeException(e);
+        } catch (ExecutionException e) {
+            throw new RuntimeException(e);
+        }
+
+
+    }
+
+
+}
+
+
+
+

+ 68 - 0
src/main/java/com/example/unusualsounds/project/vox/config/AsyncConfig.java

@@ -0,0 +1,68 @@
+package com.example.unusualsounds.project.vox.config;
+
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+
+import java.util.concurrent.*;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * 接口异步配置类
+ */
+@Configuration
+public class AsyncConfig {
+
+    @Value("${Camera.num}")
+    private Integer maxPoolSize;
+
+    @Bean(name = "videoTaskExecutor")
+    public Executor videoExecutor() {
+        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+        executor.setCorePoolSize(Runtime.getRuntime().availableProcessors());
+        executor.setMaxPoolSize(maxPoolSize);
+        executor.setThreadFactory(new CustomThreadFactory("Video-Processor-"));
+        executor.setRejectedExecutionHandler(new BlockingPolicy(30, TimeUnit.SECONDS));
+        executor.initialize();
+        return executor;
+    }
+
+    // 自定义线程工厂
+    static class CustomThreadFactory implements ThreadFactory {
+        private final AtomicInteger counter = new AtomicInteger(0);
+        private final String namePrefix;
+
+        CustomThreadFactory(String namePrefix) {
+            this.namePrefix = namePrefix;
+        }
+
+        public Thread newThread(Runnable r) {
+            return new Thread(r, namePrefix + counter.getAndIncrement());
+        }
+    }
+
+    // 带超时的阻塞策略
+    static class BlockingPolicy implements RejectedExecutionHandler {
+        private final long timeout;
+        private final TimeUnit unit;
+
+        BlockingPolicy(long timeout, TimeUnit unit) {
+            this.timeout = timeout;
+            this.unit = unit;
+        }
+
+        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
+            try {
+                if (!executor.getQueue().offer(r, timeout, unit)) {
+                    throw new RejectedExecutionException("Timeout " + timeout + " " + unit);
+                }
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                throw new RejectedExecutionException("Interrupted", e);
+            }
+        }
+    }
+
+}

+ 48 - 0
src/main/java/com/example/unusualsounds/project/vox/config/SegmentRepairService.java

@@ -0,0 +1,48 @@
+package com.example.unusualsounds.project.vox.config;
+
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Async;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.concurrent.TimeUnit;
+
+@Slf4j
+@Data
+public class SegmentRepairService {
+    @Async
+    public void repairSegment(Path segmentPath) {
+        Path tempPath = segmentPath.resolveSibling("temp_" + segmentPath.getFileName());
+
+        try {
+            // 使用FFmpeg重新封装
+            Process process = new ProcessBuilder(
+                    "D:\\tmp\\data\\vox",
+                    "-i", segmentPath.toString(),
+                    "-c", "copy",
+                    "-f", "mp4",
+                    tempPath.toString()
+            ).start();
+
+            if (process.waitFor(30, TimeUnit.SECONDS)) {
+                if (process.exitValue() == 0) {
+                    Files.move(tempPath, segmentPath, StandardCopyOption.REPLACE_EXISTING);
+                    log.info("Repaired segment: {}", segmentPath);
+                }
+            }
+        } catch (Exception e) {
+            log.error("Failed to repair segment", e);
+        } finally {
+            deleteQuietly(tempPath);
+        }
+    }
+
+    private void deleteQuietly(Path path) {
+        try {
+            Files.deleteIfExists(path);
+        } catch (IOException ignored) {}
+    }
+}

+ 289 - 0
src/main/java/com/example/unusualsounds/project/vox/config/VideoTaskManager.java

@@ -0,0 +1,289 @@
+package com.example.unusualsounds.project.vox.config;
+
+import com.example.unusualsounds.common.utils.FileToMultipartFile;
+import com.example.unusualsounds.common.utils.RedisUtils;
+import com.example.unusualsounds.framework.minio.MinioUtil;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.*;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.RestTemplate;
+
+import java.io.*;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+
+@Component
+@Slf4j
+public class VideoTaskManager {
+
+    @Value("${vox.video-dir}")
+    private String videoDir;
+
+    @Value("${minio.source-dir}")
+    private String sourceDir;
+
+    @Value("${vox.post-analysis-url}")
+    private String postAnalysisUrl;
+
+    @Autowired
+    private  MinioUtil minioUtil;
+
+    @Autowired
+    private RestTemplate restTemplate;
+
+    private final ConcurrentMap<String, VideoTask> taskMap = new ConcurrentHashMap<>();
+
+
+    public boolean startTask(String streamId,String deviceName,String skillID, String rtspUrl,String skillName,String openUuid) {
+        String compositeKey = streamId + "_" + skillName;
+        return taskMap.computeIfAbsent(compositeKey, key -> {
+            VideoTask task = new VideoTask(key,rtspUrl,skillName, skillID,deviceName,openUuid);
+            task.start();
+            return task;
+        }) != null;
+
+    }
+
+    public boolean stopTask(String streamIdSkillName) throws InterruptedException {
+        //删除redis缓存
+        RedisUtils.delete(streamIdSkillName);
+        System.out.println("删除缓存:" + streamIdSkillName);
+        VideoTask task = taskMap.remove(streamIdSkillName); // 直接使用复合键
+        if (task != null) {
+            try {
+                task.stop();
+                return true;
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                log.error("停止任务被中断: {}", streamIdSkillName);
+            } catch (Exception e) {
+                log.error("停止任务异常: {}", streamIdSkillName, e);
+            }
+        }
+        return false;
+    }
+
+    public List<TaskStatus> listTasks() {
+
+        return taskMap.entrySet().stream()
+                .map(e -> new TaskStatus(e.getKey(), e.getValue().getRtspUrl(),e.getValue().openUuid, e.getValue().isRunning()))
+                .collect(Collectors.toList());
+    }
+
+    @Data
+    @AllArgsConstructor
+    public static class TaskStatus {
+        private String streamId;
+        private String rtspUrl;
+        private String openUuid;
+        private boolean isRunning;
+    }
+
+    @Data
+    private class VideoTask {
+        //streamId = streamId+ skillName
+        private final String streamId;
+        private final String rtspUrl;
+        private String skillName;
+        private String skillId;
+        private String deviceName;
+        private String openUuid;
+        private Process process;
+        private Future<?> future;
+
+        VideoTask(String streamId, String rtspUrl,String skillName,String skillId, String deviceName,String openUuid) {
+            this.streamId = streamId;
+            this.rtspUrl = rtspUrl;
+            this.skillName=skillName;
+            this.skillId=skillId;
+            this.deviceName=deviceName;
+            this.openUuid= openUuid;
+        }
+
+        void start() {
+            List<String> command = Arrays.asList(
+                    "D:\\file\\ffmpeg\\ffmpeg.exe",
+                    "-rtsp_transport", "tcp",
+                    "-i", rtspUrl,
+                    "-vn",
+                    "-c", "copy",
+                    "-f", "segment",
+                    "-segment_time", "100",
+                    "-segment_format", "mp4",
+                    "-reset_timestamps", "1",
+                    "-force_key_frames", "expr:gte(t,n_floor(t/100)*100)",
+                    "-write_empty_segments", "1",
+                    "-segment_atclocktime", "1",
+                    "-strftime", "1",
+                    videoDir+streamId+"-%Y-%m-%d_%H-%M-%S"+".mp4"
+//                    "D:\\tmp\\data\\vox\\"+streamId+".mp4"
+            );
+
+            ProcessBuilder pb = new ProcessBuilder(command)
+                    .redirectErrorStream(true);
+
+            try {
+                this.process = pb.start();
+                this.future = Executors.newSingleThreadExecutor().submit(this::monitorProcess);
+            } catch (IOException e) {
+                throw new RuntimeException("无法启动FFmpeg进程", e);
+            }
+        }
+
+        void stop() throws InterruptedException {
+
+
+            if (future != null && !future.isDone()) {
+                future.cancel(true);
+            }
+            if (process != null && process.isAlive()) {
+                // 给FFmpeg进程发送Q信号,完成当前操作并退出。
+                OutputStream os = process.getOutputStream();
+                PrintWriter writer = new PrintWriter(os, true);
+                writer.println("q");  // 发送'q'命令给FFmpeg
+
+                int exitCode = process.waitFor();  // 等待FFmpeg进程正常结束
+                log.info("[{}] 进程退出状态: {}", streamId, exitCode);
+
+                // 如果进程没有在合理时间内响应,可以考虑强制销毁
+                if (exitCode != 0) {
+                    process.destroyForcibly();
+                    log.warn("[{}] 强制结束了未正常退出的进程", streamId);
+                } else {
+                    log.info("[{}] 进程顺利结束", streamId);
+                }
+            }
+        }
+
+
+        private Void monitorProcess() {
+            try (BufferedReader reader = new BufferedReader(
+                    new InputStreamReader(process.getInputStream()))) {
+                String line;
+                Pattern compile = Pattern.compile("Opening '([^']+\\.mp4)'");
+                while ((line = reader.readLine()) != null) {
+                    if (Thread.interrupted()) {
+                        log.info("[{}] 进程监控被中断", streamId);
+                        return null;
+                    };
+                    //[segment @ 000002bcb71e5680] Opening 'D:\tmp\data\vox\output-2025-02-14_17-53-20499aec0d-0579-4ce8-8a19-0cdea2c8a648.mp4' for writing
+//                    log.info("[{}]FFmpeg: {}", streamId, line);
+                    Matcher matcher = compile.matcher(line);
+                    if (matcher.find()) {
+                        String filePath = matcher.group(1).trim();
+                        log.info("[{}] 新的视频文件已生成: {}", streamId, filePath);
+                        //上传文件、插入数据表告知算法解析;
+                        uploadFile(filePath,skillName,streamId,skillId,deviceName);
+                    }
+
+                }
+                    int exitCode = process.waitFor();
+                    log.info("[{}] 进程退出,显示代码 {}", streamId, exitCode);
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt(); // 重新设置中断状态
+                log.warn("[{}] 进程监控被中断", streamId, e);
+            }
+            catch (IOException e) {
+                log.error("[{}] 进程监控失败", streamId, e);
+            } finally {
+                cleanup();
+                taskMap.remove(streamId);
+            }
+            return null;
+        }
+
+        private void cleanup() {
+            // 清理临时文件
+        }
+
+        boolean isRunning() {
+            return process != null && process.isAlive();
+        }
+
+    }
+
+    /**
+     * 上传切分的文件
+     * @param filePath
+     * @return
+     */
+    private CompletableFuture<Void> uploadFile(String filePath,String skillName,String streamId,String skillId, String deviceName){
+
+        return CompletableFuture.runAsync(()->{
+            try {
+                while (true){
+                //检查文件是否完成
+                    List<String> checkVoxFinish = Arrays.asList(
+                            "D:\\file\\ffmpeg\\ffmpeg.exe",
+                            "-v","error",
+                            "-i", filePath,
+                            "-f", "null",
+                            "-"
+                    );
+                    Process process = new ProcessBuilder(checkVoxFinish).start();
+                    BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
+                    StringBuilder errorOutput = new StringBuilder();
+
+                    String line;
+                    while ((line = reader.readLine()) != null) {
+                        errorOutput.append(line).append("\n");
+                    }
+                    int exitCode = process.waitFor();
+                    if (exitCode != 0 || !errorOutput.toString().trim().isEmpty()) {
+                        log.error("checkVoxFinish:[{}]文件未完成",filePath);
+                        //重置状态errorOutput状态
+                        errorOutput.delete(0, errorOutput.length());
+                        //等待1分钟后再次检查
+                        Thread.sleep(60000);
+                    }else {
+                        //上传文件
+                        File file = new File(filePath);
+                        FileToMultipartFile fileToMultipartFile = new FileToMultipartFile(file);
+                        //上传文件
+                        String upload = minioUtil.upload(fileToMultipartFile,sourceDir);
+                        log.info("上传文件:{}",upload);
+
+                        //发送消息
+                        HashMap<String, Object> hashMap = new HashMap<>();
+                        hashMap.put("filePath",upload);
+                        hashMap.put("skillName",skillName);
+                        hashMap.put("streamId",streamId);
+                        hashMap.put("deviceName",deviceName);
+                        hashMap.put("skillId",skillId);
+
+
+                        HttpHeaders httpHeaders = new HttpHeaders();
+                        httpHeaders.setContentType(MediaType.APPLICATION_JSON);
+                        HttpEntity<Map<String, Object>> mapHttpEntity = new HttpEntity<Map<String, Object>>(hashMap, httpHeaders);
+                        ResponseEntity<String> stringResponseEntity = restTemplate.postForEntity(postAnalysisUrl, mapHttpEntity, String.class);
+                        if (stringResponseEntity.getStatusCode() == HttpStatus.OK){
+                            log.info("通知算法成功");
+                        }
+
+                        break;
+                    }
+                }
+            } catch (InterruptedException e) {
+                throw new RuntimeException(e);
+            } catch (IOException e) {
+                throw new RuntimeException(e);
+            }
+        });
+
+
+
+    }
+
+
+}

+ 110 - 0
src/main/java/com/example/unusualsounds/project/vox/controller/CheckVoxController.java

@@ -0,0 +1,110 @@
+package com.example.unusualsounds.project.vox.controller;
+
+
+import com.example.unusualsounds.common.utils.UUID;
+import com.example.unusualsounds.project.vox.config.VideoTaskManager;
+import com.example.unusualsounds.project.vox.service.impl.AlarmsServiceImpl;
+import com.example.unusualsounds.project.vox.service.impl.VideoServiceImpl;
+import com.example.unusualsounds.project.vox.service.impl.VoiceTicketLogsServiceImpl;
+import com.example.unusualsounds.project.vox.service.impl.VoxServiceImpl;
+import com.example.unusualsounds.web.entity.AjaxResult;
+import com.example.unusualsounds.web.jsonobj.CameraDTO;
+import com.example.unusualsounds.web.jsonobj.Device;
+import com.example.unusualsounds.web.jsonobj.Skill;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+
+import java.util.HashMap;
+import java.util.List;
+
+@RestController
+@Tag(name = "vox 异音模块")
+@Slf4j
+@RequestMapping("/vox")
+public class CheckVoxController {
+
+
+    @Autowired
+    private VoxServiceImpl voxServiceImpl;
+    @Autowired
+    private VideoServiceImpl videoServiceImpl;
+    @Autowired
+    private VideoTaskManager videoTaskManager;
+
+    @Autowired
+    private AlarmsServiceImpl alarmsServiceImpl;
+
+    @Autowired
+    private VoiceTicketLogsServiceImpl voiceTicketLogsServiceImpl;
+
+
+    @Operation(summary = "算法启停接口")
+    @PostMapping("/start_stop")
+    public AjaxResult startStopVox(@RequestBody Device device) throws InterruptedException {
+
+        String openUuid = UUID.generateUUIDWithoutHyphens();
+        //保存启停接口的设备信息到数据库,便于后续定时任务调用
+        voiceTicketLogsServiceImpl.saveStartStop(device,openUuid);
+
+
+        //校验设备是否开启(包含传感器、摄像头),当前只做摄像头校验
+        CameraDTO camera  = voxServiceImpl.checkDevice(device.getDeviceId());
+
+        //-----测试(云环境的是mp4文件,不是rtsp流)
+        camera.setSrcURL("rtsp://192.168.1.120:8554/video");
+        //-----测试
+
+
+        if(camera == null || "offline".equals(camera.getStatus()) || "error".equals(camera.getStatus())){
+            return AjaxResult.error("设备离线或出错");
+        }else{
+            Skill skill = device.getSkills().get(0);
+            boolean  skillOpen = device.getSkills().get(0).isOpen();
+            if(skillOpen){
+
+                //添加校验视频流地址是否有效,无效则不启动算法
+                boolean checkStream = videoServiceImpl.checkStream(camera);
+                log.info("视频流地址校验结果:{}",checkStream);
+                if(!checkStream){
+                    return AjaxResult.error("视频流地址无效");
+                }
+
+                //开启处理视频流和接入算法
+                videoServiceImpl.handlesStream(device.getDeviceId(),camera.getSrcURL(),skill,camera.getName(),openUuid);
+                return AjaxResult.success("算法已启动");
+            }else{
+                //关闭处理视频流和接入算法
+                videoTaskManager.stopTask(device.getDeviceId()+"_"+skill.getName());
+                return AjaxResult.success("算法已关闭");
+            }
+
+        }
+
+    }
+
+
+
+    @Operation(summary = "返回vox分析结果")
+    @PostMapping("/resultAnalysis")
+    public AjaxResult resultVoxAnalysis(@RequestBody HashMap<String,String> resultAnalysis){
+        boolean saveResult = alarmsServiceImpl.saveResultAnalysis(resultAnalysis);
+        return AjaxResult.success("测试成功");
+
+    }
+
+
+    @Operation(summary = "列出当前工单正在运行的任务")
+    @GetMapping("/tasks")
+    public List<VideoTaskManager.TaskStatus> listActiveTasks() {
+        return videoTaskManager.listTasks();
+    }
+
+
+
+
+
+}

+ 52 - 0
src/main/java/com/example/unusualsounds/project/vox/controller/OrganizationController.java

@@ -0,0 +1,52 @@
+package com.example.unusualsounds.project.vox.controller;
+
+
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONObject;
+import com.example.unusualsounds.project.vox.entity.Skills;
+import com.example.unusualsounds.project.vox.service.EdgeOrganizationService;
+import com.example.unusualsounds.project.vox.service.impl.EdgeOrganizationServiceImpl;
+import com.example.unusualsounds.project.vox.service.impl.SkillsServiceImpl;
+import com.example.unusualsounds.web.entity.AjaxResult;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.HashMap;
+import java.util.List;
+
+@RestController
+@RequestMapping("/organizations")
+@Tag(name = "组织管理")
+public class OrganizationController {
+
+    @Autowired
+    private EdgeOrganizationServiceImpl edgeOrganizationServiceImpl;
+
+    @Autowired
+    private SkillsServiceImpl skillsServiceImpl;
+
+
+    @Operation(summary = "获取组织列表")
+    @GetMapping("/orgObj")
+    public AjaxResult getOrganizations(){
+        String result = edgeOrganizationServiceImpl.getOrganizationObj();
+        Object parse = JSON.parse(result);
+        return AjaxResult.success(parse);
+    }
+
+
+
+
+    @Operation(summary = "获取技能信息")
+    @PostMapping("/skills")
+    public AjaxResult getSkills(@RequestBody List<String> skillsName){
+
+        List<Skills> skills = skillsServiceImpl.getSkillByName(skillsName);
+
+        return AjaxResult.success(skills);
+    }
+
+
+}

+ 96 - 0
src/main/java/com/example/unusualsounds/project/vox/entity/AlarmReports.java

@@ -0,0 +1,96 @@
+package com.example.unusualsounds.project.vox.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 java.io.Serializable;
+import java.util.Date;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 告警处理表
+ * @TableName alarm_reports
+ */
+@TableName(value ="alarm_reports")
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class AlarmReports implements Serializable {
+    /**
+     * 自增id
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 告警处理id
+     */
+    @TableField(value = "uuid")
+    private String uuid;
+
+    /**
+     * 创建时间
+     */
+    @TableField(value = "created_at")
+    private Date createdAt;
+
+    /**
+     * 更新时间
+     */
+    @TableField(value = "updated_at")
+    private Date updatedAt;
+
+    /**
+     * 删除时间
+     */
+    @TableField(value = "deleted_at")
+    private Date deletedAt;
+
+    /**
+     * 部门ID
+     * 查询
+     */
+    @TableField(value = "dept_uuid")
+    private String deptUuid;
+
+    /**
+     * 组织ID
+     * 查询
+     */
+    @TableField(value = "org_uuid")
+    private String orgUuid;
+
+    /**
+     * 告警处理名称
+     *
+     */
+    @TableField(value = "name")
+    private String name;
+
+    /**
+     * 告警处理描述
+     */
+    @TableField(value = "description")
+    private String description;
+
+    /**
+     * 告警处理展示状态,对应alarms表中display_status
+     * 默认pending
+     */
+    @TableField(value = "display_status")
+    private String displayStatus;
+
+    /**
+     * 告警ID
+     */
+    @TableField(value = "alarm_uuid")
+    private String alarmUuid;
+
+    @TableField(exist = false)
+    private static final long serialVersionUID = 1L;
+
+}

+ 527 - 0
src/main/java/com/example/unusualsounds/project/vox/entity/Alarms.java

@@ -0,0 +1,527 @@
+package com.example.unusualsounds.project.vox.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 java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.Date;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 告警信息表
+ * @TableName alarms
+ */
+@TableName(value ="alarms")
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class Alarms implements Serializable {
+    /**
+     * 自增id
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 告警id
+     */
+    @TableField(value = "uuid")
+    private String uuid;
+
+    /**
+     * 创建时间
+     */
+    @TableField(value = "created_at")
+    private Date createdAt;
+
+    /**
+     * 更新时间
+     */
+    @TableField(value = "updated_at")
+    private Date updatedAt;
+
+    /**
+     * 删除时间
+     */
+    @TableField(value = "deleted_at")
+    private Date deletedAt;
+
+    /**
+     * 部门ID
+     */
+    @TableField(value = "dept_uuid")
+    private String deptUuid;
+
+    /**
+     * 组织ID
+     */
+    @TableField(value = "org_uuid")
+    private String orgUuid;
+
+    /**
+     * 告警名称(设备异音、人声呼救)
+     */
+    @TableField(value = "name")
+    private String name;
+
+    /**
+     * 告警描述 (摄像头信息+名称等简单拼接)
+     */
+    @TableField(value = "description")
+    private String description;
+
+    /**
+     * created, reported, processing, finished, mistaken
+     * 默认created
+     */
+    @TableField(value = "status")
+    private String status;
+
+    /**
+     * pending, processing, finished, mistaken
+     * 默认pending
+     */
+    @TableField(value = "display_status")
+    private String displayStatus;
+
+    /**
+     * 告警等级
+     * 算法回传
+     */
+    @TableField(value = "level")
+    private Integer level;
+
+    /**
+     * 图片链接
+     * minio中固定的图片url链接
+     */
+    @TableField(value = "image_url")
+    private String imageUrl;
+
+    /**
+     * 缩略图url
+     *  minio中固定的图片url链接
+     */
+    @TableField(value = "thumbnail_url")
+    private String thumbnailUrl;
+
+    /**
+     * 处理后的图片链接
+     * 无,随机填值
+     */
+    @TableField(value = "image_draw_url")
+    private String imageDrawUrl;
+
+    /**
+     * 视频片段链接
+     * minio中固定的音频url链接
+     */
+    @TableField(value = "video_url")
+    private String videoUrl;
+
+    /**
+     * 设备ID
+     * 接口获取
+     */
+    @TableField(value = "device_uuid")
+    private String deviceUuid;
+
+    /**
+     * 技能ID
+     * 默认,异音检测
+     */
+    @TableField(value = "skill_uuid")
+    private String skillUuid;
+
+    /**
+     * 模型的标注数据
+     * 无
+     */
+    @TableField(value = "annotations_json")
+    private String annotationsJson;
+
+    /**
+     * 剪切的图片列表
+     * 无
+     */
+    @TableField(value = "snapshots_json")
+    private String snapshotsJson;
+
+    /**
+     * 用户id
+     * 无
+     */
+    @TableField(value = "user_uuid")
+    private String userUuid;
+
+    /**
+     * 检测区域
+     * 无
+     */
+    @TableField(value = "rois_json")
+    private String roisJson;
+
+    /**
+     * 检测区域UUID
+     * 无
+     */
+    @TableField(value = "rois_uuid")
+    private String roisUuid;
+
+    /**
+     * 追踪ID
+     * 无
+     */
+    @TableField(value = "track_uuid")
+    private String trackUuid;
+
+    /**
+     * 传感器设备告警数据
+     * 无
+     */
+    @TableField(value = "figures")
+    private String figures;
+
+    /**
+     * 传感器告警或者是摄像头告警,camera or sensor
+     * 视频,camera ,传感器 sensor
+     */
+    @TableField(value = "type")
+    private String type;
+
+    /**
+     * 预警触发类型 自动(默认):auto 手动: manual
+     */
+    @TableField(value = "trigger_type")
+    private String triggerType;
+
+    /**
+     * 机器人ID
+     * 无
+     */
+    @TableField(value = "robot_uuid")
+    private String robotUuid;
+
+    /**
+     * 预警位置
+     * 无
+     */
+    @TableField(value = "location")
+    private Double location;
+
+    /**
+     * 预警原因
+     * 无
+     */
+    @TableField(value = "reason")
+    private String reason;
+
+    /**
+     * 是否已消警
+     * 默认0 未消警
+     */
+    @TableField(value = "is_canceled")
+    private Integer isCanceled;
+
+    /**
+     * 是否已升级
+     * 无
+     */
+    @TableField(value = "is_upgraded")
+    private Integer isUpgraded;
+
+    /**
+     * 消警时间
+     * 无
+     */
+    @TableField(value = "cancel_alarm_time")
+    private Integer cancelAlarmTime;
+
+    /**
+     * 数据来源 cloud/node/aiBox
+     * 默认cloud
+     */
+    @TableField(value = "data_source")
+    private String dataSource;
+
+    /**
+     * 云台垂直角度
+     * 无
+     */
+    @TableField(value = "ptz_pan_angle")
+    private BigDecimal ptzPanAngle;
+
+    /**
+     * 云台水平角度
+     * 无
+     */
+    @TableField(value = "ptz_title_angle")
+    private BigDecimal ptzTitleAngle;
+
+    /**
+     * 是否为表计告警
+     * 无
+     */
+    @TableField(value = "meter")
+    private Integer meter;
+
+    /**
+     * 告警组id
+     * 无
+     */
+    @TableField(value = "alarm_group_id")
+    private String alarmGroupId;
+
+    /**
+     * 点位id
+     * 无
+     */
+    @TableField(value = "spot_uuid")
+    private String spotUuid;
+
+    /**
+     * 是否已发布
+     * 无
+     */
+    @TableField(value = "is_published")
+    private Integer isPublished;
+
+    /**
+     * 是否被收藏
+     * 无
+     */
+    @TableField(value = "is_collected")
+    private Integer isCollected;
+
+    /**
+     * 点位类型
+     * 无
+     */
+    @TableField(value = "ponding_type")
+    private String pondingType;
+
+    /**
+     * invalid.无效;  efficient.有效;
+     * 无
+     */
+    @TableField(value = "labels")
+    private String labels;
+
+    /**
+     * 大模型复盘0.否;1.大模型复盘;2.人工审核;4.大模型
+     * 无
+     */
+    @TableField(value = "ai_filter")
+    private Integer aiFilter;
+
+    /**
+     * 智能过滤处理人
+     * 无
+     */
+    @TableField(value = "process_user")
+    private String processUser;
+
+    /**
+     * 传感器设置的阈值
+     * 无
+     */
+    @TableField(value = "thresholds")
+    private String thresholds;
+
+    /**
+     * 多模态技能ID
+     * 无
+     */
+    @TableField(value = "multi_skill_uuid")
+    private String multiSkillUuid;
+
+    /**
+     * 过滤任务ID
+     * 无
+     */
+    @TableField(value = "filter_task_uuid")
+    private String filterTaskUuid;
+
+    /**
+     * 云端分配给边缘的唯一uuid
+     * 查询
+     */
+    @TableField(value = "edge_uuid")
+    private String edgeUuid;
+
+    @TableField(exist = false)
+    private static final long serialVersionUID = 1L;
+
+    private Alarms(Builder builder){
+        this.id = builder.id;
+        this.uuid = builder.uuid;
+        this.createdAt = builder.createdAt;
+        this.updatedAt = builder.updatedAt;
+        this.deletedAt = builder.deletedAt;
+        this.deptUuid = builder.deptUuid;
+        this.orgUuid = builder.orgUuid;
+        this.name = builder.name;
+        this.description = builder.description;
+        this.status = builder.status;
+        this.displayStatus = builder.displayStatus;
+        this.level = builder.level;
+        this.imageUrl = builder.imageUrl;
+        this.thumbnailUrl = builder.thumbnailUrl;
+        this.imageDrawUrl = builder.imageDrawUrl;
+        this.videoUrl = builder.videoUrl;
+        this.deviceUuid = builder.deviceUuid;
+        this.skillUuid = builder.skillUuid;
+        this.annotationsJson = builder.annotationsJson;
+        this.snapshotsJson = builder.snapshotsJson;
+        this.userUuid = builder.userUuid;
+        this.roisJson = builder.roisJson;
+        this.roisUuid = builder.roisUuid;
+        this.trackUuid = builder.trackUuid;
+        this.figures = builder.figures;
+        this.type = builder.type;
+        this.triggerType = builder.triggerType;
+        this.robotUuid = builder.robotUuid;
+        this.location = builder.location;
+        this.reason = builder.reason;
+        this.isCanceled = builder.isCanceled;
+        this.isUpgraded = builder.isUpgraded;
+        this.cancelAlarmTime = builder.cancelAlarmTime;
+        this.dataSource = builder.dataSource;
+        this.ptzPanAngle = builder.ptzPanAngle;
+        this.ptzTitleAngle = builder.ptzTitleAngle;
+        this.meter = builder.meter;
+        this.alarmGroupId = builder.alarmGroupId;
+        this.spotUuid = builder.spotUuid;
+        this.isPublished = builder.isPublished;
+        this.isCollected = builder.isCollected;
+        this.pondingType = builder.pondingType;
+        this.labels = builder.labels;
+        this.aiFilter = builder.aiFilter;
+        this.processUser = builder.processUser;
+        this.thresholds = builder.thresholds;
+        this.multiSkillUuid = builder.multiSkillUuid;
+        this.filterTaskUuid = builder.filterTaskUuid;
+        this.edgeUuid = builder.edgeUuid;
+    }
+
+
+    /**
+     * 默认值构建器
+     */
+    public static class Builder{
+        private Long id = null;
+        private String uuid = null;
+        private Date createdAt = null;
+        private Date updatedAt = null;
+        private Date deletedAt = null;
+        private String deptUuid = "";
+        private String orgUuid = "";
+        private String name = "";
+        private String description = "";
+        private String status = "created";
+        private String displayStatus = "pending";
+        private Integer level = 4;
+        private String imageUrl = "s3://windmill/store/452b784fe34d42b1b3700b6aeb648e1e/alarm-images/cloud/2025-02-14/681082ce-31e3-4dcf-9393-cbc67e9c4749.jpeg";
+        private String thumbnailUrl = "";
+        private String imageDrawUrl = "";
+        private String videoUrl = "";
+        private String deviceUuid = "";
+        private String skillUuid = ""; // 默认值
+        private String annotationsJson = "";
+        private String snapshotsJson = "";
+        private String userUuid = "";
+        private String roisJson = "";
+        private String roisUuid = "";
+        private String trackUuid = "";
+        private String figures = "";
+        private String type = "voice";
+        private String triggerType = "auto";
+        private String robotUuid = "";
+        private Double location = 0.0;
+        private String reason = "";
+        private Integer isCanceled = 0; // 默认值
+        private Integer isUpgraded = 0;
+        private Integer cancelAlarmTime = 0;
+        private String dataSource = "cloud"; // 默认值
+        private BigDecimal ptzPanAngle = BigDecimal.valueOf(0.00);
+        private BigDecimal ptzTitleAngle = BigDecimal.valueOf(0.00);
+        private Integer meter = 0;
+        private String alarmGroupId = "";
+        private String spotUuid = "";
+        private Integer isPublished = 0;
+        private Integer isCollected = 0;
+        private String pondingType = "";
+        private String labels = "";
+        private Integer aiFilter = 0;
+        private String processUser = "";
+        private String thresholds = null;
+        private String multiSkillUuid = "";
+        private String filterTaskUuid = "";
+        private String edgeUuid = "";
+
+        public Builder setId(Long id) { this.id = id; return this; }
+        public Builder setUuid(String uuid) { this.uuid = uuid; return this; }
+        public Builder setCreatedAt(Date createdAt) { this.createdAt = createdAt; return this; }
+        public Builder setUpdatedAt(Date updatedAt) { this.updatedAt = updatedAt; return this; }
+        public Builder setDeletedAt(Date deletedAt) { this.deletedAt = deletedAt; return this; }
+        public Builder setDeptUuid(String deptUuid) { this.deptUuid = deptUuid; return this; }
+        public Builder setOrgUuid(String orgUuid) { this.orgUuid = orgUuid; return this; }
+        public Builder setName(String name) { this.name = name; return this; }
+        public Builder setDescription(String description) { this.description = description; return this; }
+        public Builder setStatus(String status) { this.status = status; return this; }
+        public Builder setDisplayStatus(String displayStatus) { this.displayStatus = displayStatus; return this; }
+        public Builder setLevel(Integer level) { this.level = level; return this; }
+        public Builder setImageUrl(String imageUrl) { this.imageUrl = imageUrl; return this; }
+        public Builder setThumbnailUrl(String thumbnailUrl) { this.thumbnailUrl = thumbnailUrl; return this; }
+        public Builder setImageDrawUrl(String imageDrawUrl) { this.imageDrawUrl = imageDrawUrl; return this; }
+        public Builder setVideoUrl(String videoUrl) { this.videoUrl = videoUrl; return this; }
+        public Builder setDeviceUuid(String deviceUuid) { this.deviceUuid = deviceUuid; return this; }
+        public Builder setSkillUuid(String skillUuid) { this.skillUuid = skillUuid; return this; }
+        public Builder setAnnotationsJson(String annotationsJson) { this.annotationsJson = annotationsJson; return this; }
+        public Builder setSnapshotsJson(String snapshotsJson) { this.snapshotsJson = snapshotsJson; return this; }
+        public Builder setUserUuid(String userUuid) { this.userUuid = userUuid; return this; }
+        public Builder setRoisJson(String roisJson) { this.roisJson = roisJson; return this; }
+        public Builder setRoisUuid(String roisUuid) { this.roisUuid = roisUuid; return this; }
+        public Builder setTrackUuid(String trackUuid) { this.trackUuid = trackUuid; return this; }
+        public Builder setFigures(String figures) { this.figures = figures; return this; }
+        public Builder setType(String type) { this.type = type; return this; }
+        public Builder setTriggerType(String triggerType) { this.triggerType = triggerType; return this; }
+        public Builder setRobotUuid(String robotUuid) { this.robotUuid = robotUuid; return this; }
+        public Builder setLocation(Double location) { this.location = location; return this; }
+        public Builder setReason(String reason) { this.reason = reason; return this; }
+        public Builder setIsCanceled(Integer isCanceled) { this.isCanceled = isCanceled; return this; }
+        public Builder setIsUpgraded(Integer isUpgraded) { this.isUpgraded = isUpgraded; return this; }
+        public Builder setCancelAlarmTime(Integer cancelAlarmTime) { this.cancelAlarmTime = cancelAlarmTime; return this; }
+        public Builder setDataSource(String dataSource) { this.dataSource = dataSource; return this; }
+        public Builder setPtzPanAngle(BigDecimal ptzPanAngle) { this.ptzPanAngle = ptzPanAngle; return this; }
+        public Builder setPtzTitleAngle(BigDecimal ptzTitleAngle) { this.ptzTitleAngle = ptzTitleAngle; return this; }
+        public Builder setMeter(Integer meter) { this.meter = meter; return this; }
+        public Builder setAlarmGroupId(String alarmGroupId) { this.alarmGroupId = alarmGroupId; return this; }
+        public Builder setSpotUuid(String spotUuid) { this.spotUuid = spotUuid; return this; }
+        public Builder setIsPublished(Integer isPublished) { this.isPublished = isPublished; return this; }
+        public Builder setIsCollected(Integer isCollected) { this.isCollected = isCollected; return this; }
+        public Builder setPondingType(String pondingType) { this.pondingType = pondingType; return this; }
+        public Builder setLabels(String labels) { this.labels = labels; return this; }
+        public Builder setAiFilter(Integer aiFilter) { this.aiFilter = aiFilter; return this; }
+        public Builder setProcessUser(String processUser) { this.processUser = processUser; return this; }
+        public Builder setThresholds(String thresholds) { this.thresholds = thresholds; return this; }
+        public Builder setMultiSkillUuid(String multiSkillUuid) { this.multiSkillUuid = multiSkillUuid; return this; }
+        public Builder setFilterTaskUuid(String filterTaskUuid) { this.filterTaskUuid = filterTaskUuid; return this; }
+        public Builder setEdgeUuid(String edgeUuid) { this.edgeUuid = edgeUuid; return this; }
+
+
+
+        public Alarms build(){
+            return new Alarms(this);
+        }
+
+    };
+
+
+
+}

+ 63 - 0
src/main/java/com/example/unusualsounds/project/vox/entity/EdgeOrganization.java

@@ -0,0 +1,63 @@
+package com.example.unusualsounds.project.vox.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 java.io.Serializable;
+import java.util.Date;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 边缘组织表
+ * @TableName edge_organization
+ */
+@TableName(value ="edge_organization")
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class EdgeOrganization implements Serializable {
+    /**
+     * 自增id
+     */
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 创建时间
+     */
+    private Date createdAt;
+
+    /**
+     * 更新时间
+     */
+    private Date updatedAt;
+
+    /**
+     * 删除时间
+     */
+    private Date deletedAt;
+
+    /**
+     * 部门ID
+     */
+    private String deptUuid;
+
+    /**
+     * 云端分配给边缘的唯一组织uuid
+     */
+    private String orgUuid;
+
+    /**
+     * 云端分配给边缘的唯一uuid
+     */
+    private String edgeUuid;
+
+    @TableField(exist = false)
+    private static final long serialVersionUID = 1L;
+
+
+}

+ 304 - 0
src/main/java/com/example/unusualsounds/project/vox/entity/Skills.java

@@ -0,0 +1,304 @@
+package com.example.unusualsounds.project.vox.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 java.io.Serializable;
+import java.util.Date;
+import lombok.Data;
+
+/**
+ * 技能信息表
+ * @TableName skills
+ */
+@TableName(value ="skills")
+@Data
+public class Skills implements Serializable {
+    /**
+     * 自增id
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 
+     */
+    @TableField(value = "uuid")
+    private String uuid;
+
+    /**
+     * 
+     */
+    @TableField(value = "created_at")
+    private Date createdAt;
+
+    /**
+     * 
+     */
+    @TableField(value = "updated_at")
+    private Date updatedAt;
+
+    /**
+     * 删除时间
+     */
+    @TableField(value = "deleted_at")
+    private Date deletedAt;
+
+    /**
+     * 
+     */
+    @TableField(value = "dept_uuid")
+    private String deptUuid;
+
+    /**
+     * 
+     */
+    @TableField(value = "org_uuid")
+    private String orgUuid;
+
+    /**
+     * 
+     */
+    @TableField(value = "name")
+    private String name;
+
+    /**
+     * 
+     */
+    @TableField(value = "object_name")
+    private String objectName;
+
+    /**
+     * 
+     */
+    @TableField(value = "released_version")
+    private Integer releasedVersion;
+
+    /**
+     * 
+     */
+    @TableField(value = "version_nums")
+    private Integer versionNums;
+
+    /**
+     * 
+     */
+    @TableField(value = "description")
+    private String description;
+
+    /**
+     * 
+     */
+    @TableField(value = "status")
+    private String status;
+
+    /**
+     * 
+     */
+    @TableField(value = "composition_json")
+    private String compositionJson;
+
+    /**
+     * 
+     */
+    @TableField(value = "version")
+    private Integer version;
+
+    /**
+     * 
+     */
+    @TableField(value = "user_uuid")
+    private String userUuid;
+
+    /**
+     * 
+     */
+    @TableField(value = "period")
+    private Integer period;
+
+    /**
+     * 
+     */
+    @TableField(value = "model_package_uuid")
+    private String modelPackageUuid;
+
+    /**
+     * 
+     */
+    @TableField(value = "serving_uuid")
+    private String servingUuid;
+
+    /**
+     * 
+     */
+    @TableField(value = "failed_reason")
+    private String failedReason;
+
+    /**
+     * 
+     */
+    @TableField(value = "skill_type")
+    private String skillType;
+
+    /**
+     * 
+     */
+    @TableField(value = "image_url")
+    private String imageUrl;
+
+    /**
+     * 
+     */
+    @TableField(value = "default_level")
+    private Integer defaultLevel;
+
+    /**
+     * 
+     */
+    @TableField(value = "merge_rule")
+    private String mergeRule;
+
+    /**
+     * 
+     */
+    @TableField(value = "robot_merge_rule")
+    private String robotMergeRule;
+
+    /**
+     * 
+     */
+    @TableField(value = "workspace_id")
+    private String workspaceId;
+
+    /**
+     * 
+     */
+    @TableField(value = "graph_name")
+    private String graphName;
+
+    /**
+     * 
+     */
+    @TableField(value = "graph_kind")
+    private String graphKind;
+
+    @TableField(exist = false)
+    private static final long serialVersionUID = 1L;
+
+    @Override
+    public boolean equals(Object that) {
+        if (this == that) {
+            return true;
+        }
+        if (that == null) {
+            return false;
+        }
+        if (getClass() != that.getClass()) {
+            return false;
+        }
+        Skills other = (Skills) that;
+        return (this.getId() == null ? other.getId() == null : this.getId().equals(other.getId()))
+            && (this.getUuid() == null ? other.getUuid() == null : this.getUuid().equals(other.getUuid()))
+            && (this.getCreatedAt() == null ? other.getCreatedAt() == null : this.getCreatedAt().equals(other.getCreatedAt()))
+            && (this.getUpdatedAt() == null ? other.getUpdatedAt() == null : this.getUpdatedAt().equals(other.getUpdatedAt()))
+            && (this.getDeletedAt() == null ? other.getDeletedAt() == null : this.getDeletedAt().equals(other.getDeletedAt()))
+            && (this.getDeptUuid() == null ? other.getDeptUuid() == null : this.getDeptUuid().equals(other.getDeptUuid()))
+            && (this.getOrgUuid() == null ? other.getOrgUuid() == null : this.getOrgUuid().equals(other.getOrgUuid()))
+            && (this.getName() == null ? other.getName() == null : this.getName().equals(other.getName()))
+            && (this.getObjectName() == null ? other.getObjectName() == null : this.getObjectName().equals(other.getObjectName()))
+            && (this.getReleasedVersion() == null ? other.getReleasedVersion() == null : this.getReleasedVersion().equals(other.getReleasedVersion()))
+            && (this.getVersionNums() == null ? other.getVersionNums() == null : this.getVersionNums().equals(other.getVersionNums()))
+            && (this.getDescription() == null ? other.getDescription() == null : this.getDescription().equals(other.getDescription()))
+            && (this.getStatus() == null ? other.getStatus() == null : this.getStatus().equals(other.getStatus()))
+            && (this.getCompositionJson() == null ? other.getCompositionJson() == null : this.getCompositionJson().equals(other.getCompositionJson()))
+            && (this.getVersion() == null ? other.getVersion() == null : this.getVersion().equals(other.getVersion()))
+            && (this.getUserUuid() == null ? other.getUserUuid() == null : this.getUserUuid().equals(other.getUserUuid()))
+            && (this.getPeriod() == null ? other.getPeriod() == null : this.getPeriod().equals(other.getPeriod()))
+            && (this.getModelPackageUuid() == null ? other.getModelPackageUuid() == null : this.getModelPackageUuid().equals(other.getModelPackageUuid()))
+            && (this.getServingUuid() == null ? other.getServingUuid() == null : this.getServingUuid().equals(other.getServingUuid()))
+            && (this.getFailedReason() == null ? other.getFailedReason() == null : this.getFailedReason().equals(other.getFailedReason()))
+            && (this.getSkillType() == null ? other.getSkillType() == null : this.getSkillType().equals(other.getSkillType()))
+            && (this.getImageUrl() == null ? other.getImageUrl() == null : this.getImageUrl().equals(other.getImageUrl()))
+            && (this.getDefaultLevel() == null ? other.getDefaultLevel() == null : this.getDefaultLevel().equals(other.getDefaultLevel()))
+            && (this.getMergeRule() == null ? other.getMergeRule() == null : this.getMergeRule().equals(other.getMergeRule()))
+            && (this.getRobotMergeRule() == null ? other.getRobotMergeRule() == null : this.getRobotMergeRule().equals(other.getRobotMergeRule()))
+            && (this.getWorkspaceId() == null ? other.getWorkspaceId() == null : this.getWorkspaceId().equals(other.getWorkspaceId()))
+            && (this.getGraphName() == null ? other.getGraphName() == null : this.getGraphName().equals(other.getGraphName()))
+            && (this.getGraphKind() == null ? other.getGraphKind() == null : this.getGraphKind().equals(other.getGraphKind()));
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((getId() == null) ? 0 : getId().hashCode());
+        result = prime * result + ((getUuid() == null) ? 0 : getUuid().hashCode());
+        result = prime * result + ((getCreatedAt() == null) ? 0 : getCreatedAt().hashCode());
+        result = prime * result + ((getUpdatedAt() == null) ? 0 : getUpdatedAt().hashCode());
+        result = prime * result + ((getDeletedAt() == null) ? 0 : getDeletedAt().hashCode());
+        result = prime * result + ((getDeptUuid() == null) ? 0 : getDeptUuid().hashCode());
+        result = prime * result + ((getOrgUuid() == null) ? 0 : getOrgUuid().hashCode());
+        result = prime * result + ((getName() == null) ? 0 : getName().hashCode());
+        result = prime * result + ((getObjectName() == null) ? 0 : getObjectName().hashCode());
+        result = prime * result + ((getReleasedVersion() == null) ? 0 : getReleasedVersion().hashCode());
+        result = prime * result + ((getVersionNums() == null) ? 0 : getVersionNums().hashCode());
+        result = prime * result + ((getDescription() == null) ? 0 : getDescription().hashCode());
+        result = prime * result + ((getStatus() == null) ? 0 : getStatus().hashCode());
+        result = prime * result + ((getCompositionJson() == null) ? 0 : getCompositionJson().hashCode());
+        result = prime * result + ((getVersion() == null) ? 0 : getVersion().hashCode());
+        result = prime * result + ((getUserUuid() == null) ? 0 : getUserUuid().hashCode());
+        result = prime * result + ((getPeriod() == null) ? 0 : getPeriod().hashCode());
+        result = prime * result + ((getModelPackageUuid() == null) ? 0 : getModelPackageUuid().hashCode());
+        result = prime * result + ((getServingUuid() == null) ? 0 : getServingUuid().hashCode());
+        result = prime * result + ((getFailedReason() == null) ? 0 : getFailedReason().hashCode());
+        result = prime * result + ((getSkillType() == null) ? 0 : getSkillType().hashCode());
+        result = prime * result + ((getImageUrl() == null) ? 0 : getImageUrl().hashCode());
+        result = prime * result + ((getDefaultLevel() == null) ? 0 : getDefaultLevel().hashCode());
+        result = prime * result + ((getMergeRule() == null) ? 0 : getMergeRule().hashCode());
+        result = prime * result + ((getRobotMergeRule() == null) ? 0 : getRobotMergeRule().hashCode());
+        result = prime * result + ((getWorkspaceId() == null) ? 0 : getWorkspaceId().hashCode());
+        result = prime * result + ((getGraphName() == null) ? 0 : getGraphName().hashCode());
+        result = prime * result + ((getGraphKind() == null) ? 0 : getGraphKind().hashCode());
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append(getClass().getSimpleName());
+        sb.append(" [");
+        sb.append("Hash = ").append(hashCode());
+        sb.append(", id=").append(id);
+        sb.append(", uuid=").append(uuid);
+        sb.append(", createdAt=").append(createdAt);
+        sb.append(", updatedAt=").append(updatedAt);
+        sb.append(", deletedAt=").append(deletedAt);
+        sb.append(", deptUuid=").append(deptUuid);
+        sb.append(", orgUuid=").append(orgUuid);
+        sb.append(", name=").append(name);
+        sb.append(", objectName=").append(objectName);
+        sb.append(", releasedVersion=").append(releasedVersion);
+        sb.append(", versionNums=").append(versionNums);
+        sb.append(", description=").append(description);
+        sb.append(", status=").append(status);
+        sb.append(", compositionJson=").append(compositionJson);
+        sb.append(", version=").append(version);
+        sb.append(", userUuid=").append(userUuid);
+        sb.append(", period=").append(period);
+        sb.append(", modelPackageUuid=").append(modelPackageUuid);
+        sb.append(", servingUuid=").append(servingUuid);
+        sb.append(", failedReason=").append(failedReason);
+        sb.append(", skillType=").append(skillType);
+        sb.append(", imageUrl=").append(imageUrl);
+        sb.append(", defaultLevel=").append(defaultLevel);
+        sb.append(", mergeRule=").append(mergeRule);
+        sb.append(", robotMergeRule=").append(robotMergeRule);
+        sb.append(", workspaceId=").append(workspaceId);
+        sb.append(", graphName=").append(graphName);
+        sb.append(", graphKind=").append(graphKind);
+        sb.append(", serialVersionUID=").append(serialVersionUID);
+        sb.append("]");
+        return sb.toString();
+    }
+}

+ 20 - 0
src/main/java/com/example/unusualsounds/project/vox/entity/VideoStatus.java

@@ -0,0 +1,20 @@
+package com.example.unusualsounds.project.vox.entity;
+
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 视频流状态实体类
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class VideoStatus {
+
+    private String streamId;
+    private String rtspUrl;
+    private boolean isRunning;
+
+}

+ 45 - 0
src/main/java/com/example/unusualsounds/project/vox/entity/VideoTask.java

@@ -0,0 +1,45 @@
+package com.example.unusualsounds.project.vox.entity;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.concurrent.Future;
+
+/**
+ * 视频流任务实体类
+ */
+@Data
+public class VideoTask {
+
+    private String streamId;
+    private String rtspUrl;
+    private Process process;
+    private Future<?> future;
+
+    public VideoTask() {
+    }
+
+    public VideoTask(String streamId, String rtspUrl) {
+        this.streamId = streamId;
+        this.rtspUrl = rtspUrl;
+    }
+
+    public VideoTask(String streamId, String rtspUrl, Process process, Future<?> future) {
+        this.streamId = streamId;
+        this.rtspUrl = rtspUrl;
+        this.process = process;
+        this.future = future;
+    }
+
+
+    public void start() {
+
+
+    }
+
+    public void stop() {
+
+    }
+
+}

+ 81 - 0
src/main/java/com/example/unusualsounds/project/vox/entity/VoiceTicketLogs.java

@@ -0,0 +1,81 @@
+package com.example.unusualsounds.project.vox.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 java.io.Serializable;
+import java.util.Date;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 异音调用工单记录表
+ * @TableName voice_ticket_logs
+ */
+@TableName(value ="voice_ticket_logs")
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class VoiceTicketLogs implements Serializable {
+    /**
+     * 自增id
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 设备uuid
+     */
+    @TableField(value = "device_uuid")
+    private String deviceUuid;
+
+    /**
+     * 技能uuid
+     */
+    @TableField(value = "skill_uuid")
+    private String skillUuid;
+
+    /**
+     * 技能名称
+     */
+    @TableField(value = "skill_name")
+    private String skillName;
+
+    /**
+     * 启动时间
+     */
+    @TableField(value = "start_times")
+    private Date startTimes;
+
+    /**
+     * 结束时间
+     */
+    @TableField(value = "end_times")
+    private Date endTimes;
+
+    /**
+     * 0 关闭,1开启
+     */
+    @TableField(value = "open")
+    private Integer open;
+
+    /**
+     * 开启时候的uuid
+     */
+    @TableField(value = "open_uuid")
+    private String openUuid;
+
+    /**
+     * 备注
+     */
+    @TableField(value = "remark")
+    private String remark;
+
+    @TableField(exist = false)
+    private static final long serialVersionUID = 1L;
+
+
+}

+ 20 - 0
src/main/java/com/example/unusualsounds/project/vox/mapper/AlarmReportsMapper.java

@@ -0,0 +1,20 @@
+package com.example.unusualsounds.project.vox.mapper;
+
+import com.example.unusualsounds.project.vox.entity.AlarmReports;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+* @author pengc
+* @description 针对表【alarm_reports(告警处理表)】的数据库操作Mapper
+* @createDate 2025-02-18 14:45:57
+* @Entity com.example.unusualsounds.project.vox.entity.AlarmReports
+*/
+@Mapper
+public interface AlarmReportsMapper extends BaseMapper<AlarmReports> {
+
+}
+
+
+
+

+ 20 - 0
src/main/java/com/example/unusualsounds/project/vox/mapper/AlarmsMapper.java

@@ -0,0 +1,20 @@
+package com.example.unusualsounds.project.vox.mapper;
+
+import com.example.unusualsounds.project.vox.entity.Alarms;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+* @author pengc
+* @description 针对表【alarms(告警信息表)】的数据库操作Mapper
+* @createDate 2025-02-18 14:45:58
+* @Entity com.example.unusualsounds.project.vox.entity.Alarms
+*/
+@Mapper
+public interface AlarmsMapper extends BaseMapper<Alarms> {
+
+}
+
+
+
+

+ 21 - 0
src/main/java/com/example/unusualsounds/project/vox/mapper/EdgeOrganizationMapper.java

@@ -0,0 +1,21 @@
+package com.example.unusualsounds.project.vox.mapper;
+
+import com.example.unusualsounds.project.vox.entity.EdgeOrganization;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+* @author pengc
+* @description 针对表【edge_organization(边缘组织表)】的数据库操作Mapper
+* @createDate 2025-02-18 14:21:30
+* @Entity com.example.unusualsounds.project.vox.entity.EdgeOrganization
+*/
+@Mapper
+public interface EdgeOrganizationMapper extends BaseMapper<EdgeOrganization> {
+
+
+}
+
+
+
+

+ 20 - 0
src/main/java/com/example/unusualsounds/project/vox/mapper/SkillsMapper.java

@@ -0,0 +1,20 @@
+package com.example.unusualsounds.project.vox.mapper;
+
+import com.example.unusualsounds.project.vox.entity.Skills;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+* @author pengc
+* @description 针对表【skills(技能信息表)】的数据库操作Mapper
+* @createDate 2025-02-19 13:20:42
+* @Entity com.example.unusualsounds.project.vox.entity.Skills
+*/
+@Mapper
+public interface SkillsMapper extends BaseMapper<Skills> {
+
+}
+
+
+
+

+ 20 - 0
src/main/java/com/example/unusualsounds/project/vox/mapper/VoiceTicketLogsMapper.java

@@ -0,0 +1,20 @@
+package com.example.unusualsounds.project.vox.mapper;
+
+import com.example.unusualsounds.project.vox.entity.VoiceTicketLogs;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+* @author pengc
+* @description 针对表【voice_ticket_logs(异音调用工单记录表)】的数据库操作Mapper
+* @createDate 2025-02-25 10:42:53
+* @Entity com.example.unusualsounds.project.vox.entity.VoiceTicketLogs
+*/
+@Mapper
+public interface VoiceTicketLogsMapper extends BaseMapper<VoiceTicketLogs> {
+
+}
+
+
+
+

+ 20 - 0
src/main/java/com/example/unusualsounds/project/vox/service/AlarmReportsService.java

@@ -0,0 +1,20 @@
+package com.example.unusualsounds.project.vox.service;
+
+import com.example.unusualsounds.project.vox.entity.AlarmReports;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.example.unusualsounds.project.vox.entity.Alarms;
+
+/**
+* @author pengc
+* @description 针对表【alarm_reports(告警处理表)】的数据库操作Service
+* @createDate 2025-02-18 14:45:58
+*/
+public interface AlarmReportsService extends IService<AlarmReports> {
+
+    boolean saveAlarmReport(Alarms alarms);
+
+
+
+
+
+}

+ 17 - 0
src/main/java/com/example/unusualsounds/project/vox/service/AlarmsService.java

@@ -0,0 +1,17 @@
+package com.example.unusualsounds.project.vox.service;
+
+import com.example.unusualsounds.project.vox.entity.Alarms;
+import com.baomidou.mybatisplus.extension.service.IService;
+
+import java.util.HashMap;
+
+/**
+* @author pengc
+* @description 针对表【alarms(告警信息表)】的数据库操作Service
+* @createDate 2025-02-18 14:45:58
+*/
+public interface AlarmsService extends IService<Alarms> {
+
+
+    boolean saveResultAnalysis(HashMap<String, String> resultAnalysis);
+}

+ 29 - 0
src/main/java/com/example/unusualsounds/project/vox/service/EdgeOrganizationService.java

@@ -0,0 +1,29 @@
+package com.example.unusualsounds.project.vox.service;
+
+import com.example.unusualsounds.project.vox.entity.EdgeOrganization;
+import com.baomidou.mybatisplus.extension.service.IService;
+
+import java.util.HashMap;
+
+/**
+* @author pengc
+* @description 针对表【edge_organization(边缘组织表)】的数据库操作Service
+* @createDate 2025-02-18 14:21:30
+*/
+public interface EdgeOrganizationService extends IService<EdgeOrganization> {
+
+    /**
+     * 请求Organizations 接口,返回deptID
+     * 根据deptID获取orgID
+     * @return
+     */
+    HashMap<String,String> getOrganizations();
+
+    /**
+     * 获取一见平台组织结构
+     * @return
+     */
+    String getOrganizationObj();
+
+
+}

+ 18 - 0
src/main/java/com/example/unusualsounds/project/vox/service/SkillsService.java

@@ -0,0 +1,18 @@
+package com.example.unusualsounds.project.vox.service;
+
+import com.example.unusualsounds.project.vox.entity.Skills;
+import com.baomidou.mybatisplus.extension.service.IService;
+
+import java.util.List;
+
+/**
+* @author pengc
+* @description 针对表【skills(技能信息表)】的数据库操作Service
+* @createDate 2025-02-19 13:20:42
+*/
+public interface SkillsService extends IService<Skills> {
+
+    Skills selectSkillByName(String skillName);
+
+    List<Skills> getSkillByName(List<String> skillsName);
+}

+ 36 - 0
src/main/java/com/example/unusualsounds/project/vox/service/VideoService.java

@@ -0,0 +1,36 @@
+package com.example.unusualsounds.project.vox.service;
+
+
+import com.example.unusualsounds.project.vox.entity.Skills;
+import com.example.unusualsounds.web.jsonobj.CameraDTO;
+import com.example.unusualsounds.web.jsonobj.Skill;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Future;
+
+public interface VideoService {
+
+    /**
+     * 处理视频流
+     * @param videoId
+     * @param rtspUrl
+     * @return
+     */
+    CompletableFuture<Void> handlesStream(String videoId, String rtspUrl, Skill skill, String deviceName,String openUuid);
+
+    /**
+     * 设置Redis报警消息体
+     * @param skill
+     * @param deviceName
+     * @param deviceId
+     */
+    void setRedisAlarms(String skill,String skillId,String deviceName,String deviceId);
+
+
+    /**
+     * 检查视频流是否在线
+     * @param camera
+     * @return
+     */
+    boolean checkStream(CameraDTO camera);
+}

+ 29 - 0
src/main/java/com/example/unusualsounds/project/vox/service/VoiceTicketLogsService.java

@@ -0,0 +1,29 @@
+package com.example.unusualsounds.project.vox.service;
+
+import com.example.unusualsounds.project.vox.entity.VoiceTicketLogs;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.example.unusualsounds.web.jsonobj.Device;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+* @author pengc
+* @description 针对表【voice_ticket_logs(异音调用工单记录表)】的数据库操作Service
+* @createDate 2025-02-25 10:42:53
+*/
+public interface VoiceTicketLogsService extends IService<VoiceTicketLogs> {
+
+    /**
+     * 一见调用算法时,保存发送的时间
+     * @param device
+     */
+    void saveStartStop(Device device,String openUuid);
+
+    /**
+     * 获取正在运行的任务和表里的任务对比,返回超时的任务map<uuid,skillName>
+     * @param taskStatusesList
+     * @return
+     */
+    List<VoiceTicketLogs> contrastTasks(Map<String, String> taskStatusesList);
+}

+ 19 - 0
src/main/java/com/example/unusualsounds/project/vox/service/VoxService.java

@@ -0,0 +1,19 @@
+package com.example.unusualsounds.project.vox.service;
+
+import com.example.unusualsounds.web.jsonobj.CameraDTO;
+
+public interface VoxService {
+    /**
+     * 根据设备ID请求摄像头信息接口,验证是否在线
+     * @param device_id
+     * @return
+     */
+    CameraDTO checkDevice(String device_id);
+
+
+    /**
+     * 获取组织信息
+     * @return
+     */
+    String getGetOrganization();
+}

+ 43 - 0
src/main/java/com/example/unusualsounds/project/vox/service/impl/AlarmReportsServiceImpl.java

@@ -0,0 +1,43 @@
+package com.example.unusualsounds.project.vox.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.example.unusualsounds.common.utils.UUID;
+import com.example.unusualsounds.project.vox.entity.AlarmReports;
+import com.example.unusualsounds.project.vox.entity.Alarms;
+import com.example.unusualsounds.project.vox.service.AlarmReportsService;
+import com.example.unusualsounds.project.vox.mapper.AlarmReportsMapper;
+import org.springframework.stereotype.Service;
+
+import java.util.Date;
+
+/**
+* @author pengc
+* @description 针对表【alarm_reports(告警处理表)】的数据库操作Service实现
+* @createDate 2025-02-18 14:45:57
+*/
+@Service
+public class AlarmReportsServiceImpl extends ServiceImpl<AlarmReportsMapper, AlarmReports>
+    implements AlarmReportsService{
+
+    @Override
+    public boolean saveAlarmReport(Alarms alarms) {
+
+        AlarmReports alarmReports = new AlarmReports();
+        alarmReports.setUuid(UUID.generateUUIDWithoutHyphens());
+        alarmReports.setCreatedAt(new Date());
+        alarmReports.setUpdatedAt(new Date());
+        alarmReports.setDeptUuid(alarms.getDeptUuid());
+        alarmReports.setOrgUuid(alarms.getOrgUuid());
+        alarmReports.setName("[产生预警]"+alarms.getDescription()+";");
+        alarmReports.setDescription("[产生预警]"+alarms.getDescription()+";");
+        alarmReports.setDisplayStatus("pending");
+        alarmReports.setAlarmUuid(alarms.getUuid());
+
+        boolean save = this.save(alarmReports);
+        return save;
+    }
+}
+
+
+
+

+ 73 - 0
src/main/java/com/example/unusualsounds/project/vox/service/impl/AlarmsServiceImpl.java

@@ -0,0 +1,73 @@
+package com.example.unusualsounds.project.vox.service.impl;
+
+import com.alibaba.fastjson2.JSON;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.example.unusualsounds.common.utils.RedisUtils;
+import com.example.unusualsounds.common.utils.UUID;
+import com.example.unusualsounds.project.vox.entity.Alarms;
+import com.example.unusualsounds.project.vox.service.AlarmsService;
+import com.example.unusualsounds.project.vox.mapper.AlarmsMapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.Date;
+import java.util.HashMap;
+
+/**
+* @author pengc
+* @description 针对表【alarms(告警信息表)】的数据库操作Service实现
+* @createDate 2025-02-18 14:45:58
+*/
+@Service
+public class AlarmsServiceImpl extends ServiceImpl<AlarmsMapper, Alarms>
+    implements AlarmsService{
+
+
+    @Autowired
+    private AlarmReportsServiceImpl alarmReportsService;
+
+    @Autowired
+    private EdgeOrganizationServiceImpl edgeOrganizationServiceImpl;
+
+
+    @Override
+    public boolean saveResultAnalysis(HashMap<String, String> resultAnalysis) {
+
+        String deviceId = resultAnalysis.get("deviceId");
+        String deviceName = resultAnalysis.get("deviceName");
+        String skillName = resultAnalysis.get("skillName");
+        String skillId = resultAnalysis.get("skillId");
+        String videoUrl = resultAnalysis.get("videoUrl");
+        String level = resultAnalysis.get("level");
+        Object o = null;
+        Alarms alarms = null;
+        if (RedisUtils.exists(deviceId)) {
+            o = RedisUtils.get(deviceId);
+            alarms = JSON.parseObject(o.toString(), Alarms.class);
+        }else {
+            Alarms build = new Alarms.Builder().build();
+            alarms=build;
+            HashMap<String, String> organizations = edgeOrganizationServiceImpl.getOrganizations();
+            alarms.setName(skillName);
+            alarms.setDescription(deviceName+","+skillName);
+            alarms.setDeptUuid(organizations.get("deptUuid"));
+            alarms.setOrgUuid(organizations.get("orgUuid"));
+            alarms.setDeviceUuid(deviceId.split("_")[0]);
+            alarms.setSkillUuid(skillId);
+        }
+        String alarmUUID = UUID.generateUUIDWithoutHyphens();
+        alarms.setVideoUrl(videoUrl);
+        alarms.setLevel(Integer.parseInt(level));
+        alarms.setUuid(alarmUUID);
+        alarms.setCreatedAt(new Date());
+        alarms.setUpdatedAt(new Date());
+        boolean save = this.save(alarms);
+        boolean reportSave = alarmReportsService.saveAlarmReport(alarms);
+        return reportSave;
+
+    }
+}
+
+
+
+

+ 109 - 0
src/main/java/com/example/unusualsounds/project/vox/service/impl/EdgeOrganizationServiceImpl.java

@@ -0,0 +1,109 @@
+package com.example.unusualsounds.project.vox.service.impl;
+
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONArray;
+import com.alibaba.fastjson2.JSONObject;
+import com.baidubce.http.HttpMethodName;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.example.unusualsounds.common.utils.BaiDuUtils;
+import com.example.unusualsounds.project.vox.entity.EdgeOrganization;
+import com.example.unusualsounds.project.vox.service.EdgeOrganizationService;
+import com.example.unusualsounds.project.vox.mapper.EdgeOrganizationMapper;
+import com.example.unusualsounds.web.jsonobj.CameraDTO;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Service;
+
+import java.sql.Wrapper;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+* @author pengc
+* @description 针对表【edge_organization(边缘组织表)】的数据库操作Service实现
+* @createDate 2025-02-18 14:21:30
+*/
+@Service
+public class EdgeOrganizationServiceImpl extends ServiceImpl<EdgeOrganizationMapper, EdgeOrganization>
+    implements EdgeOrganizationService{
+
+
+    @Value("${yijian-interface.organizations}")
+    private String getOrganizations;
+
+    @Value("${yijian-interface.organizations-directors}")
+    private String getOrganizationDirectors;
+
+    @Value("${yijian-interface.uri-path-prefix}")
+    private String uriPathPrefix;
+
+    @Value("${yijian-interface.edge-ends.ak}")
+    private String edgeEndsAk;
+
+    @Value("${yijian-interface.edge-ends.sk}")
+    private String edgeEndsSk;
+
+    @Value("${yijian-interface.edge-ends.endpoint}")
+    private String edgeEndsEndpoint;
+
+
+
+
+    @Override
+    public HashMap<String, String> getOrganizations() {
+
+        HashMap<String, String> stringStringHashMap = new HashMap<>();
+
+        try {
+            ResponseEntity<String> response = BaiDuUtils.sendRequest(getOrganizations, HttpMethodName.GET,
+                    null, null, String.class, edgeEndsAk, edgeEndsSk,
+                    edgeEndsEndpoint, uriPathPrefix);
+            if (response.getStatusCode().is2xxSuccessful()) {
+
+                JSONArray objects = JSONArray.parseArray(response.getBody());
+                Object o = objects.get(0);
+                JSONObject jsonObject = JSON.parseObject(o.toString());
+                String deptID = jsonObject.get("deptID").toString();
+                stringStringHashMap.put("deptUuid", deptID.toString());
+
+                QueryWrapper<EdgeOrganization> queryWrapper = new QueryWrapper<>();
+                queryWrapper.eq("dept_uuid", deptID)
+                        .last("limit 1");
+
+                EdgeOrganization deptUuid = this.baseMapper.selectOne(queryWrapper);
+                if (deptUuid != null) {
+                    stringStringHashMap.put("orgUuid", deptUuid.getOrgUuid());
+                }
+                return stringStringHashMap;
+            } else {
+                //请求失败,抛出异常
+                throw new RuntimeException("请求失败:" + response.getStatusCode());
+            }
+        } catch (Exception e) {
+            throw new RuntimeException("其他异常:"+e);
+        }
+    }
+
+    @Override
+    public String getOrganizationObj() {
+
+        ResponseEntity<String> response = BaiDuUtils.sendRequest(getOrganizations, HttpMethodName.GET,
+                null, null, String.class, edgeEndsAk, edgeEndsSk,
+                edgeEndsEndpoint, uriPathPrefix);
+        if (response.getStatusCode().is2xxSuccessful()) {
+            return response.getBody();
+        }else {
+            //请求失败,抛出异常
+            throw new RuntimeException(getOrganizations+":请求失败:" + response.getStatusCode());
+        }
+
+    }
+
+
+}
+
+
+
+

+ 52 - 0
src/main/java/com/example/unusualsounds/project/vox/service/impl/SkillsServiceImpl.java

@@ -0,0 +1,52 @@
+package com.example.unusualsounds.project.vox.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.example.unusualsounds.project.vox.entity.Skills;
+import com.example.unusualsounds.project.vox.service.SkillsService;
+import com.example.unusualsounds.project.vox.mapper.SkillsMapper;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+* @author pengc
+* @description 针对表【skills(技能信息表)】的数据库操作Service实现
+* @createDate 2025-02-19 13:20:42
+*/
+@Service
+public class SkillsServiceImpl extends ServiceImpl<SkillsMapper, Skills>
+    implements SkillsService{
+
+    @Override
+    public Skills selectSkillByName(String skillName) {
+
+        QueryWrapper<Skills> selectQuery = new QueryWrapper<Skills>();
+        selectQuery.eq("name", skillName)
+                .last("LIMIT 1");
+
+        Skills skills = baseMapper.selectOne(selectQuery);
+
+        if (skills != null) {
+            return skills;
+        }
+
+        return null;
+    }
+
+    @Override
+    public List<Skills> getSkillByName(List<String> skillsName) {
+        QueryWrapper<Skills> queryWrapper = new QueryWrapper<Skills>();
+        if(skillsName.isEmpty()||skillsName.size()<1){
+            skillsName.add("人声呼救测试");
+            skillsName.add("设备异音测试");
+        }
+        queryWrapper.in("name", skillsName);
+        List<Skills> skills = baseMapper.selectList(queryWrapper);
+        return skills;
+    }
+}
+
+
+
+

+ 133 - 0
src/main/java/com/example/unusualsounds/project/vox/service/impl/VideoServiceImpl.java

@@ -0,0 +1,133 @@
+package com.example.unusualsounds.project.vox.service.impl;
+
+import com.alibaba.fastjson2.JSON;
+import com.example.unusualsounds.common.utils.RedisUtils;
+import com.example.unusualsounds.project.vox.config.VideoTaskManager;
+import com.example.unusualsounds.project.vox.entity.Alarms;
+import com.example.unusualsounds.project.vox.service.VideoService;
+import com.example.unusualsounds.web.jsonobj.CameraDTO;
+import com.example.unusualsounds.web.jsonobj.Skill;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.util.HashMap;
+import java.util.concurrent.CompletableFuture;
+
+@Service
+public class VideoServiceImpl implements VideoService {
+
+    @Autowired
+    private VideoTaskManager taskManager;
+
+    @Autowired
+    private EdgeOrganizationServiceImpl edgeOrganizationServiceImpl;
+
+    @Autowired
+    private SkillsServiceImpl skillsService;
+
+
+    @Override
+    @Async("videoTaskExecutor")
+    public CompletableFuture<Void> handlesStream(String deviceId,String rtspUrl, Skill skill, String deviceName,String openUuid) {
+
+        if(!taskManager.startTask(deviceId,deviceName,skill.getId(), rtspUrl,skill.getName(),openUuid)){
+            throw new IllegalStateException("任务已存在"+ deviceId);
+        }
+        //查询redis 是否存在deviceId这个key,deviceId是工单id
+        //添加同一个设备接入两个技能的情况
+        if(!RedisUtils.exists(deviceId+"_"+skill.getName())){
+            //不存在,则设置deviceId这个key
+            setRedisAlarms(skill.getName(),skill.getId(),deviceName,deviceId);
+        }else {
+            Object o = RedisUtils.get(deviceId + "_" + skill.getName());
+            Alarms alarms = JSON.parseObject(o.toString(), Alarms.class);
+            if (skill.getName().equals(alarms.getName())) {
+                setRedisAlarms(skill.getName(),skill.getId(), deviceName, deviceId);
+            }
+        }
+        return CompletableFuture.completedFuture(null);
+    }
+
+
+    /**
+     * 向redis插入alarms信息
+     * @param skillName
+     * @param deviceName
+     * @param deviceId
+     */
+    @Override
+    public void setRedisAlarms(String skillName,String skillId,String deviceName,String deviceId){
+        Alarms alarms = new Alarms.Builder().build();
+        HashMap<String, String> organizations = edgeOrganizationServiceImpl.getOrganizations();
+        alarms.setName(skillName);
+        alarms.setDescription(deviceName+","+skillName);
+        alarms.setDeptUuid(organizations.get("deptUuid"));
+        alarms.setOrgUuid(organizations.get("orgUuid"));
+        alarms.setDeviceUuid(deviceId);
+        alarms.setSkillUuid(skillId);
+        RedisUtils.setForeverKey(deviceId+"_"+skillName, alarms);
+    }
+
+    @Override
+    public boolean checkStream(CameraDTO camera) {
+        Process process = null;
+        try {
+            ProcessBuilder processBuilder = new ProcessBuilder(
+                    "D:\\file\\ffmpeg\\ffprobe.exe",
+                    "-v", "error",
+                    "-rtsp_transport", "tcp",
+                    "-timeout", "15000000", // 单位:微秒
+                    "-i", camera.getSrcURL());
+            process = processBuilder.start();
+            BufferedReader errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
+
+            // 读取错误流,非阻塞方式检查是否有错误
+            boolean hasError = false;
+            String line;
+            long startTime = System.currentTimeMillis();
+            // 总超时15秒
+            long timeout = 15000;
+            while (true) {
+                if (errorReader.ready()) { // 检查是否有数据可读
+                    line = errorReader.readLine();
+                    if (line != null) {
+                        System.out.println("FFPROBE Error: " + line);
+                        hasError = true;
+                        process.destroyForcibly();
+                        break; // 发现错误立即退出
+                    }
+                } else {
+                    // 检查进程是否已退出
+                    try {
+                        int exitCode = process.exitValue();
+                        break; // 进程已退出,停止循环
+                    } catch (IllegalThreadStateException e) {
+
+                    }
+                }
+                // 检查总超时
+                if (System.currentTimeMillis() - startTime > timeout) {
+                    break;
+                }
+                Thread.sleep(1000); // 避免CPU忙等
+            }
+            // 确保进程终止
+            if (process.isAlive()) {
+                process.destroyForcibly();
+            }
+            // 获取最终退出码
+            int exitCode = process.waitFor();
+            return exitCode == 0 && !hasError;
+        } catch (Exception e) {
+            e.printStackTrace();
+            if (process != null) {
+                process.destroyForcibly();
+            }
+            return false;
+        }
+    }
+
+}

+ 71 - 0
src/main/java/com/example/unusualsounds/project/vox/service/impl/VoiceTicketLogsServiceImpl.java

@@ -0,0 +1,71 @@
+package com.example.unusualsounds.project.vox.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.example.unusualsounds.common.utils.DateUtils;
+import com.example.unusualsounds.project.vox.entity.VoiceTicketLogs;
+import com.example.unusualsounds.project.vox.service.VoiceTicketLogsService;
+import com.example.unusualsounds.project.vox.mapper.VoiceTicketLogsMapper;
+import com.example.unusualsounds.web.jsonobj.Device;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+* @author pengc
+* @description 针对表【voice_ticket_logs(异音调用工单记录表)】的数据库操作Service实现
+* @createDate 2025-02-25 10:42:53
+*/
+@Service
+@Slf4j
+public class VoiceTicketLogsServiceImpl extends ServiceImpl<VoiceTicketLogsMapper, VoiceTicketLogs>
+    implements VoiceTicketLogsService{
+
+    @Override
+    public void saveStartStop(Device device,String openUuid) {
+
+        VoiceTicketLogs voiceTicketLogs = new VoiceTicketLogs();
+        voiceTicketLogs.setDeviceUuid(device.getDeviceId());
+        voiceTicketLogs.setSkillUuid(device.getSkills().get(0).getId());
+        voiceTicketLogs.setSkillName(device.getSkills().get(0).getName());
+        voiceTicketLogs.setStartTimes(DateUtils.parseDate(device.getSkills().get(0).getTimes().get(0).getStart()));
+        voiceTicketLogs.setEndTimes(DateUtils.parseDate(device.getSkills().get(0).getTimes().get(0).getEnd()));
+        voiceTicketLogs.setOpen(device.getSkills().get(0).isOpen() ? 1 : 0);
+        voiceTicketLogs.setOpenUuid(openUuid);
+        boolean save = save(voiceTicketLogs);
+    }
+
+    @Override
+    public List<VoiceTicketLogs> contrastTasks(Map<String, String> taskStatusesList) {
+
+        QueryWrapper<VoiceTicketLogs> queryWrapper = new QueryWrapper<>();
+        List<String> openUuidList = new ArrayList<>();
+
+        taskStatusesList.forEach((k,v) -> {
+            openUuidList.add(k);
+        });
+        queryWrapper.in("open_uuid", openUuidList)
+                .eq("open", 1);
+        List<VoiceTicketLogs> voiceTicketLogs = baseMapper.selectList(queryWrapper);
+
+        List<VoiceTicketLogs> collect = voiceTicketLogs.stream().filter(
+                voicetask -> {
+                    Date endTimes = voicetask.getEndTimes();
+                    Date nowDate = DateUtils.getNowDate();
+                    return nowDate.compareTo(endTimes) > 0;
+                }
+        ).collect(Collectors.toList());
+
+
+        return collect;
+    }
+}
+
+
+
+

+ 93 - 0
src/main/java/com/example/unusualsounds/project/vox/service/impl/VoxServiceImpl.java

@@ -0,0 +1,93 @@
+package com.example.unusualsounds.project.vox.service.impl;
+
+import com.baidubce.http.HttpMethodName;
+import com.example.unusualsounds.common.utils.BaiDuUtils;
+import com.example.unusualsounds.project.vox.service.VoxService;
+import com.example.unusualsounds.web.jsonobj.CameraDTO;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+
+@Service
+@Slf4j
+public class VoxServiceImpl implements VoxService {
+
+    @Autowired
+    private  RestTemplate restTemplate;
+
+    @Value("${yijian-interface.device-info}")
+    private String getDeviceInfoUrl;
+
+    @Value("${yijian-interface.organizations}")
+    private String getOrganizations;
+
+    @Value("${yijian-interface.uri-path-prefix}")
+    private String uriPathPrefix;
+
+
+    @Value("${yijian-interface.center.ak}")
+    private String centerAk;
+
+    @Value("${yijian-interface.center.sk}")
+    private String centerSk;
+
+    @Value("${yijian-interface.center.endpoint}")
+    private String centerEndpoint;
+
+
+    @Value("${yijian-interface.edge-ends.ak}")
+    private String edgeEndsAk;
+
+    @Value("${yijian-interface.edge-ends.sk}")
+    private String edgeEndsSk;
+
+    @Value("${yijian-interface.edge-ends.endpoint}")
+    private String edgeEndsEndpoint;
+
+
+
+    @Override
+    public CameraDTO checkDevice(String device_id) {
+
+        try {
+            String url = getDeviceInfoUrl.replace("{deviceID}", device_id);
+            ResponseEntity<CameraDTO> response = BaiDuUtils.sendRequest(url, HttpMethodName.GET,
+                    null, null, CameraDTO.class, edgeEndsAk, edgeEndsSk,
+                    edgeEndsEndpoint, uriPathPrefix);
+
+            if (response.getStatusCode().is2xxSuccessful()) {
+                return response.getBody();
+            } else {
+                //请求失败,抛出异常
+                throw new RuntimeException("请求失败:" + response.getStatusCode());
+
+            }
+        } catch (Exception e) {
+            throw new RuntimeException("其他异常:"+e);
+        }
+    }
+
+
+    public String getGetOrganization(){
+        try {
+            ResponseEntity<String> response = BaiDuUtils.sendRequest(getOrganizations, HttpMethodName.GET,
+                    null, null, String.class, edgeEndsAk, edgeEndsSk,
+                    edgeEndsEndpoint, uriPathPrefix);
+            if (response.getStatusCode().is2xxSuccessful()) {
+                return response.getBody();
+            } else {
+                //请求失败,抛出异常
+                throw new RuntimeException("请求失败:" + response.getStatusCode());
+
+            }
+        } catch (Exception e) {
+            throw new RuntimeException("其他异常:"+e);
+        }
+
+    }
+
+
+}

+ 86 - 0
src/main/java/com/example/unusualsounds/project/vox/task/TicketsTask.java

@@ -0,0 +1,86 @@
+package com.example.unusualsounds.project.vox.task;
+
+
+import com.example.unusualsounds.project.vox.config.VideoTaskManager;
+import com.example.unusualsounds.project.vox.entity.VoiceTicketLogs;
+import com.example.unusualsounds.project.vox.service.impl.VoiceTicketLogsServiceImpl;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 工单定时任务
+ * ---
+ * 用于应对边缘测网络问题,主要针对定时关闭问题
+ * 1. 算法启接口调用后,将工单存入表中
+ * 2. 定时查询本地存入的工单,主要侦测当前停止的工单任务是否再运行
+ * 3. 只添加算法停接口因网络问题未收到停止的请求
+ * 4. 忽略因网络问题未收到启动的请求,所以定时查询是自己表内已经插入的数据
+ *
+ */
+@Component
+@Slf4j
+public class TicketsTask {
+
+    @Autowired
+    private VideoTaskManager videoTaskManager;
+
+    @Autowired
+    private VoiceTicketLogsServiceImpl voiceTicketLogsServiceImpl;
+
+    @Value("${vox.video-dir}")
+    private String videoDir;
+
+
+    @Scheduled(cron = "0 */10 * * * ?")
+    public void checkStopTickets() {
+
+        List<String> strings = this.stopTickets();
+        if (strings != null && strings.size() > 0) {
+            strings.forEach(s -> {
+                try {
+                    videoTaskManager.stopTask(s);
+                } catch (InterruptedException e) {
+                    throw new RuntimeException(e);
+                }
+            });
+        }else {
+            log.info("定时停止工单:无");
+        }
+    }
+
+    /**
+     * 停止到时间未停止的工单
+     * @return
+     */
+    public List<String> stopTickets() {
+        List<VideoTaskManager.TaskStatus> taskStatuses = videoTaskManager.listTasks();
+        if(taskStatuses == null || taskStatuses.size() == 0) return  null;
+        //openUuid,streamId
+        Map<String, String> taskStatusesList = new HashMap<>();
+        taskStatuses.forEach(
+                task ->{
+                    taskStatusesList.put(task.getOpenUuid(),task.getStreamId());
+                }
+        );
+
+        List<VoiceTicketLogs> delTasks = voiceTicketLogsServiceImpl.contrastTasks(taskStatusesList);
+        delTasks.forEach(str -> {
+            log.info("需要停止的工单:{}",str);
+        });
+        List<String> collect = delTasks.stream().map(
+                task -> {
+                    return task.getDeviceUuid() + "_" + task.getSkillName();
+                })
+                .distinct().collect(Collectors.toList());
+        return collect;
+    }
+
+
+}

+ 197 - 0
src/main/java/com/example/unusualsounds/web/entity/AjaxResult.java

@@ -0,0 +1,197 @@
+package com.example.unusualsounds.web.entity;
+
+
+import com.example.unusualsounds.common.constant.HttpStatus;
+import com.example.unusualsounds.common.utils.StringUtilsEx;
+
+
+import java.util.HashMap;
+import java.util.Objects;
+
+/**
+ * 操作消息提醒
+ *
+ * @author alibct
+ */
+public class AjaxResult extends HashMap<String, Object> {
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 状态码
+     */
+    public static final String CODE_TAG = "code";
+
+    /**
+     * 返回内容
+     */
+    public static final String MSG_TAG = "result";
+
+    /**
+     * 数据对象
+     */
+    public static final String DATA_TAG = "data";
+
+    /**
+     * 初始化一个新创建的 AjaxResult 对象,使其表示一个空消息。
+     */
+    public AjaxResult() {
+    }
+
+    /**
+     * 初始化一个新创建的 AjaxResult 对象
+     *
+     * @param code 状态码
+     * @param msg  返回内容
+     */
+    public AjaxResult(int code, String msg) {
+        super.put(CODE_TAG, code);
+        super.put(MSG_TAG, msg);
+    }
+
+    /**
+     * 初始化一个新创建的 AjaxResult 对象
+     *
+     * @param code 状态码
+     * @param msg  返回内容
+     * @param data 数据对象
+     */
+    public AjaxResult(int code, String msg, Object data) {
+        super.put(CODE_TAG, code);
+        super.put(MSG_TAG, msg);
+        if (StringUtilsEx.isNotNull(data)) {
+            super.put(DATA_TAG, data);
+        }
+    }
+
+    /**
+     * 返回成功消息
+     *
+     * @return 成功消息
+     */
+    public static AjaxResult success() {
+        return AjaxResult.success("操作成功");
+    }
+
+    /**
+     * 返回成功数据
+     *
+     * @return 成功消息
+     */
+    public static AjaxResult success(Object data) {
+        return AjaxResult.success("操作成功", data);
+    }
+
+    /**
+     * 返回成功消息
+     *
+     * @param msg 返回内容
+     * @return 成功消息
+     */
+    public static AjaxResult success(String msg) {
+        return AjaxResult.success(msg, null);
+    }
+
+    /**
+     * 返回成功消息
+     *
+     * @param msg  返回内容
+     * @param data 数据对象
+     * @return 成功消息
+     */
+    public static AjaxResult success(String msg, Object data) {
+        return new AjaxResult(HttpStatus.SUCCESS, msg, data);
+    }
+
+    /**
+     * 返回警告消息
+     *
+     * @param msg 返回内容
+     * @return 警告消息
+     */
+    public static AjaxResult warn(String msg) {
+        return AjaxResult.warn(msg, null);
+    }
+
+    /**
+     * 返回警告消息
+     *
+     * @param msg  返回内容
+     * @param data 数据对象
+     * @return 警告消息
+     */
+    public static AjaxResult warn(String msg, Object data) {
+        return new AjaxResult(HttpStatus.WARN, msg, data);
+    }
+
+    /**
+     * 返回错误消息
+     *
+     * @return 错误消息
+     */
+    public static AjaxResult error() {
+        return AjaxResult.error("操作失败");
+    }
+
+    /**
+     * 返回错误消息
+     *
+     * @param msg 返回内容
+     * @return 错误消息
+     */
+    public static AjaxResult error(String msg) {
+        return AjaxResult.error(msg, null);
+    }
+
+    /**
+     * 返回错误消息
+     *
+     * @param msg  返回内容
+     * @param data 数据对象
+     * @return 错误消息
+     */
+    public static AjaxResult error(String msg, Object data) {
+        return new AjaxResult(HttpStatus.ERROR, msg, data);
+    }
+
+    /**
+     * 返回错误消息
+     *
+     * @param code 状态码
+     * @param msg  返回内容
+     * @return 错误消息
+     */
+    public static AjaxResult error(int code, String msg) {
+        return new AjaxResult(code, msg, null);
+    }
+
+    /**
+     * 是否为成功消息
+     *
+     * @return 结果
+     */
+    public boolean isSuccess() {
+        return !isError();
+    }
+
+    /**
+     * 是否为错误消息
+     *
+     * @return 结果
+     */
+    public boolean isError() {
+        return Objects.equals(HttpStatus.ERROR, this.get(CODE_TAG));
+    }
+
+    /**
+     * 方便链式调用
+     *
+     * @param key   键
+     * @param value 值
+     * @return 数据对象
+     */
+    @Override
+    public AjaxResult put(String key, Object value) {
+        super.put(key, value);
+        return this;
+    }
+}

+ 45 - 0
src/main/java/com/example/unusualsounds/web/jsonobj/CameraDTO.java

@@ -0,0 +1,45 @@
+package com.example.unusualsounds.web.jsonobj;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class CameraDTO {
+    private String id;
+    private String name;
+    private String createdAt;
+    private String updatedAt;
+    private String srcURL;
+    private String desensitizedSrcURL;
+    private String streamURL;
+    private String analysisHDURL;
+    private String analysisSDURL;
+    private String status;
+    private String type;
+    private String imageURL;
+    private String description;
+    private String ip;
+    private String userUUID;
+    private String robotID;
+    private String robotName;
+    private Object cameraMatrix;
+    private int roisOpenNum;
+    private int skillsNum;
+    private int roisNum;
+    private String gbID;
+    private String gbRootID;
+    private String dataSource;
+    private boolean isMeasureUsed;
+    private Object spotInfo;
+    private int alarmStatus;
+    private String deptID;
+    private String deptName;
+    private Department department;
+    private List<String> directors;
+    private boolean isTurninged;
+    private boolean isManualRecording;
+    private long manualRecordStartTime;
+    private String videoType;
+
+}

+ 10 - 0
src/main/java/com/example/unusualsounds/web/jsonobj/Department.java

@@ -0,0 +1,10 @@
+package com.example.unusualsounds.web.jsonobj;
+
+import lombok.Data;
+
+@Data
+public class Department {
+    private String id;
+    private String name;
+    
+}

+ 17 - 0
src/main/java/com/example/unusualsounds/web/jsonobj/Device.java

@@ -0,0 +1,17 @@
+package com.example.unusualsounds.web.jsonobj;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class Device {
+    private String deviceId;
+    private List<Skill> skills;
+
+}
+
+
+
+
+

+ 10 - 0
src/main/java/com/example/unusualsounds/web/jsonobj/Fps.java

@@ -0,0 +1,10 @@
+package com.example.unusualsounds.web.jsonobj;
+
+import lombok.Data;
+
+@Data
+public class Fps {
+    private int frames;
+    private int seconds;
+
+}

+ 17 - 0
src/main/java/com/example/unusualsounds/web/jsonobj/Skill.java

@@ -0,0 +1,17 @@
+package com.example.unusualsounds.web.jsonobj;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class Skill {
+    private String id;
+    private String name;
+    private boolean isOpen;
+    private List<TimeFrame> times;
+    private Fps fps;
+    private int version;
+    private String status;
+    private List<?> meter; // 根据实际内容替换合适的类型
+}

+ 10 - 0
src/main/java/com/example/unusualsounds/web/jsonobj/TimeFrame.java

@@ -0,0 +1,10 @@
+package com.example.unusualsounds.web.jsonobj;
+
+import lombok.Data;
+
+@Data
+public class TimeFrame {
+    private String start;
+    private String end;
+
+}

+ 53 - 0
src/main/resources/application-dev.yml

@@ -0,0 +1,53 @@
+# 应用服务 WEB 访问端口
+server:
+  port: 19801
+
+knife4j:
+  #关闭生产环境屏蔽
+  production: false
+
+
+minio:
+#  中心测
+#  endpoint: http://10.68.204.52:8077
+#  边缘测
+  endpoint: http://10.68.208.94:8077
+  ak: AKIAIOSFODNN7EXAMPLE
+  sk: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
+  bucket: windmill
+  dir: store/abnormal-sound/
+  source-dir: store/abnormal-sound/source/
+
+spring:
+  datasource:
+    dynamic:
+      primary: slave
+      datasource:
+        #中心测
+        master:
+          type: com.alibaba.druid.pool.DruidDataSource
+          driver-class-name: com.mysql.cj.jdbc.Driver
+          url: jdbc:mysql://10.68.208.94:8639/iip_api_service?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&allowMultiQueries=true
+          username: root
+          password: 123456
+        #边缘测
+        slave:
+          type: com.alibaba.druid.pool.DruidDataSource
+          driver-class-name: com.mysql.cj.jdbc.Driver
+          url: jdbc:mysql://10.68.208.94:8639/iip_api_service?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&allowMultiQueries=true
+          username: root
+          password: fbLfJZ1sFMAw
+    druid:
+      initial-size: 5
+      min-idle: 5
+      maxActive: 20
+      # 配置获取连接等待超时的时间
+      maxWait: 60000
+      # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
+      timeBetweenEvictionRunsMillis: 6000
+      # 配置一个连接在池中最小生存的时间,单位是毫秒
+      minEvictableIdleTimeMillis: 60000
+      # 配置一个连接在池中最大生存的时间,单位是毫秒
+      maxEvictableIdleTimeMillis: 900000
+      access-to-underlying-connection-allowed:
+

+ 51 - 0
src/main/resources/application-prod.yml

@@ -0,0 +1,51 @@
+# 应用服务 WEB 访问端口
+server:
+  port: 19801
+
+knife4j:
+  #开启生产环境屏蔽
+  production: true
+
+
+minio:
+#  endpoint: http://192.168.10.135:9000
+  ak: AKIAIOSFODNN7EXAMPLE
+  sk: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKE
+  bucket: windmill
+  dir: store/abnormal-sound/
+  source-dir: store/abnormal-sound/source/
+
+spring:
+  datasource:
+    dynamic:
+      primary: slave
+      datasource:
+        #中心测
+        master:
+          type: com.alibaba.druid.pool.DruidDataSource
+          driver-class-name: com.mysql.cj.jdbc.Driver
+          url: jdbc:mysql://10.68.208.94:8639/iip_api_service?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&allowMultiQueries=true
+          username: root
+          password: 123456
+        #边缘测
+        slave:
+          type: com.alibaba.druid.pool.DruidDataSource
+          driver-class-name: com.mysql.cj.jdbc.Driver
+          url: jdbc:mysql://10.68.208.94:8639/iip_api_service?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&allowMultiQueries=true
+          username: root
+          password: fbLfJZ1sFMAw
+    druid:
+      initial-size: 5
+      min-idle: 5
+      maxActive: 20
+      # 配置获取连接等待超时的时间
+      maxWait: 60000
+      # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
+      timeBetweenEvictionRunsMillis: 6000
+      # 配置一个连接在池中最小生存的时间,单位是毫秒
+      minEvictableIdleTimeMillis: 60000
+      # 配置一个连接在池中最大生存的时间,单位是毫秒
+      maxEvictableIdleTimeMillis: 900000
+      access-to-underlying-connection-allowed:
+
+

+ 102 - 0
src/main/resources/application.yml

@@ -0,0 +1,102 @@
+spring:
+  servlet:
+    multipart:
+      max-file-size: 100MB
+      max-request-size: 100MB
+
+  profiles:
+    active: dev
+  data:
+    redis:
+      host: 192.168.1.120
+      port: 6390
+      password:
+      database: 0
+      timeout: 6000ms
+      lettuce:
+        pool:
+          min-idle: 5
+          max-idle: 10
+          max-wait: -1ms
+          max-active: 1000
+
+
+
+springdoc:
+  swagger-ui:
+    path: /swagger-ui.html
+    tags-sorter: alpha
+    operations-sorter: alpha
+  api-docs:
+    path: /v3/api-docs
+  group-configs:
+    - group: 'vox'
+      paths-to-match: '/**'
+      packages-to-scan: com.example.unusualsounds.project.vox.controller
+    - group: 'device'
+      paths-to-match: '/**'
+      packages-to-scan: com.example.unusualsounds.project.device.controller
+
+# mybatis-plus 配置
+mybatis-plus:
+  # 搜索指定包名
+  type-aliases-package: com.example.unusualsounds.project
+  # 配置mapper扫描,扫描mybatis/mapper/下所有 .xml文件
+  mapper-locations: classpath*:/mybatis/mapper/*Mapper.xml
+  # 加载全局配置文件
+  config-location: classpath:mybatis/mybatis-config.xml
+
+
+#knife4j配置
+knife4j:
+  #启用增强设置
+  enable: true
+  #启用登录认证
+  basic:
+    enable: true
+    username: admin
+    password: 123456
+  setting:
+    language: zh_cn
+    enable-version: true
+    enable-swagger-models: true
+
+
+logging:
+  level:
+    root: warn
+    com.example.unusualsounds: debug
+
+#摄像头数量配置
+Camera:
+  num: 40
+
+#一见平台相关配置
+yijian-interface:
+  #中心节点配置信息
+  center:
+    ak: fd01ec4de8724d71bb764190f202929c
+    sk: f44a391cadcc42f98167e7e42086514f
+    endpoint: http://10.68.204.52:8412
+  #边缘节点配置信息
+  edge-ends:
+    ak: a440a816330a4e558ec33d50aaad379d
+    sk: d9068f8ff3604a45a83b81c987de673a
+    endpoint: http://10.68.208.94:8412
+
+  #接口地址前缀配置
+  uri-path-prefix: /api/spi
+  #设备信息接口地址 GET /v1/devices/{deviceID}
+  device-info: /v1/devices/{deviceID}
+  #组织树接口地址 GET /v1/organizations
+  organizations: /v1/organizations
+  #责任人请求 /v1/organizations/directors?deptID=452b784fe34d42b1b3700b6aeb648e1e
+  organizations-directors: /v1/organizations/directors?deptID={deptId}
+
+#异音算法接口
+vox:
+  #vox服务地址
+  post-analysis-url: http://192.168.1.188:8899/audio
+  #视频流切片保存地址
+  video-dir: D:\tmp\data\vox\
+

+ 94 - 0
src/main/resources/logback.xml

@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration>
+    <!-- 日志存放路径 -->
+	<property name="log.path" value="D:\\tmp\\logs\\vox" />
+<!--    <property name="log.path" value="/logs" />-->
+    <!-- 日志输出格式 -->
+	<property name="log.pattern" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />
+
+	<!-- 控制台输出 -->
+	<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
+		<encoder>
+			<pattern>${log.pattern}</pattern>
+		</encoder>
+	</appender>
+	
+	<!-- 系统日志输出 -->
+	<appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
+	    <file>${log.path}/sys-info.log</file>
+        <!-- 循环政策:基于时间创建日志文件 -->
+		<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!-- 日志文件名格式 -->
+			<fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.log</fileNamePattern>
+			<!-- 日志最大的历史 60天 -->
+			<maxHistory>60</maxHistory>
+		</rollingPolicy>
+		<encoder>
+			<pattern>${log.pattern}</pattern>
+		</encoder>
+		<filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <!-- 过滤的级别 -->
+            <level>INFO</level>
+            <!-- 匹配时的操作:接收(记录) -->
+            <onMatch>ACCEPT</onMatch>
+            <!-- 不匹配时的操作:拒绝(不记录) -->
+            <onMismatch>DENY</onMismatch>
+        </filter>
+	</appender>
+	
+	<appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
+	    <file>${log.path}/sys-error.log</file>
+        <!-- 循环政策:基于时间创建日志文件 -->
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!-- 日志文件名格式 -->
+            <fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log</fileNamePattern>
+			<!-- 日志最大的历史 60天 -->
+			<maxHistory>60</maxHistory>
+        </rollingPolicy>
+        <encoder>
+            <pattern>${log.pattern}</pattern>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <!-- 过滤的级别 -->
+            <level>ERROR</level>
+			<!-- 匹配时的操作:接收(记录) -->
+            <onMatch>ACCEPT</onMatch>
+			<!-- 不匹配时的操作:拒绝(不记录) -->
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+	
+	<!-- 用户访问日志输出  -->
+    <appender name="sys-user" class="ch.qos.logback.core.rolling.RollingFileAppender">
+		<file>${log.path}/sys-user.log</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!-- 按天回滚 daily -->
+            <fileNamePattern>${log.path}/sys-user.%d{yyyy-MM-dd}.log</fileNamePattern>
+            <!-- 日志最大的历史 60天 -->
+            <maxHistory>60</maxHistory>
+        </rollingPolicy>
+        <encoder>
+            <pattern>${log.pattern}</pattern>
+        </encoder>
+    </appender>
+	
+	<!-- 系统模块日志级别控制  -->
+	<logger name="com.ruoyi" level="info" />
+	<!-- Spring日志级别控制  -->
+	<logger name="org.springframework" level="warn" />
+
+	<root level="info">
+		<appender-ref ref="console" />
+	</root>
+	
+	<!--系统操作日志-->
+    <root level="info">
+        <appender-ref ref="file_info" />
+        <appender-ref ref="file_error" />
+    </root>
+	
+	<!--系统用户操作日志-->
+    <logger name="sys-user" level="info">
+        <appender-ref ref="sys-user"/>
+    </logger>
+</configuration> 

+ 27 - 0
src/main/resources/mybatis/mapper/AlarmReportsMapper.xml

@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper
+        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.example.unusualsounds.project.vox.mapper.AlarmReportsMapper">
+
+    <resultMap id="BaseResultMap" type="com.example.unusualsounds.project.vox.entity.AlarmReports">
+            <id property="id" column="id" jdbcType="BIGINT"/>
+            <result property="uuid" column="uuid" jdbcType="CHAR"/>
+            <result property="createdAt" column="created_at" jdbcType="TIMESTAMP"/>
+            <result property="updatedAt" column="updated_at" jdbcType="TIMESTAMP"/>
+            <result property="deletedAt" column="deleted_at" jdbcType="TIMESTAMP"/>
+            <result property="deptUuid" column="dept_uuid" jdbcType="CHAR"/>
+            <result property="orgUuid" column="org_uuid" jdbcType="CHAR"/>
+            <result property="name" column="name" jdbcType="VARCHAR"/>
+            <result property="description" column="description" jdbcType="VARCHAR"/>
+            <result property="displayStatus" column="display_status" jdbcType="CHAR"/>
+            <result property="alarmUuid" column="alarm_uuid" jdbcType="CHAR"/>
+    </resultMap>
+
+    <sql id="Base_Column_List">
+        id,uuid,created_at,
+        updated_at,deleted_at,dept_uuid,
+        org_uuid,name,description,
+        display_status,alarm_uuid
+    </sql>
+</mapper>

+ 78 - 0
src/main/resources/mybatis/mapper/AlarmsMapper.xml

@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper
+        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.example.unusualsounds.project.vox.mapper.AlarmsMapper">
+
+    <resultMap id="BaseResultMap" type="com.example.unusualsounds.project.vox.entity.Alarms">
+            <id property="id" column="id" jdbcType="BIGINT"/>
+            <result property="uuid" column="uuid" jdbcType="CHAR"/>
+            <result property="createdAt" column="created_at" jdbcType="TIMESTAMP"/>
+            <result property="updatedAt" column="updated_at" jdbcType="TIMESTAMP"/>
+            <result property="deletedAt" column="deleted_at" jdbcType="TIMESTAMP"/>
+            <result property="deptUuid" column="dept_uuid" jdbcType="CHAR"/>
+            <result property="orgUuid" column="org_uuid" jdbcType="CHAR"/>
+            <result property="name" column="name" jdbcType="VARCHAR"/>
+            <result property="description" column="description" jdbcType="VARCHAR"/>
+            <result property="status" column="status" jdbcType="CHAR"/>
+            <result property="displayStatus" column="display_status" jdbcType="CHAR"/>
+            <result property="level" column="level" jdbcType="INTEGER"/>
+            <result property="imageUrl" column="image_url" jdbcType="VARCHAR"/>
+            <result property="thumbnailUrl" column="thumbnail_url" jdbcType="VARCHAR"/>
+            <result property="imageDrawUrl" column="image_draw_url" jdbcType="VARCHAR"/>
+            <result property="videoUrl" column="video_url" jdbcType="VARCHAR"/>
+            <result property="deviceUuid" column="device_uuid" jdbcType="CHAR"/>
+            <result property="skillUuid" column="skill_uuid" jdbcType="CHAR"/>
+            <result property="annotationsJson" column="annotations_json" jdbcType="VARCHAR"/>
+            <result property="snapshotsJson" column="snapshots_json" jdbcType="VARCHAR"/>
+            <result property="userUuid" column="user_uuid" jdbcType="VARCHAR"/>
+            <result property="roisJson" column="rois_json" jdbcType="VARCHAR"/>
+            <result property="roisUuid" column="rois_uuid" jdbcType="CHAR"/>
+            <result property="trackUuid" column="track_uuid" jdbcType="CHAR"/>
+            <result property="figures" column="figures" jdbcType="VARCHAR"/>
+            <result property="type" column="type" jdbcType="CHAR"/>
+            <result property="triggerType" column="trigger_type" jdbcType="CHAR"/>
+            <result property="robotUuid" column="robot_uuid" jdbcType="CHAR"/>
+            <result property="location" column="location" jdbcType="DOUBLE"/>
+            <result property="reason" column="reason" jdbcType="VARCHAR"/>
+            <result property="isCanceled" column="is_canceled" jdbcType="TINYINT"/>
+            <result property="isUpgraded" column="is_upgraded" jdbcType="TINYINT"/>
+            <result property="cancelAlarmTime" column="cancel_alarm_time" jdbcType="INTEGER"/>
+            <result property="dataSource" column="data_source" jdbcType="CHAR"/>
+            <result property="ptzPanAngle" column="ptz_pan_angle" jdbcType="DECIMAL"/>
+            <result property="ptzTitleAngle" column="ptz_title_angle" jdbcType="DECIMAL"/>
+            <result property="meter" column="meter" jdbcType="TINYINT"/>
+            <result property="alarmGroupId" column="alarm_group_id" jdbcType="CHAR"/>
+            <result property="spotUuid" column="spot_uuid" jdbcType="CHAR"/>
+            <result property="isPublished" column="is_published" jdbcType="TINYINT"/>
+            <result property="isCollected" column="is_collected" jdbcType="TINYINT"/>
+            <result property="pondingType" column="ponding_type" jdbcType="VARCHAR"/>
+            <result property="labels" column="labels" jdbcType="VARCHAR"/>
+            <result property="aiFilter" column="ai_filter" jdbcType="INTEGER"/>
+            <result property="processUser" column="process_user" jdbcType="VARCHAR"/>
+            <result property="thresholds" column="thresholds" jdbcType="VARCHAR"/>
+            <result property="multiSkillUuid" column="multi_skill_uuid" jdbcType="CHAR"/>
+            <result property="filterTaskUuid" column="filter_task_uuid" jdbcType="CHAR"/>
+            <result property="edgeUuid" column="edge_uuid" jdbcType="CHAR"/>
+    </resultMap>
+
+    <sql id="Base_Column_List">
+        id,uuid,created_at,
+        updated_at,deleted_at,dept_uuid,
+        org_uuid,name,description,
+        status,display_status,level,
+        image_url,thumbnail_url,image_draw_url,
+        video_url,device_uuid,skill_uuid,
+        annotations_json,snapshots_json,user_uuid,
+        rois_json,rois_uuid,track_uuid,
+        figures,type,trigger_type,
+        robot_uuid,location,reason,
+        is_canceled,is_upgraded,cancel_alarm_time,
+        data_source,ptz_pan_angle,ptz_title_angle,
+        meter,alarm_group_id,spot_uuid,
+        is_published,is_collected,ponding_type,
+        labels,ai_filter,process_user,
+        thresholds,multi_skill_uuid,filter_task_uuid,
+        edge_uuid
+    </sql>
+</mapper>

+ 61 - 0
src/main/resources/mybatis/mapper/DevicesSoundSensorsMapper.xml

@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper
+        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.example.unusualsounds.project.device.mapper.DevicesSoundSensorsMapper">
+
+    <resultMap id="BaseResultMap" type="com.example.unusualsounds.project.device.entity.DevicesSoundSensors">
+            <id property="id" column="id" jdbcType="BIGINT"/>
+            <result property="uuid" column="uuid" jdbcType="CHAR"/>
+            <result property="deptUuid" column="dept_uuid" jdbcType="CHAR"/>
+            <result property="name" column="name" jdbcType="VARCHAR"/>
+            <result property="streamUrl" column="stream_url" jdbcType="VARCHAR"/>
+            <result property="analysisSdUrl" column="analysis_sd_url" jdbcType="VARCHAR"/>
+            <result property="analysisHdUrl" column="analysis_hd_url" jdbcType="VARCHAR"/>
+            <result property="srcUrl" column="src_url" jdbcType="VARCHAR"/>
+            <result property="description" column="description" jdbcType="VARCHAR"/>
+            <result property="ip" column="ip" jdbcType="VARCHAR"/>
+            <result property="createdAt" column="created_at" jdbcType="TIMESTAMP"/>
+            <result property="updatedAt" column="updated_at" jdbcType="TIMESTAMP"/>
+            <result property="deletedAt" column="deleted_at" jdbcType="TIMESTAMP"/>
+            <result property="status" column="status" jdbcType="CHAR"/>
+            <result property="isUsed" column="is_used" jdbcType="TINYINT"/>
+            <result property="imageUrl" column="image_url" jdbcType="VARCHAR"/>
+            <result property="imageResolutionJson" column="image_resolution_json" jdbcType="VARCHAR"/>
+            <result property="userUuid" column="user_uuid" jdbcType="CHAR"/>
+            <result property="orgUuid" column="org_uuid" jdbcType="CHAR"/>
+            <result property="type" column="type" jdbcType="VARCHAR"/>
+            <result property="streamId" column="stream_id" jdbcType="VARCHAR"/>
+            <result property="transferUrl" column="transfer_url" jdbcType="VARCHAR"/>
+            <result property="streamHost" column="stream_host" jdbcType="VARCHAR"/>
+            <result property="robotId" column="robot_id" jdbcType="CHAR"/>
+            <result property="cameraMatrixJson" column="camera_matrix_json" jdbcType="VARCHAR"/>
+            <result property="dataSource" column="data_source" jdbcType="CHAR"/>
+            <result property="gbId" column="gb_id" jdbcType="CHAR"/>
+            <result property="gbRootId" column="gb_root_id" jdbcType="CHAR"/>
+            <result property="location" column="location" jdbcType="FLOAT"/>
+            <result property="isTurninged" column="is_turninged" jdbcType="TINYINT"/>
+            <result property="spotUuid" column="spot_uuid" jdbcType="CHAR"/>
+            <result property="department" column="department" jdbcType="VARCHAR"/>
+            <result property="director" column="director" jdbcType="VARCHAR"/>
+            <result property="videoType" column="video_type" jdbcType="CHAR"/>
+            <result property="fileUuid" column="file_uuid" jdbcType="CHAR"/>
+            <result property="skillUuid" column="skill_uuid" jdbcType="CHAR"/>
+            <result property="responsiblePerson" column="responsible_person" jdbcType="TINYINT"/>
+    </resultMap>
+
+    <sql id="Base_Column_List">
+        id,uuid,dept_uuid,
+        name,stream_url,analysis_sd_url,
+        analysis_hd_url,src_url,description,
+        ip,created_at,updated_at,
+        deleted_at,status,is_used,
+        image_url,image_resolution_json,user_uuid,
+        org_uuid,type,stream_id,
+        transfer_url,stream_host,robot_id,
+        camera_matrix_json,data_source,gb_id,
+        gb_root_id,location,is_turninged,
+        spot_uuid,department,director,
+        video_type,file_uuid,skill_uuid,responsible_person
+    </sql>
+</mapper>

+ 22 - 0
src/main/resources/mybatis/mapper/EdgeOrganizationMapper.xml

@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper
+        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.example.unusualsounds.project.vox.mapper.EdgeOrganizationMapper">
+
+    <resultMap id="BaseResultMap" type="com.example.unusualsounds.project.vox.entity.EdgeOrganization">
+            <id property="id" column="id" jdbcType="BIGINT"/>
+            <result property="createdAt" column="created_at" jdbcType="TIMESTAMP"/>
+            <result property="updatedAt" column="updated_at" jdbcType="TIMESTAMP"/>
+            <result property="deletedAt" column="deleted_at" jdbcType="TIMESTAMP"/>
+            <result property="deptUuid" column="dept_uuid" jdbcType="CHAR"/>
+            <result property="orgUuid" column="org_uuid" jdbcType="CHAR"/>
+            <result property="edgeUuid" column="edge_uuid" jdbcType="CHAR"/>
+    </resultMap>
+
+    <sql id="Base_Column_List">
+        id,created_at,updated_at,
+        deleted_at,dept_uuid,org_uuid,
+        edge_uuid
+    </sql>
+</mapper>

+ 50 - 0
src/main/resources/mybatis/mapper/SkillsMapper.xml

@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper
+        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.example.unusualsounds.project.vox.mapper.SkillsMapper">
+
+    <resultMap id="BaseResultMap" type="com.example.unusualsounds.project.vox.entity.Skills">
+            <id property="id" column="id" jdbcType="BIGINT"/>
+            <result property="uuid" column="uuid" jdbcType="CHAR"/>
+            <result property="createdAt" column="created_at" jdbcType="TIMESTAMP"/>
+            <result property="updatedAt" column="updated_at" jdbcType="TIMESTAMP"/>
+            <result property="deletedAt" column="deleted_at" jdbcType="TIMESTAMP"/>
+            <result property="deptUuid" column="dept_uuid" jdbcType="VARCHAR"/>
+            <result property="orgUuid" column="org_uuid" jdbcType="VARCHAR"/>
+            <result property="name" column="name" jdbcType="VARCHAR"/>
+            <result property="objectName" column="object_name" jdbcType="VARCHAR"/>
+            <result property="releasedVersion" column="released_version" jdbcType="INTEGER"/>
+            <result property="versionNums" column="version_nums" jdbcType="INTEGER"/>
+            <result property="description" column="description" jdbcType="VARCHAR"/>
+            <result property="status" column="status" jdbcType="CHAR"/>
+            <result property="compositionJson" column="composition_json" jdbcType="VARCHAR"/>
+            <result property="version" column="version" jdbcType="INTEGER"/>
+            <result property="userUuid" column="user_uuid" jdbcType="VARCHAR"/>
+            <result property="period" column="period" jdbcType="INTEGER"/>
+            <result property="modelPackageUuid" column="model_package_uuid" jdbcType="CHAR"/>
+            <result property="servingUuid" column="serving_uuid" jdbcType="CHAR"/>
+            <result property="failedReason" column="failed_reason" jdbcType="VARCHAR"/>
+            <result property="skillType" column="skill_type" jdbcType="VARCHAR"/>
+            <result property="imageUrl" column="image_url" jdbcType="VARCHAR"/>
+            <result property="defaultLevel" column="default_level" jdbcType="INTEGER"/>
+            <result property="mergeRule" column="merge_rule" jdbcType="VARCHAR"/>
+            <result property="robotMergeRule" column="robot_merge_rule" jdbcType="VARCHAR"/>
+            <result property="workspaceId" column="workspace_id" jdbcType="CHAR"/>
+            <result property="graphName" column="graph_name" jdbcType="VARCHAR"/>
+            <result property="graphKind" column="graph_kind" jdbcType="CHAR"/>
+    </resultMap>
+
+    <sql id="Base_Column_List">
+        id,uuid,created_at,
+        updated_at,deleted_at,dept_uuid,
+        org_uuid,name,object_name,
+        released_version,version_nums,description,
+        status,composition_json,version,
+        user_uuid,period,model_package_uuid,
+        serving_uuid,failed_reason,skill_type,
+        image_url,default_level,merge_rule,
+        robot_merge_rule,workspace_id,graph_name,
+        graph_kind
+    </sql>
+</mapper>

+ 24 - 0
src/main/resources/mybatis/mapper/VoiceTicketLogsMapper.xml

@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper
+        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.example.unusualsounds.project.vox.mapper.VoiceTicketLogsMapper">
+
+    <resultMap id="BaseResultMap" type="com.example.unusualsounds.project.vox.entity.VoiceTicketLogs">
+            <id property="id" column="id" jdbcType="BIGINT"/>
+            <result property="deviceUuid" column="device_uuid" jdbcType="CHAR"/>
+            <result property="skillUuid" column="skill_uuid" jdbcType="CHAR"/>
+            <result property="skillName" column="skill_name" jdbcType="VARCHAR"/>
+            <result property="startTimes" column="start_times" jdbcType="TIMESTAMP"/>
+            <result property="endTimes" column="end_times" jdbcType="TIMESTAMP"/>
+            <result property="open" column="open" jdbcType="INTEGER"/>
+            <result property="remark" column="remark" jdbcType="VARCHAR"/>
+            <result property="openUuid" column="skill_uuid" jdbcType="CHAR"/>
+    </resultMap>
+
+    <sql id="Base_Column_List">
+        id,device_uuid,skill_uuid,
+        skill_name,start_times,end_times,
+        open,remark,open_uuid
+    </sql>
+</mapper>

+ 20 - 0
src/main/resources/mybatis/mybatis-config.xml

@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE configuration
+        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-config.dtd">
+<configuration>
+    <!-- 全局参数 -->
+    <settings>
+        <!-- 使全局的映射器启用或禁用缓存 -->
+        <setting name="cacheEnabled" value="true"/>
+        <!-- 允许JDBC 支持自动生成主键 -->
+        <setting name="useGeneratedKeys" value="true"/>
+        <!-- 配置默认的执行器.SIMPLE就是普通执行器;REUSE执行器会重用预处理语句(prepared statements);BATCH执行器将重用语句并执行批量更新 -->
+        <setting name="defaultExecutorType" value="SIMPLE"/>
+        <!-- 指定 MyBatis 所用日志的具体实现 -->
+        <setting name="logImpl" value="SLF4J"/>
+        <!-- 使用驼峰命名法转换字段 -->
+        <!-- <setting name="mapUnderscoreToCamelCase" value="true"/> -->
+    </settings>
+
+</configuration>

+ 243 - 0
src/test/java/com/example/unusualsounds/UnusualSoundsApplicationTests.java

@@ -0,0 +1,243 @@
+package com.example.unusualsounds;
+
+import static com.baidubce.auth.SignOptions.DEFAULT;
+
+import com.alibaba.fastjson2.JSON;
+import com.baidubce.http.HttpMethodName;
+import com.example.unusualsounds.common.utils.BaiDuUtils;
+import com.example.unusualsounds.common.utils.DateUtils;
+import com.example.unusualsounds.common.utils.RedisUtils;
+import com.example.unusualsounds.common.utils.UUID;
+import com.example.unusualsounds.project.device.entity.vo.SoundSensorsQueryVo;
+import com.example.unusualsounds.project.vox.entity.AlarmReports;
+import com.example.unusualsounds.project.vox.entity.Alarms;
+import com.example.unusualsounds.project.vox.entity.VoiceTicketLogs;
+import com.example.unusualsounds.project.vox.service.impl.*;
+import com.example.unusualsounds.web.jsonobj.CameraDTO;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.http.ResponseEntity;
+
+import java.io.IOException;
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import com.baidubce.auth.BceCredentials;
+import com.baidubce.auth.BceV1Signer;
+import com.baidubce.auth.DefaultBceCredentials;
+import com.baidubce.internal.InternalRequest;
+import com.baidubce.util.JsonUtils;
+import java.net.URI;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.client.RestTemplate;
+
+
+
+@SpringBootTest
+@Slf4j
+class UnusualSoundsApplicationTests {
+
+    /**
+     * 请求地址
+     */
+    public static String endpoint = "http://10.68.204.52:8412";
+    /**
+     * AK, 从 idaas console获取
+     */
+    public static String ak = "fd01ec4de8724d71bb764190f202929c";
+    /**
+     * SK, 从 idaas console获取
+     */
+    public static String sk = "f44a391cadcc42f98167e7e42086514f";
+    /**
+     * 请求路径前缀,如果能够直接请求一见后端服务的地址,则置为空
+     */
+    public static String uriPathPrefix = "/api/spi";
+
+    /**
+     * 测试百度AKSK
+     */
+    @Test
+    void testAKSK(){
+        String uri = "/v1/devices";
+        // 请求参数中携带
+        Map<String, String> paramsMap = new HashMap<>();
+        paramsMap.put("pageNo", "1");
+        paramsMap.put("pageSize", "10");
+        // 部门id,根据用户id自己获取
+//        paramsMap.put("deptID", "xxxx");
+
+        ResponseEntity<String> response = BaiDuUtils.sendRequest(uri, HttpMethodName.GET, null, paramsMap, String.class, ak, sk, endpoint, uriPathPrefix);
+        System.out.println(response.getStatusCode());
+        System.out.println(response.getBody());
+
+    }
+
+
+
+    @Test
+    void contextLoads() {
+
+//        System.out.println("进行修复视频:");
+//        repairIncompleteVideo();
+        // 定义日志字符串
+        String log = "[segment @ 0000011bc2a04d00] Opening 'D:\\tmp\\data\\vox\\output-2025-02-14_17-36-408da58aa6-50d8-45cd-ac09-f393e0ae3f7e.mp4' for writing";
+        Pattern pattern = Pattern.compile("Opening '([^']+\\.mp4)'");
+        Matcher matcher = pattern.matcher(log);
+
+        // 查找匹配
+        if (matcher.find()) {
+            // 获取匹配到的文件路径
+            String filePath = matcher.group(1);
+            System.out.println("Found file path: " + filePath);
+        } else {
+            System.out.println("No match found.");
+        }
+    }
+
+    /**
+     * ffmpeg修复视频文件
+     */
+    private void repairIncompleteVideo() {
+        // 使用FFmpeg修复视频,这里假设我们需要确保视频和音频流正确封装
+        List<String> command = Arrays.asList(
+                "D:\\file\\ffmpeg\\ffmpeg.exe",
+                "-i", "D:\\tmp\\data\\vox\\output-2025-02-14_14-20-581d6c0f9f-daeb-41a6-b072-30fcddeee451.mp4",
+                "-c", "copy",
+                "-movflags", "+faststart",
+                "-fflags", "+genpts",
+                "D:\\tmp\\data\\vox\\output-2025-02-14_14-20-581d6c0f9f-daeb-41a6-b072-30fcddeee451.mp4".replace(".mp4", "_repaired.mp4")
+        );
+
+        ProcessBuilder pb = new ProcessBuilder(command)
+                .redirectErrorStream(true);
+
+        try {
+            Process repairProcess = pb.start();
+            int exitCode = repairProcess.waitFor();
+            if (exitCode == 0) {
+                log.info("视频文件 已成功修复");
+
+            } else {
+                log.error("视频文件 修复失败");
+            }
+        } catch (IOException | InterruptedException e) {
+            log.error("视频文件修复过程中出现异常", e);
+        }
+    }
+
+    @Autowired
+    private VoxServiceImpl voxService;
+    @Test
+    void testGetDeviceInfo(){
+
+//        CameraDTO cameraDTO = voxService.checkDevice("2c04027ed02e4d539ccdd568441dbd32");
+//        System.out.println(cameraDTO.toString());
+
+        String getOrganization = voxService.getGetOrganization();
+        System.out.println(getOrganization);
+
+    }
+
+    @Autowired
+    private AlarmReportsServiceImpl alarmReportsService;
+
+    @Test
+    void testRedis(){
+//        RedisUtils.set("test","123",10, TimeUnit.HOURS);
+//        System.out.println("获取:"+RedisUtils.get("test"));
+//        System.out.println("是否存在:"+RedisUtils.exists("test"));
+        List<AlarmReports> alarmReports = alarmReportsService.getBaseMapper().selectList(null);
+        alarmReports.forEach(alarmReports1 -> {
+            System.out.println("-->"+alarmReports1.toString());});
+    }
+
+    @Autowired
+    private AlarmsServiceImpl alarmsServiceImpl;
+    @Test
+    void testAlarms(){
+
+        Alarms alarms = new Alarms.Builder().build();
+
+        alarms.setUuid(UUID.generateUUIDWithoutHyphens());
+        alarms.setDescription("测试报警,设备异音");
+        RedisUtils.setForeverKey(alarms.getUuid(),alarms);
+        Object o = RedisUtils.get(alarms.getUuid());
+        Alarms alarms1 = JSON.parseObject(o.toString(), Alarms.class);
+        System.out.println("redis返回:"+alarms1.toString());
+
+    }
+
+    @Autowired
+    private EdgeOrganizationServiceImpl organizationService;
+    @Test
+    void testOrganization(){
+
+//        HashMap<String, String> organizations = organizationService.getOrganizations();
+//
+//        organizations.forEach((k,v)->{
+//            System.out.println("key:"+k+" value:"+v);
+//        });
+
+
+//        ResponseEntity<String> response = BaiDuUtils.sendRequest("/v1/configurations",
+//                HttpMethodName.GET, null, null, String.class, ak, sk, endpoint, uriPathPrefix);
+//
+//        if(response.getStatusCode().is2xxSuccessful()){
+//            System.out.println("成功");
+//            System.out.println(JSON.parseObject(response.getBody()).toString());
+//        }
+
+        String uuid1= UUID.generateUUIDv5("testVideo.mp4");
+        String uuid2= UUID.generateUUIDv5("testVideo.mp4");
+        String uuid3= UUID.generateUUIDv5("testVideo.mp4");
+        System.out.println("uuid1:"+UUID.removeHyphens(uuid1));
+        System.out.println("uuid2:"+uuid2);
+        System.out.println("uuid3:"+uuid3);
+
+    }
+
+
+    @Autowired
+    private VoiceTicketLogsServiceImpl voiceTicketLogsServiceImpl;
+    @Test
+    void testTickets(){
+
+        HashMap<String, String> hashMap = new HashMap<>();
+
+        hashMap.put("29f01235ec1141608c099c63db0af4c6","623080afe45446d9a0088d3c2bd78c2b_设备异音测试");
+        hashMap.put("e093da78edab47bca5b99b4dcc9cf990","623080afe45446d9a0088d3c2bd78c2b_设备异音测试");
+        hashMap.put("109e8b333bf1443087f7dae80c52713b","2c04012ed02e4d53aqq1d5684584bd34_人声呼救测试");
+        hashMap.put("3a91aab4f2a94a8189793849e3d15b11","622080afe45446d9a0088d3c2bd78c2b_人声呼救测试");
+
+
+        List<VoiceTicketLogs> voiceTicketLogs = voiceTicketLogsServiceImpl.contrastTasks(hashMap);
+        voiceTicketLogs.forEach(voicetask -> {
+            System.out.println("->:"+voicetask.toString());
+        });
+        List<String> collect = voiceTicketLogs.stream()
+                .map(voicetask -> {
+                    return voicetask.getDeviceUuid() + "_" + voicetask.getSkillName();
+                }).collect(Collectors.toList());
+
+        collect.forEach(s -> System.out.println("需要停止的工单:"+s));
+
+
+    }
+
+}