Selaa lähdekoodia

添加hhface-common和hhface-middleware模块

lsh 2 kuukautta sitten
vanhempi
commit
22fad18633
99 muutettua tiedostoa jossa 7145 lisäystä ja 0 poistoa
  1. 171 0
      hhface-common/pom.xml
  2. 18 0
      hhface-common/src/main/java/cn/hh/common/CommonApplication.java
  3. 18 0
      hhface-common/src/main/java/cn/hh/common/bean/BaseBean.java
  4. 55 0
      hhface-common/src/main/java/cn/hh/common/config/Constants.java
  5. 103 0
      hhface-common/src/main/java/cn/hh/common/config/RedisConfiguration.java
  6. 71 0
      hhface-common/src/main/java/cn/hh/common/config/ThreadPoolConfig.java
  7. 90 0
      hhface-common/src/main/java/cn/hh/common/constants/CommonConstants.java
  8. 80 0
      hhface-common/src/main/java/cn/hh/common/enumeration/BusinessTypeEnum.java
  9. 24 0
      hhface-common/src/main/java/cn/hh/common/enumeration/FetchLogTypeEnum.java
  10. 41 0
      hhface-common/src/main/java/cn/hh/common/enumeration/TaskStatusEnum.java
  11. 23 0
      hhface-common/src/main/java/cn/hh/common/enumeration/UpgradeTypeEnum.java
  12. 52 0
      hhface-common/src/main/java/cn/hh/common/restful/BusinessException.java
  13. 138 0
      hhface-common/src/main/java/cn/hh/common/restful/GlobalExceptionHandler.java
  14. 40 0
      hhface-common/src/main/java/cn/hh/common/restful/RestCode.java
  15. 66 0
      hhface-common/src/main/java/cn/hh/common/restful/RestDTO.java
  16. 48 0
      hhface-common/src/main/java/cn/hh/common/restful/RestResponse.java
  17. 64 0
      hhface-common/src/main/java/cn/hh/common/restful/RestResult.java
  18. 25 0
      hhface-common/src/main/java/cn/hh/common/serializer/BigDecimalSerializer.java
  19. 51 0
      hhface-common/src/main/java/cn/hh/common/serializer/LongSerializer.java
  20. 20 0
      hhface-common/src/main/java/cn/hh/common/utils/AppTokenUtil.java
  21. 101 0
      hhface-common/src/main/java/cn/hh/common/utils/AppUtils.java
  22. 442 0
      hhface-common/src/main/java/cn/hh/common/utils/DateUtils.java
  23. 69 0
      hhface-common/src/main/java/cn/hh/common/utils/EnumUtils.java
  24. 323 0
      hhface-common/src/main/java/cn/hh/common/utils/ExcelUtil.java
  25. 82 0
      hhface-common/src/main/java/cn/hh/common/utils/FileUtils.java
  26. 317 0
      hhface-common/src/main/java/cn/hh/common/utils/IPUtils.java
  27. 304 0
      hhface-common/src/main/java/cn/hh/common/utils/ImageUtil.java
  28. 45 0
      hhface-common/src/main/java/cn/hh/common/utils/ListUtils.java
  29. 18 0
      hhface-common/src/main/java/cn/hh/common/utils/PasswordUtils.java
  30. 67 0
      hhface-common/src/main/java/cn/hh/common/utils/Preconditions.java
  31. 168 0
      hhface-common/src/main/java/cn/hh/common/utils/QrCodeUtils.java
  32. 168 0
      hhface-common/src/main/java/cn/hh/common/utils/RandomUtils.java
  33. 53 0
      hhface-common/src/main/java/cn/hh/common/utils/RequestUriUtils.java
  34. 117 0
      hhface-common/src/main/java/cn/hh/common/utils/Sm4Utils.java
  35. 203 0
      hhface-common/src/main/java/cn/hh/common/utils/SnowflakeIdWorker.java
  36. 26 0
      hhface-middleware/DockerFile
  37. 84 0
      hhface-middleware/pom.xml
  38. 27 0
      hhface-middleware/src/main/java/cn/hh/hhface/HhFaceMiddlewareApplication.java
  39. 22 0
      hhface-middleware/src/main/java/cn/hh/hhface/bean/ChunkPO.java
  40. 25 0
      hhface-middleware/src/main/java/cn/hh/hhface/bean/FilePO.java
  41. 41 0
      hhface-middleware/src/main/java/cn/hh/hhface/bean/account/AccountDto.java
  42. 36 0
      hhface-middleware/src/main/java/cn/hh/hhface/bean/account/AccountLoginVo.java
  43. 47 0
      hhface-middleware/src/main/java/cn/hh/hhface/bean/account/AccountPageDto.java
  44. 64 0
      hhface-middleware/src/main/java/cn/hh/hhface/bean/account/AccountVo.java
  45. 53 0
      hhface-middleware/src/main/java/cn/hh/hhface/bean/apk/AppApkDto.java
  46. 51 0
      hhface-middleware/src/main/java/cn/hh/hhface/bean/apk/AppApkEditDto.java
  47. 47 0
      hhface-middleware/src/main/java/cn/hh/hhface/bean/apk/AppApkPageDto.java
  48. 73 0
      hhface-middleware/src/main/java/cn/hh/hhface/bean/apk/AppApkVo.java
  49. 18 0
      hhface-middleware/src/main/java/cn/hh/hhface/bean/device/DeviceAppRestartDecryptDto.java
  50. 30 0
      hhface-middleware/src/main/java/cn/hh/hhface/bean/device/DeviceBizDataDto.java
  51. 24 0
      hhface-middleware/src/main/java/cn/hh/hhface/bean/device/DeviceCloseDto.java
  52. 26 0
      hhface-middleware/src/main/java/cn/hh/hhface/bean/device/DeviceDto.java
  53. 28 0
      hhface-middleware/src/main/java/cn/hh/hhface/bean/device/DeviceFilePathDto.java
  54. 27 0
      hhface-middleware/src/main/java/cn/hh/hhface/bean/device/DeviceFilePathPageDto.java
  55. 35 0
      hhface-middleware/src/main/java/cn/hh/hhface/bean/device/DeviceFilePathVo.java
  56. 28 0
      hhface-middleware/src/main/java/cn/hh/hhface/bean/device/DeviceHeartTrackingDto.java
  57. 19 0
      hhface-middleware/src/main/java/cn/hh/hhface/bean/device/DeviceInfoDecryptDto.java
  58. 58 0
      hhface-middleware/src/main/java/cn/hh/hhface/bean/device/DeviceInfoVo.java
  59. 55 0
      hhface-middleware/src/main/java/cn/hh/hhface/bean/device/DevicePageDto.java
  60. 70 0
      hhface-middleware/src/main/java/cn/hh/hhface/bean/device/DeviceVo.java
  61. 28 0
      hhface-middleware/src/main/java/cn/hh/hhface/bean/device/DownloadLogDto.java
  62. 30 0
      hhface-middleware/src/main/java/cn/hh/hhface/bean/device/FetchLogDto.java
  63. 21 0
      hhface-middleware/src/main/java/cn/hh/hhface/bean/device/SendZfb5YearMsgDto.java
  64. 31 0
      hhface-middleware/src/main/java/cn/hh/hhface/bean/device/Sm4EncryptDto.java
  65. 25 0
      hhface-middleware/src/main/java/cn/hh/hhface/bean/heart/HeartBeatBase.java
  66. 16 0
      hhface-middleware/src/main/java/cn/hh/hhface/bean/heart/OfflineStatusPushReq.java
  67. 41 0
      hhface-middleware/src/main/java/cn/hh/hhface/bean/heart/OnlineStatusPushDataReq.java
  68. 38 0
      hhface-middleware/src/main/java/cn/hh/hhface/bean/heart/OnlineStatusPushReq.java
  69. 52 0
      hhface-middleware/src/main/java/cn/hh/hhface/bean/isssue/WebsocketMsgReq.java
  70. 37 0
      hhface-middleware/src/main/java/cn/hh/hhface/bean/log/AppLogDto.java
  71. 43 0
      hhface-middleware/src/main/java/cn/hh/hhface/bean/log/AppLogPageDto.java
  72. 35 0
      hhface-middleware/src/main/java/cn/hh/hhface/bean/log/AppLogReportDto.java
  73. 41 0
      hhface-middleware/src/main/java/cn/hh/hhface/bean/log/AppLogVo.java
  74. 32 0
      hhface-middleware/src/main/java/cn/hh/hhface/bean/task/AppApkDeviceTaskDto.java
  75. 22 0
      hhface-middleware/src/main/java/cn/hh/hhface/bean/task/AppApkDeviceTaskErrorVo.java
  76. 50 0
      hhface-middleware/src/main/java/cn/hh/hhface/bean/task/AppApkDeviceTaskPageDto.java
  77. 22 0
      hhface-middleware/src/main/java/cn/hh/hhface/bean/task/AppApkDeviceTaskReq.java
  78. 73 0
      hhface-middleware/src/main/java/cn/hh/hhface/bean/task/AppApkDeviceTaskVo.java
  79. 39 0
      hhface-middleware/src/main/java/cn/hh/hhface/bean/task/DeviceTaskStatusVo.java
  80. 24 0
      hhface-middleware/src/main/java/cn/hh/hhface/bean/websocket/WebsocketSenderDto.java
  81. 17 0
      hhface-middleware/src/main/java/cn/hh/hhface/config/Authority.java
  82. 47 0
      hhface-middleware/src/main/java/cn/hh/hhface/config/NioWebSocketChannelInitializer.java
  83. 71 0
      hhface-middleware/src/main/java/cn/hh/hhface/config/NioWebSocketChannelPool.java
  84. 309 0
      hhface-middleware/src/main/java/cn/hh/hhface/config/NioWebSocketHandler.java
  85. 75 0
      hhface-middleware/src/main/java/cn/hh/hhface/config/NioWebSocketServer.java
  86. 30 0
      hhface-middleware/src/main/java/cn/hh/hhface/config/PrintBanner.java
  87. 94 0
      hhface-middleware/src/main/java/cn/hh/hhface/config/RedisConfig.java
  88. 88 0
      hhface-middleware/src/main/java/cn/hh/hhface/config/RequestInterceptor.java
  89. 82 0
      hhface-middleware/src/main/java/cn/hh/hhface/config/SwaggerConfig.java
  90. 124 0
      hhface-middleware/src/main/java/cn/hh/hhface/config/WebAppConfig.java
  91. 24 0
      hhface-middleware/src/main/java/cn/hh/hhface/config/WebSocketProperties.java
  92. 37 0
      hhface-middleware/src/main/java/cn/hh/hhface/controller/HhFaceIssueController.java
  93. 103 0
      hhface-middleware/src/main/java/cn/hh/hhface/jwt/JwtService.java
  94. 409 0
      hhface-middleware/src/main/java/cn/hh/hhface/service/WebsocketService.java
  95. 81 0
      hhface-middleware/src/main/resources/application-dev.yml
  96. 80 0
      hhface-middleware/src/main/resources/application-prod.yml
  97. 80 0
      hhface-middleware/src/main/resources/application-test.yml
  98. 3 0
      hhface-middleware/src/main/resources/application.yml
  99. 2 0
      pom.xml

+ 171 - 0
hhface-common/pom.xml

@@ -0,0 +1,171 @@
+<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <groupId>com.yx.face</groupId>
+        <artifactId>nm-yqk</artifactId>
+        <version>1.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>hhface-common</artifactId>
+<!--    <version>${project.version}</version>-->
+
+    <properties>
+        <skipTests>true</skipTests>
+        <guava.version>30.1.1-jre</guava.version>
+        <fastjson.version>1.2.60</fastjson.version>
+        <thumbnailator.version>0.4.8</thumbnailator.version>
+        <zxing.core.version>3.3.3</zxing.core.version>
+        <zxing.javase.version>3.3.3</zxing.javase.version>
+        <jakarta.persistence-api.version>2.2.3</jakarta.persistence-api.version>
+        <crypto-gmsm.version>1.0</crypto-gmsm.version>
+        <commons-io.version>2.8.0</commons-io.version>
+        <commons-lang3.version>3.12.0</commons-lang3.version>
+        <commons-codec.version>1.15</commons-codec.version>
+        <okhttp.version>3.10.0</okhttp.version>
+        <hutool-all.version>5.8.11</hutool-all.version>
+        <java-jw.version>3.14.0</java-jw.version>
+        <spring-boot-starter-data-redis.version>2.2.6.RELEASE</spring-boot-starter-data-redis.version>
+        <easyexcel.version>2.2.8</easyexcel.version>
+        <knife4j-spring-boot-starter.version>3.0.2</knife4j-spring-boot-starter.version>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-redis</artifactId>
+            <version>${spring-boot-starter-data-redis.version}</version>
+        </dependency>
+        <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-pool2 -->
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-pool2</artifactId>
+            <version>2.11.1</version>
+        </dependency>
+
+        <dependency>
+            <groupId>commons-io</groupId>
+            <artifactId>commons-io</artifactId>
+            <version>${commons-io.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+            <version>${commons-lang3.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>commons-codec</groupId>
+            <artifactId>commons-codec</artifactId>
+            <version>${commons-codec.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+            <version>${guava.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.squareup.okhttp3</groupId>
+            <artifactId>okhttp</artifactId>
+            <version>${okhttp.version}</version>
+        </dependency>
+        <!-- https://mvnrepository.com/artifact/cn.hutool/hutool-all -->
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-all</artifactId>
+            <version>${hutool-all.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>fastjson</artifactId>
+            <version>${fastjson.version}</version>
+        </dependency>
+        <!--========================【EXCEL相关依赖】===============================-->
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>easyexcel</artifactId>
+            <version>${easyexcel.version}</version>
+        </dependency>
+        <!--========================【在线接口文档相关依赖】===============================-->
+        <dependency>
+            <groupId>com.github.xiaoymin</groupId>
+            <artifactId>knife4j-spring-boot-starter</artifactId>
+            <version>${knife4j-spring-boot-starter.version}</version>
+        </dependency>
+        <!--========================【接口安全相关依赖】===============================-->
+        <dependency>
+            <groupId>com.auth0</groupId>
+            <artifactId>java-jwt</artifactId>
+            <version>${java-jw.version}</version>
+        </dependency>
+        <!-- ...............二维码生成器......................................................... -->
+        <dependency>
+            <groupId>com.google.zxing</groupId>
+            <artifactId>core</artifactId>
+            <version>${zxing.core.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.google.zxing</groupId>
+            <artifactId>javase</artifactId>
+            <version>${zxing.javase.version}</version>
+        </dependency>
+        <!-- ...............图片压缩处理......................................................... -->
+        <dependency>
+            <groupId>net.coobird</groupId>
+            <artifactId>thumbnailator</artifactId>
+            <version>${thumbnailator.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>cn.superfw</groupId>
+            <artifactId>crypto-gmsm</artifactId>
+            <version>${crypto-gmsm.version}</version>
+        </dependency>
+        <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <version>1.18.24</version>
+        </dependency>
+        <dependency>
+            <groupId>jakarta.persistence</groupId>
+            <artifactId>jakarta.persistence-api</artifactId>
+            <version>${jakarta.persistence-api.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.json</groupId>
+            <artifactId>json</artifactId>
+            <version>20160810</version>
+        </dependency>
+        <dependency>
+            <groupId>commons-httpclient</groupId>
+            <artifactId>commons-httpclient</artifactId>
+            <version>3.1</version>
+        </dependency>
+
+        <!-- https://mvnrepository.com/artifact/org.dom4j/dom4j -->
+        <dependency>
+            <groupId>org.dom4j</groupId>
+            <artifactId>dom4j</artifactId>
+            <version>2.1.3</version>
+        </dependency>
+
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-jar-plugin</artifactId>
+                <!--                <version>3.1.0</version>-->
+            </plugin>
+        </plugins>
+    </build>
+
+</project>

+ 18 - 0
hhface-common/src/main/java/cn/hh/common/CommonApplication.java

@@ -0,0 +1,18 @@
+package cn.hh.common;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ * @author shisl
+ * @package cn.hh.common
+ * @class CommonApplication
+ * @date 2023/12/7 下午1:39
+ * @description 启动类
+ */
+@SpringBootApplication(scanBasePackages = {"cn.hh"})
+public class CommonApplication {
+    public static void main(String[] args) {
+        SpringApplication.run(CommonApplication.class, args);
+    }
+}

+ 18 - 0
hhface-common/src/main/java/cn/hh/common/bean/BaseBean.java

@@ -0,0 +1,18 @@
+package cn.hh.common.bean;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * @author shisl
+ * @package cn.hh.common.bean
+ * @class BaseBean
+ * @date 2023/12/7 下午1:42
+ * @description
+ */
+@Data
+public class BaseBean implements Serializable {
+
+
+}

+ 55 - 0
hhface-common/src/main/java/cn/hh/common/config/Constants.java

@@ -0,0 +1,55 @@
+package cn.hh.common.config;
+
+/**
+ * @description: Constants <br>
+ * @date: 2021/3/29 17:08 <br>
+ * @author: PWB <br>
+ */
+public class Constants {
+
+    public static final String UPLOAD_PATH = "file/image/upload/";
+    public static final String UPLOAD_IMAGE = "file/upload/images/";
+    public static final String UPLOAD_IMAGE_NEW = "file/upload/imagesnew/";
+    public static final String UPLOAD_EXCEL = "file/upload/excel/";
+    public static final String UPLOAD_PHOTO = "file/upload/photo/";
+    public static final String UPLOAD_VIDEO = "file/upload/videos/";
+    public static final String UPLOAD_DOCUMENT = "file/upload/document/";
+    public static final String UPLOAD_OTHER = "file/upload/other";
+    public static final String SYSTEM_FILE = "file/system/";
+    public static final String SALT = "YcYp65l;uq234pwb;iIjHg872S-ajfL.khp636**YX2021";
+    public static final String SALT_PRE = "SU2ti983*238--=833##383yuu";
+    public static final String UTF8 = "UTF-8";
+    public static final String DOT = ".";
+    public static final String COMMA = ",";
+    public static final String COLON = ":";
+    public static final String MIDLINE = "-";
+    public static final String UNDERLINE = "_";
+    public static final String SLASH = "/";
+    public static final String QUESTION_MARK = "?";
+    public static final String SEMICOLON = ";";
+    public static final String AND = "&";
+    public static final String STAR = "*";
+    public static final String SHARP = "#";
+    public static final String EQUAL = "=";
+    public static final String EMPTY_STRING = "";
+    public static final int ZERO = 0;
+    public static final int ONE = 1;
+    public static final int TWO = 2;
+    public static final int THREE = 3;
+    public static final int FOUR = 4;
+    public static final int FIVE = 5;
+    public static final int SIX = 6;
+    public static final int SEVEN = 7;
+    public static final int EIGHT = 8;
+    public static final int NINE = 9;
+    public static final String STR_ZERO = "0";
+    public static final String STR_ONE = "1";
+    public static final String STR_TWO = "2";
+    public static final String STR_THREE = "3";
+    public static final String STR_FOUR = "4";
+    public static final String STR_FIVE = "5";
+    public static final String STR_SIX = "6";
+    public static final String STR_SEVEN = "7";
+    public static final String STR_EIGHT = "8";
+    public static final String STR_NINE = "9";
+}

+ 103 - 0
hhface-common/src/main/java/cn/hh/common/config/RedisConfiguration.java

@@ -0,0 +1,103 @@
+package cn.hh.common.config;
+
+import cn.hh.common.serializer.LongSerializer;
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.PropertyAccessor;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.springframework.data.redis.cache.RedisCacheConfiguration;
+import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
+import org.springframework.data.redis.serializer.RedisSerializationContext;
+
+import java.time.Duration;
+
+/**
+ * @author shisl
+ * @package cn.hh.common.config
+ * @class RedisConfiguration
+ * @description redis配置
+ */
+public class RedisConfiguration {
+
+    /**
+     * 天数
+     *
+     * @param days
+     * @return
+     */
+    public static RedisCacheConfiguration getRedisCacheConfigurationWithDays(Long days) {
+        return getRedisCacheConfigurationWithTtl(Duration.ofDays(days));
+    }
+
+    /**
+     * 小时
+     *
+     * @param hours
+     * @return
+     */
+    public static RedisCacheConfiguration getRedisCacheConfigurationWithHours(Long hours) {
+        return getRedisCacheConfigurationWithTtl(Duration.ofHours(hours));
+    }
+
+    /**
+     * 秒数
+     *
+     * @param seconds
+     * @return
+     */
+    public static RedisCacheConfiguration getRedisCacheConfigurationWithSeconds(Long seconds) {
+        return getRedisCacheConfigurationWithTtl(Duration.ofSeconds(seconds));
+    }
+
+
+    private static RedisCacheConfiguration getRedisCacheConfigurationWithTtl(Duration ttl) {
+        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
+        ObjectMapper om = new ObjectMapper();
+        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
+        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
+        jackson2JsonRedisSerializer.setObjectMapper(om);
+
+        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
+        redisCacheConfiguration = redisCacheConfiguration
+                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
+                .entryTtl(ttl);
+
+        return redisCacheConfiguration;
+    }
+
+
+    public static RedisCacheConfiguration getRedisCacheConfigurationByLongWithHours(Long hours) {
+        return getRedisCacheConfigurationByLongWithTtl(Duration.ofHours(hours));
+    }
+
+
+    public static RedisCacheConfiguration getRedisCacheConfigurationByLongWithSeconds(Long seconds) {
+        return getRedisCacheConfigurationByLongWithTtl(Duration.ofSeconds(seconds));
+    }
+
+
+    public static RedisCacheConfiguration getRedisCacheConfigurationByLongWithDays(Long days) {
+        return getRedisCacheConfigurationByLongWithTtl(Duration.ofDays(days));
+    }
+
+    /**
+     * long serializer
+     *
+     * @param ttl
+     * @return
+     */
+    private static RedisCacheConfiguration getRedisCacheConfigurationByLongWithTtl(Duration ttl) {
+        LongSerializer longSerializer = new LongSerializer();
+        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
+        redisCacheConfiguration = redisCacheConfiguration
+                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(longSerializer))
+                .entryTtl(ttl);
+
+        return redisCacheConfiguration;
+    }
+
+
+
+
+
+
+}

+ 71 - 0
hhface-common/src/main/java/cn/hh/common/config/ThreadPoolConfig.java

@@ -0,0 +1,71 @@
+package cn.hh.common.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.stereotype.Component;
+
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.RejectedExecutionHandler;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * @author shisl
+ * @package cn.hh.common.config
+ * @class ThreadPoolConfig
+ * @description
+ */
+@Component
+public class ThreadPoolConfig {
+    /**
+     * 参数初始化
+     */
+    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
+    /**
+     * 核心线程数量大小
+     */
+    private static final int corePoolSize = Math.max(8, Math.min(CPU_COUNT * 2 - 1, 8));
+    /**
+     * 线程池最大容纳线程数
+     */
+    private static final int maximumPoolSize = CPU_COUNT * 4 + 1;
+    /**
+     * 线程空闲后的存活时长
+     */
+    private static final int keepAliveTime = 30;
+
+    /**
+     * 线程队列长度
+     */
+    BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<Runnable>(500, true);
+
+
+    /**
+     * 线程的创建工厂
+     */
+    ThreadFactory threadFactory = new ThreadFactory() {
+        private final AtomicInteger mCount = new AtomicInteger(1);
+
+        @Override
+        public Thread newThread(Runnable r) {
+            return new Thread(r, "thread #" + mCount.getAndIncrement());
+        }
+    };
+
+    /**
+     * 线程池任务满载后采取的任务拒绝策略
+     */
+    RejectedExecutionHandler rejectHandler = new ThreadPoolExecutor.DiscardOldestPolicy();
+
+
+    @Bean
+    public ThreadPoolExecutor threadPoolExecutor() {
+        /**
+         * 线程池对象,创建线程
+         */
+        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.SECONDS, workQueue, threadFactory, rejectHandler);
+        return threadPoolExecutor;
+    }
+}

+ 90 - 0
hhface-common/src/main/java/cn/hh/common/constants/CommonConstants.java

@@ -0,0 +1,90 @@
+package cn.hh.common.constants;
+
+/**
+ * @author shisl
+ * @package cn.hh.common.constants
+ * @class CommonConstants
+ * @date 2023/1/10 上午11:07
+ * @description
+ */
+public interface CommonConstants {
+
+    /**
+     * 角色关联权限的缓存key
+     */
+    String ROLE_AUTH_CACHE_KEY = "role:auth:%s";
+
+    String UTF8 = "utf-8";
+
+    String BASE64_PREFIX = "data:image/;base64,";
+
+    /**
+     * 站内信消息 SEND 系统发送,SHARE 用户分享
+     */
+    String PARTY_INNER_MSG_SOURCE_SEND = "SEND";
+    String PARTY_INNER_MSG_SOURCE_SHARE = "SHARE";
+
+    /**
+     * websocket
+     */
+    String WEBSOCKET_SN_KEY = "websocket:sn:%s";
+    String WEBSOCKET_PC_KEY = "websocket:pc:channel";
+    String WEBSOCKET_PC_TOKEN_KEY = "websocket:pc:token:%s";
+
+    String WEBSOCKET_SN_SOURCE_KEY = "websocket:sn:%s:%s";
+    String WEBSOCKET_PC_SOURCE_KEY = "websocket:pc:source:channel";
+    String WEBSOCKET_PC_TOKEN_SOURCE_KEY = "websocket:pc:token:%s:%s";
+
+
+    //websocket连接
+    String WEBSOCKET_CONNECT_PC = "token";
+    String WEBSOCKET_CONNECT_APP = "sn";
+    String WEBSOCKET_CONNECT_SOURCE = "source";
+    //来源
+    String WEBSOCKET_SOURCE = "source";
+    //来源--pc端
+    String WEBSOCKET_SOURCE_PC = "pc";
+    //来源--app端
+    String WEBSOCKET_SOURCE_APP = "app";
+    //来源--塘栖PC端
+    String WEBSOCKET_SOURCE_TQ_PC = "TQ";
+    //来源--塘栖服务端
+    String WEBSOCKET_SOURCE_TQ_SERVER = "TQSERVER";
+    //来源--HHFACE端
+    String WEBSOCKET_SOURCE_HHFACE_APP = "HHFACE";
+    //操作
+    String WEBSOCKET_OP = "op";
+    //关闭应用
+    String WEBSOCKET_OP_CLOSE_APP = "CLOSE_APP";
+    //卸载应用
+    String WEBSOCKET_OP_UNINSTALL_APP = "UNINSTALL_APP";
+    //重启设备
+    String WEBSOCKET_OP_REBOOT = "REBOOT";
+    //停止下载
+    String WEBSOCKET_OP_STOP_DOWNLOAD = "STOP_DOWNLOAD";
+    //取消安装升级
+    String WEBSOCKET_OP_CANCEL_UPGRADE = "CANCEL_UPGRADE";
+    //安装升级
+    String WEBSOCKET_OP_INSTALL_UPGRADE = "INSTALL_UPGRADE";
+    //上报设备状态(sn、在离线、软硬件版本、包名、ip地址等)
+    String WEBSOCKET_OP_REPORT_STATUS = "REPORT_STATUS";
+    //安装升级进度(任务id,下载进度(下载中),升级状态(下载中、安装中、安装成功、安装完成))
+    String WEBSOCKET_OP_UPGRADE_PROCESSING = "UPGRADE_PROCESSING";
+    //拉取日志
+    String WEBSOCKET_OP_FETCH_LOG = "FETCH_LOG";
+    //下载日志
+    String WEBSOCKET_OP_DOWNLOAD_LOG = "DOWNLOAD_LOG";
+    //上报在线日志
+    String WEBSOCKET_OP_REPORT_LOG = "REPORT_LOG";
+    //设备业务参数
+    String WEBSOCKET_OP_QUERY_DEVICE_BIZ_DATA = "QUERY_DEVICE_BIZ_DATA";
+    //支付宝五年陈数据
+    String WEBSOCKET_OP_SEND_ZFB_FIVE_YEAR_DATA = "SEND_ZFB_FIVE_YEAR_DATA";
+    //下发用户数据回调
+    String WEBSOCKET_OP_ISSUE_CALLBACK = "ISSUE_CALLBACK";
+
+    //hhface心跳
+    String WEBSOCKET_OP_TRACKING = "HHFACE_HEART_TRACKING";
+
+
+}

+ 80 - 0
hhface-common/src/main/java/cn/hh/common/enumeration/BusinessTypeEnum.java

@@ -0,0 +1,80 @@
+package cn.hh.common.enumeration;
+
+/**
+ * @author shisl
+ * @package cn.hh.common.enumeration
+ * @class BusinessTypeEnum
+ * @date 2023/1/12 下午5:42
+ * @description
+ */
+public enum BusinessTypeEnum {
+    /**
+     * 其它
+     */
+    OTHER,
+
+    /**
+     * 新增
+     */
+    INSERT,
+
+    /**
+     * 修改
+     */
+    UPDATE,
+
+    /**
+     * 删除
+     */
+    DELETE,
+
+    /**
+     * 授权
+     */
+    GRANT,
+
+    /**
+     * 导出
+     */
+    EXPORT,
+
+    /**
+     * 导入
+     */
+    IMPORT,
+
+    /**
+     * 强退
+     */
+    LOGIN_OUT,
+
+    /**
+     * 清空数据
+     */
+    CLEAN,
+
+    /**
+     * 查询
+     */
+    SELECT,
+
+    /**
+     * 关闭应用
+     */
+    CLOSE,
+
+    /**
+     * 重启
+     */
+    REBOOT,
+
+    /**
+     * 抓取日志
+     */
+    FETCH_LOG,
+    /**
+     * 下载日志
+     */
+    DOWNLOAD_LOG,
+
+}

+ 24 - 0
hhface-common/src/main/java/cn/hh/common/enumeration/FetchLogTypeEnum.java

@@ -0,0 +1,24 @@
+package cn.hh.common.enumeration;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * @author shisl
+ * @package cn.hh.common.enumeration
+ * @class FetchLogTypeEnum
+ * @date 2023/12/13 下午2:10
+ * @description 抓取日志类型
+ */
+@AllArgsConstructor
+@Getter
+public enum FetchLogTypeEnum {
+
+    LOGCAT("Logcat", "日志"),
+    ERROR("Error", "错误日志"),
+    CRASH("Crash", "崩溃日志");
+
+    private String code;
+    private String desc;
+
+}

+ 41 - 0
hhface-common/src/main/java/cn/hh/common/enumeration/TaskStatusEnum.java

@@ -0,0 +1,41 @@
+package cn.hh.common.enumeration;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * @author shisl
+ * @package cn.hh.common.enumeration
+ * @class TaskStatusEnum
+ * @date 2023/12/13 下午2:10
+ * @description 安装任务状态 0:等待中(设备离线 可取消) 1:下载中(可取消)
+ * 2:等待安装(静默升级 可取消) 3:安装中 4:安装成功 5:安装失败  6:已取消
+ */
+@AllArgsConstructor
+@Getter
+public enum TaskStatusEnum {
+
+    WAITING(0, "等待中"),
+    DOWNLOADING(1, "下载中"),
+    WAIT_INSTALL(2, "等待安装"),
+    INSTALLING(3, "安装中"),
+    INSTALL_SUCCESS(4, "安装成功"),
+    INSTALL_FAILED(5, "安装失败"),
+    CANCEL(6, "已取消");
+
+    private Integer code;
+    private String desc;
+
+    public static TaskStatusEnum getByCode(Integer code) {
+        TaskStatusEnum[] values = TaskStatusEnum.values();
+        for (TaskStatusEnum item : values) {
+            if (code.intValue() == item.getCode().intValue()) {
+                return item;
+            }
+        }
+        return null;
+    }
+
+
+
+}

+ 23 - 0
hhface-common/src/main/java/cn/hh/common/enumeration/UpgradeTypeEnum.java

@@ -0,0 +1,23 @@
+package cn.hh.common.enumeration;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * @author shisl
+ * @package cn.hh.common.enumeration
+ * @class UpgradeTypeEnum
+ * @date 2023/12/13 下午2:01
+ * @description 升级类型
+ */
+@AllArgsConstructor
+@Getter
+public enum UpgradeTypeEnum {
+
+    FORCE(1, "强制"),
+    SILENT(2, "静默");
+
+    private Integer code;
+    private String desc;
+
+}

+ 52 - 0
hhface-common/src/main/java/cn/hh/common/restful/BusinessException.java

@@ -0,0 +1,52 @@
+package cn.hh.common.restful;
+
+import java.io.Serializable;
+
+/**
+ * @author shisl
+ * @package cn.hh.common.restful
+ * @class BusinessException
+ * @date 2022/12/20 上午11:16
+ * @description
+ */
+public class BusinessException extends RuntimeException implements Serializable {
+
+    private static final long serialVersionUID = -1L;
+
+    private Integer code;
+
+    public BusinessException() {
+        super(RestCode.FAIL.getMsg());
+        this.code = RestCode.FAIL.getCode();
+    }
+
+    public BusinessException(String message) {
+        super(message);
+        this.code = RestCode.FAIL.getCode();
+    }
+
+    public BusinessException(Integer code, String message) {
+        super(message);
+        this.code = code;
+    }
+
+    public BusinessException(RestCode resCode) {
+        super(resCode.getMsg());
+        this.code = resCode.getCode();
+    }
+
+    public BusinessException(String message, Throwable cause) {
+        super(message, cause);
+        this.code = RestCode.FAIL.getCode();
+    }
+
+    public Integer getCode() {
+        return code;
+    }
+
+    public void setCode(Integer code) {
+        this.code = code;
+    }
+
+
+}

+ 138 - 0
hhface-common/src/main/java/cn/hh/common/restful/GlobalExceptionHandler.java

@@ -0,0 +1,138 @@
+package cn.hh.common.restful;
+
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.dao.DuplicateKeyException;
+import org.springframework.validation.BindException;
+import org.springframework.validation.FieldError;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.servlet.NoHandlerFoundException;
+
+import javax.validation.ConstraintViolation;
+import javax.validation.ConstraintViolationException;
+import javax.validation.Path;
+import javax.validation.ValidationException;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+
+/**
+ * @author shisl
+ * @package cn.hh.common.restful
+ * @class GlobalExceptionHandler
+ * @date 2022/12/20 上午11:16
+ * @description 全局异常处理器
+ */
+@Slf4j
+@RestControllerAdvice
+public class GlobalExceptionHandler {
+
+    /**
+     * 处理自定义异常
+     *
+     * @param e
+     * @return
+     */
+    @ExceptionHandler(BusinessException.class)
+    public RestResult<Object> handleBusinessException(BusinessException e) {
+        log.error(e.getMessage(), e);
+        return RestResponse.build(e.getCode(), e.getMessage());
+    }
+
+    /**
+     * 统一处理请求参数校验(实体对象传参)
+     *
+     * @param e BindException
+     * @return
+     */
+    @ExceptionHandler(BindException.class)
+    public RestResult<Object> validExceptionHandler(BindException e) {
+        StringBuilder message = new StringBuilder();
+        List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
+        for (FieldError error : fieldErrors) {
+            message.append(error.getField()).append(error.getDefaultMessage()).append(",");
+        }
+        message = new StringBuilder(message.substring(0, message.length() - 1));
+        return RestResponse.build(RestCode.FAIL.getCode(), message.toString());
+    }
+
+    /**
+     * 处理方法校验
+     *
+     * @param e
+     * @return
+     */
+    @ExceptionHandler(MethodArgumentNotValidException.class)
+    public RestResult<Object> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
+        log.error(e.getMessage(), e);
+        return RestResponse.build(RestCode.FAIL.getCode(), Objects.requireNonNull(e.getBindingResult().getFieldError()).getDefaultMessage());
+    }
+
+    /**
+     * 处理参数校验
+     *
+     * @param e
+     * @return
+     */
+    @ExceptionHandler(ValidationException.class)
+    public RestResult<Object> handleValidationException(ValidationException e) {
+        log.error(e.getMessage(), e);
+        return RestResponse.build(RestCode.METHOD_ARGUMENT_NOT_VALID_EXCEPTION.getCode(), e.getCause().getMessage());
+    }
+
+    /**
+     * 统一处理请求参数校验(普通传参)
+     *
+     * @param e ConstraintViolationException
+     * @return
+     */
+    @ExceptionHandler(value = ConstraintViolationException.class)
+    public RestResult<Object> handleConstraintViolationException(ConstraintViolationException e) {
+        StringBuilder message = new StringBuilder();
+        Set<ConstraintViolation<?>> violations = e.getConstraintViolations();
+        for (ConstraintViolation<?> violation : violations) {
+            Path path = violation.getPropertyPath();
+            String[] pathArr = StringUtils.splitByWholeSeparatorPreserveAllTokens(path.toString(), ".");
+            message.append(String.format("[%s]", pathArr[1])).append(violation.getMessage()).append(";");
+        }
+        message = new StringBuilder(message.substring(0, message.length() - 1));
+        return RestResponse.build(RestCode.FAIL.getCode(), message.toString());
+    }
+
+    @ExceptionHandler(NoHandlerFoundException.class)
+    public RestResult<Object> handlerNoFoundException(Exception e) {
+        log.error(e.getMessage(), e);
+        return RestResponse.build(RestCode.NOT_FOUND.getCode(), RestCode.NOT_FOUND.getMsg());
+    }
+
+    /**
+     * 处理数据重复
+     *
+     * @param e
+     * @return
+     */
+    @ExceptionHandler(DuplicateKeyException.class)
+    public RestResult<Object> handleDuplicateKeyException(DuplicateKeyException e) {
+        log.error(e.getMessage(), e);
+        return RestResponse.build(RestCode.DUPLICATE_KEY_EXCEPTION.getCode(), RestCode.DUPLICATE_KEY_EXCEPTION.getMsg());
+    }
+
+
+    /**
+     * 处理异常
+     *
+     * @param e
+     * @return
+     */
+    @ExceptionHandler(Exception.class)
+    public RestResult<Object> handleException(Exception e) {
+        log.error(e.getMessage(), e);
+        return RestResponse.build(RestCode.INTERNAL_SERVER_ERROR.getCode(), RestCode.INTERNAL_SERVER_ERROR.getMsg());
+    }
+
+
+}

+ 40 - 0
hhface-common/src/main/java/cn/hh/common/restful/RestCode.java

@@ -0,0 +1,40 @@
+package cn.hh.common.restful;
+
+import lombok.Getter;
+
+/**
+ * @author shisl
+ * @package cn.hh.common.restful
+ * @class RestCode
+ * @date 2022/12/20 上午11:18
+ * @description
+ */
+@Getter
+public enum RestCode {
+    SUCCESS(200, "成功"),
+    FAIL(400, "失败"),
+    UNAUTHORIZED(401, "无权访问"),
+    FORBIDDEN(403, "禁止访问该资源"),
+    NOT_FOUND(404, "未发现该资源"),
+    REQUEST_TIMEOUT(408, "请求超时"),
+    INTERNAL_SERVER_ERROR(500, "服务器异常"),
+    SERVICE_UNAVAILABLE(503, "服务无法获得"),
+    GATEWAY_TIMEOUT(504, "网关超时"),
+    REMOTE_INVOCATION_FAIL(900, "远程服务调用失败"),
+    INVALID_TOKEN(10001, "无效token"),
+    PARAMS_ERROR(10003, "参数有误"),
+    METHOD_ARGUMENT_NOT_VALID_EXCEPTION(10004, "数据校验错误"),
+    DUPLICATE_KEY_EXCEPTION(10005, "数据重复"),
+    USER_NOT_EXIST(40001, "用户不存在"),
+    DATA_NOT_EXIST(40004, "数据不存在或已删除"),
+    ;
+
+    public int code;
+    private String msg;
+
+    RestCode(int code, String msg) {
+        this.code = code;
+        this.msg = msg;
+    }
+
+}

+ 66 - 0
hhface-common/src/main/java/cn/hh/common/restful/RestDTO.java

@@ -0,0 +1,66 @@
+package cn.hh.common.restful;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.SneakyThrows;
+
+/**
+ * @author shisl
+ * @package cn.hh.common.restful
+ * @class RestDTO
+ * @date 2022/12/20 上午11:27
+ * @description
+ */
+@ApiModel("分页查询参数")
+public class RestDTO<T> {
+
+    @ApiModelProperty("第几页 默认1 ")
+    private Integer pageNum = 1;
+
+    @ApiModelProperty("每页大小 默认 20")
+    private Integer pageSize = 20;
+
+    @ApiModelProperty("其它查询参数 ")
+    private T data;
+
+    public Integer getPageNum() {
+        return pageNum;
+    }
+
+    public void setPageNum(Integer pageNum) {
+        if (pageNum == null || pageNum < 1) {
+            this.pageNum = 1;
+            return;
+        }
+        this.pageNum = pageNum;
+    }
+
+    public Integer getPageSize() {
+        return pageSize;
+    }
+
+    public void setPageSize(Integer pageSize) {
+        if (pageSize == null || pageSize < 1) {
+            this.pageSize = 20;
+            return;
+        }
+        this.pageSize = pageSize;
+    }
+
+    public T getData() {
+        return data;
+    }
+
+    public void setData(T data) {
+        this.data = data;
+    }
+
+
+    @SneakyThrows
+    @Override
+    public String toString() {
+        ObjectMapper om = new ObjectMapper();
+        return om.writeValueAsString(this);
+    }
+}

+ 48 - 0
hhface-common/src/main/java/cn/hh/common/restful/RestResponse.java

@@ -0,0 +1,48 @@
+package cn.hh.common.restful;
+
+import cn.hh.common.utils.Preconditions;
+
+public class RestResponse {
+
+    public static RestResult<String> ok() {
+        return new RestResult<String>().setCode(RestCode.SUCCESS).setMsg(RestCode.SUCCESS.getMsg());
+    }
+
+    public static <T> RestResult<T> ok(T data) {
+        return new RestResult<T>().setCode(RestCode.SUCCESS).setMsg(RestCode.SUCCESS.getMsg()).setData(data);
+    }
+
+    public static <T> RestResult<T> error(String message) {
+        return new RestResult<T>().setCode(RestCode.FAIL).setMsg(message);
+    }
+
+    public static <T> RestResult<T> error(RestCode restCode, T data) {
+        return new RestResult<T>().setCode(restCode.getCode()).setMsg(restCode.getMsg()).setData(data);
+    }
+
+    public static <T> RestResult<T> error(RestCode restCode, T data, String message) {
+        return new RestResult<T>().setCode(restCode.getCode()).setMsg(restCode.getMsg()).setData(data).setMsg(message);
+    }
+
+    public static <T> RestResult<T> error(RestCode restCode) {
+        return new RestResult<T>().setCode(restCode.getCode()).setMsg(restCode.getMsg());
+    }
+
+    public static <T> RestResult<T> build(int code, String msg) {
+        return new RestResult<T>().setCode(code).setMsg(msg);
+    }
+
+    public static <T> RestResult<T> build(int code, String msg, T data) {
+        return new RestResult<T>().setCode(code).setMsg(msg).setData(data);
+    }
+
+    public static <T> T verifyRestResult(RestResult<T> restResult) {
+        if (Preconditions.isBlank(restResult)) {
+            throw new BusinessException(RestCode.SERVICE_UNAVAILABLE);
+        }
+        if (restResult.getCode() != RestCode.SUCCESS.getCode()) {
+            throw new BusinessException(restResult.getCode(), restResult.getMsg());
+        }
+        return restResult.getData();
+    }
+}

+ 64 - 0
hhface-common/src/main/java/cn/hh/common/restful/RestResult.java

@@ -0,0 +1,64 @@
+package cn.hh.common.restful;
+
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.SneakyThrows;
+
+import java.io.Serializable;
+
+
+@ApiModel("返回类")
+public class RestResult<T> extends RestResponse implements Serializable {
+
+    private static final long serialVersionUID = -1L;
+
+    @ApiModelProperty("状态码")
+    public int code;
+
+    @ApiModelProperty("返回消息")
+    private String msg;
+
+    @ApiModelProperty("对象")
+    private T data;
+
+    public RestResult<T> setCode(RestCode restCode) {
+        this.code = restCode.code;
+        return this;
+    }
+
+    public int getCode() {
+        return code;
+    }
+
+    public RestResult<T> setCode(int code) {
+        this.code = code;
+        return this;
+    }
+
+    public String getMsg() {
+        return msg;
+    }
+
+    public RestResult<T> setMsg(String msg) {
+        this.msg = msg;
+        return this;
+    }
+
+    public T getData() {
+        return data;
+    }
+
+    public RestResult<T> setData(T data) {
+        this.data = data;
+        return this;
+    }
+
+    @SneakyThrows
+    @Override
+    public String toString() {
+        ObjectMapper om = new ObjectMapper();
+        return om.writeValueAsString(this);
+    }
+}

+ 25 - 0
hhface-common/src/main/java/cn/hh/common/serializer/BigDecimalSerializer.java

@@ -0,0 +1,25 @@
+package cn.hh.common.serializer;
+
+import com.alibaba.fastjson.serializer.JSONSerializer;
+import com.alibaba.fastjson.serializer.ObjectSerializer;
+import com.alibaba.fastjson.serializer.SerializeWriter;
+
+import java.lang.reflect.Type;
+import java.math.BigDecimal;
+
+/**
+ * @author shisl
+ * @package cn.hh.common.serializer
+ * @class BigDecimalSerializer
+ * @date 2022/12/19 下午5:18
+ * @description
+ */
+public class BigDecimalSerializer implements ObjectSerializer {
+
+    @Override
+    public void write(JSONSerializer serializer, Object object, Object fieldName, Type fieldType, int features) {
+        SerializeWriter out = serializer.out;
+        BigDecimal value = (BigDecimal) object;
+        out.writeString(value.toPlainString());
+    }
+}

+ 51 - 0
hhface-common/src/main/java/cn/hh/common/serializer/LongSerializer.java

@@ -0,0 +1,51 @@
+package cn.hh.common.serializer;
+
+import org.springframework.data.redis.serializer.RedisSerializer;
+import org.springframework.data.redis.serializer.SerializationException;
+import org.springframework.lang.Nullable;
+import org.springframework.stereotype.Component;
+
+/**
+ * @author shisl
+ * @package cn.hh.common.serializer
+ * @class LongSerializer
+ * @date 2022/12/19 下午5:08
+ * @description
+ */
+@Component
+public class LongSerializer implements RedisSerializer<Long> {
+
+    /**
+     * Serialize the given object to binary data.
+     *
+     * @param aLong object to serialize. Can be {@literal null}.
+     * @return the equivalent binary data. Can be {@literal null}.
+     */
+    @Nullable
+    @Override
+    public byte[] serialize(@Nullable Long aLong) throws SerializationException {
+        if (null != aLong){
+            return aLong.toString().getBytes();
+        }
+        else{
+            return new byte[0];
+        }
+    }
+
+    /**
+     * Deserialize an object from the given binary data.
+     *
+     * @param bytes object binary representation. Can be {@literal null}.
+     * @return the equivalent object instance. Can be {@literal null}.
+     */
+    @Nullable
+    @Override
+    public Long deserialize(@Nullable byte[] bytes) throws SerializationException {
+        if (bytes.length > 0){
+            return Long.parseLong(new String(bytes));
+        }
+        else{
+            return null;
+        }
+    }
+}

+ 20 - 0
hhface-common/src/main/java/cn/hh/common/utils/AppTokenUtil.java

@@ -0,0 +1,20 @@
+package cn.hh.common.utils;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * @author shisl
+ * @package cn.hh.common.utils
+ * @class AppTokenUtil
+ * @date 2023/12/13 下午1:30
+ * @description
+ */
+public class AppTokenUtil {
+
+    public static List<String> getTokens() {
+        return Arrays.asList("e69dade5b79ee888aae6b187",
+                "728a6f4f91d23c601d85906129a1647ca110f20f",
+                "4db67a72642967e2766b7445543298745f8cd958");
+    }
+}

+ 101 - 0
hhface-common/src/main/java/cn/hh/common/utils/AppUtils.java

@@ -0,0 +1,101 @@
+package cn.hh.common.utils;
+
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.UUID;
+
+/**
+ * @author shisl
+ * @package cn.hh.common.utils
+ * @class AppUtils
+ * @date 2024/2/28 上午11:51
+ * @description
+ */
+public class AppUtils {
+    private final static String APP_KEY_PREFIX = "hh";
+    private final static String SERVER_NAME = "hanghui_api_nqj##117521";
+    private final static String[] chars = new String[]{"a", "b", "c", "d", "e", "f",
+            "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s",
+            "t", "u", "v", "w", "x", "y", "z", "0", "1", "2", "3", "4", "5",
+            "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", "I",
+            "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V",
+            "W", "X", "Y", "Z"};
+
+    /**
+     * <p>
+     * 短8位UUID思想其实借鉴微博短域名的生成方式,但是其重复概率过高,而且每次生成4个,需要随即选取一个。
+     * 本算法利用62个可打印字符,通过随机生成32位UUID,由于UUID都为十六进制,
+     * 所以将UUID + 时间戳 分成9组,每5个为一组,然后通过模62操作,结果作为索引取出字符,
+     * 这样重复率大大降低。
+     * 经测试,在生成一千万个数据也没有出现重复,完全满足大部分需求。
+     * </p>
+     *
+     * @return
+     */
+    public static String getAppKey() {
+        StringBuffer shortBuffer = new StringBuffer().append(APP_KEY_PREFIX);
+        String uuid = UUID.randomUUID().toString().replace("-", "") + System.currentTimeMillis();
+        for (int i = 0; i < 9; i++) {
+            String str = uuid.substring(i * 5, i * 5 + 5);
+            int x = Integer.parseInt(str, 16);
+            shortBuffer.append(chars[x % 0x3E]);
+        }
+        return shortBuffer.toString();
+    }
+
+    /**
+     * 算法sha1生成AppSecret
+     *
+     * @param appKey
+     * @return
+     */
+    public static String getAppSecret(String appKey) {
+        try {
+
+            StringBuffer sb = new StringBuffer();
+            String uuid = UUID.randomUUID().toString();
+            sb.append(appKey).append(SERVER_NAME).append(uuid);
+
+            String str = sb.toString();
+            MessageDigest md = MessageDigest.getInstance("SHA-1");
+            md.update(str.getBytes());
+            byte[] digest = md.digest();
+
+            StringBuffer hexstr = new StringBuffer();
+            String shaHex = "";
+            for (int i = 0; i < digest.length; i++) {
+                shaHex = Integer.toHexString(digest[i] & 0xFF);
+                if (shaHex.length() < 2) {
+                    hexstr.append(0);
+                }
+                hexstr.append(shaHex);
+            }
+            return hexstr.toString();
+        } catch (NoSuchAlgorithmException e) {
+            e.printStackTrace();
+        }
+        return appKey;
+    }
+
+    /**
+     * 返回一个定长的随机字符串(只包含大小写字母、数字)
+     *
+     * @return 随机字符串
+     */
+    public static String getPrivateKey() {
+
+        return RandomUtils.generateString(16).toLowerCase();
+    }
+
+    public static void main(String[] args) {
+        String appKey = getAppKey();
+        String appSecret = getAppSecret(appKey);
+        System.out.println("--->time: " + System.currentTimeMillis());
+
+        System.out.println("--->appKey: " + appKey);
+        System.out.println("--->appSecret: " + appSecret);
+        System.out.println("--->privateKey: " + getPrivateKey());
+
+    }
+}

+ 442 - 0
hhface-common/src/main/java/cn/hh/common/utils/DateUtils.java

@@ -0,0 +1,442 @@
+package cn.hh.common.utils;
+
+import cn.hh.common.config.Constants;
+import cn.hutool.core.date.DateUtil;
+import lombok.extern.slf4j.Slf4j;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.time.ZoneId;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.TimeZone;
+
+/**
+ * @author shisl
+ * @package cn.hh.common.util
+ * @class DateUtils
+ * @date 2023/1/5 下午4:06
+ * @description 日期工具类
+ */
+@Slf4j
+public class DateUtils {
+
+    public static final String FORMAT_Y = "yyyy";
+    public static final String FORMAT_D_1 = "yyyy/MM/dd";
+    public static final String FORMAT_D_2 = "yyyy-MM-dd";
+    public static final String FORMAT_D_3 = "yyyyMMdd";
+    public static final String FORMAT_D_4 = "yyyy.MM.dd";
+    public static final String FORMAT_D = "dd";
+    public static final String FORMAT_DT_1 = "yyyy/MM/dd HH:mm:ss";
+    public static final String FORMAT_DT_2 = "yyyy-MM-dd HH:mm:ss";
+    public static final String FORMAT_DT_3 = "yyyyMMdd HH:mm:ss";
+    public static final String FORMAT_DT_4 = "yyyy-MM-dd HH:mm";
+    public static final String FORMAT_DT_5 = "yyyy.MM.dd HH:mm:ss";
+    public static final String FORMAT_DT_6 = "yyyyMMddHHmmss";
+    public static final String FORMAT_M_1 = "yyyy/MM";
+    public static final String FORMAT_M_2 = "yyyy-MM";
+    public static final String FORMAT_M_3 = "yyyyMM";
+    public static final String FORMAT_M = "MM";
+    public static final String FORMAT_MD_1 = "MM/dd";
+    public static final String FORMAT_MD_2 = "MM-dd";
+    public static final String FORMAT_MD_3 = "MMdd";
+    public static final String FORMAT_T_1 = "HH:mm:ss";
+    public static final String FORMAT_T_2 = "HH:mm";
+    public static final String FORMAT_TH = "HH";
+    public static final String FORMAT_TM = "mm";
+    public static final String FORMAT_TS = "ss";
+    public static final String FORMAT_UTC_ISO8601 = "yyyy-MM-dd'T'HH:mm:ss'Z'";
+
+    public static String format(String format, Date date) {
+        SimpleDateFormat sdf = new SimpleDateFormat(format);
+        return sdf.format(date);
+    }
+
+    /**
+     * @param format
+     * @param date
+     * @param timeZone 时区数字 -8, 0, 8 等
+     * @return
+     */
+    public static String format(String format, Date date, int timeZone) {
+        timeZone = timeZone % 13;
+        SimpleDateFormat sdf = new SimpleDateFormat(format);
+        ZoneId zoneId = ZoneId.of("GMT" + (timeZone >= 0 ? "+" : "") + timeZone);
+        TimeZone tz = TimeZone.getTimeZone(zoneId);
+
+        sdf.setTimeZone(tz);
+
+        return sdf.format(date);
+    }
+
+    public static Date parseDate(Date date) {
+        String zoneDate = format(FORMAT_DT_2, date, 8);
+        SimpleDateFormat sdf = new SimpleDateFormat(FORMAT_DT_2);
+        try {
+            return sdf.parse(zoneDate);
+        } catch (ParseException e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    public static Date parse(String dateString, String format) {
+        SimpleDateFormat sdf = new SimpleDateFormat(format);
+
+        try {
+            return sdf.parse(dateString);
+        } catch (ParseException var4) {
+            return null;
+        }
+    }
+
+
+    public static Date parse(String dateString, String format, int timeZone) {
+
+        SimpleDateFormat sdf = new SimpleDateFormat(format);
+        ZoneId zoneId = ZoneId.of("GMT" + (timeZone >= 0 ? "+" : "") + timeZone);
+        TimeZone tz = TimeZone.getTimeZone(zoneId);
+
+        sdf.setTimeZone(tz);
+
+        try {
+            return sdf.parse(dateString);
+        } catch (ParseException var4) {
+            return null;
+        }
+    }
+
+    public static Date todayStart() {
+        Calendar calendar1 = Calendar.getInstance();
+        calendar1.set(calendar1.get(Calendar.YEAR), calendar1.get(Calendar.MONTH), calendar1.get(Calendar.DAY_OF_MONTH),
+                0, 0, 0);
+        calendar1.setTimeZone(TimeZone.getTimeZone("GMT+8"));
+        Date beginOfDate = calendar1.getTime();
+        return beginOfDate;
+    }
+
+    public static Date todayEnd() {
+        Calendar calendar1 = Calendar.getInstance();
+        calendar1.set(calendar1.get(Calendar.YEAR), calendar1.get(Calendar.MONTH), calendar1.get(Calendar.DAY_OF_MONTH),
+                23, 59, 59);
+        calendar1.setTimeZone(TimeZone.getTimeZone("GMT+8"));
+        Date endOfDate = calendar1.getTime();
+        return endOfDate;
+    }
+
+    public static String format(Date date, String format) {
+        SimpleDateFormat sdf = new SimpleDateFormat(format);
+        return sdf.format(date);
+    }
+
+    public static Long millSecToSec(Long millSeconds) {
+        return millSeconds / 1000;
+    }
+
+    /**
+     * 某天的开始时间
+     *
+     * @param date
+     * @return
+     */
+    public static Date dateStart(Date date) {
+        Calendar calendar1 = Calendar.getInstance();
+        calendar1.setTime(date);
+        calendar1.set(calendar1.get(Calendar.YEAR), calendar1.get(Calendar.MONTH), calendar1.get(Calendar.DAY_OF_MONTH), 0, 0, 0);
+        calendar1.setTimeZone(TimeZone.getTimeZone("GMT+8"));
+        Date end = calendar1.getTime();
+        return end;
+    }
+
+    /**
+     * 某天的最后时间
+     *
+     * @param date
+     * @return
+     */
+    public static Date dateEnd(Date date) {
+        Calendar calendar1 = Calendar.getInstance();
+        calendar1.setTime(date);
+        calendar1.set(calendar1.get(Calendar.YEAR), calendar1.get(Calendar.MONTH), calendar1.get(Calendar.DAY_OF_MONTH), 23, 59, 59);
+        calendar1.setTimeZone(TimeZone.getTimeZone("GMT+8"));
+        Date end = calendar1.getTime();
+        return end;
+    }
+
+
+    /**
+     * 为指定日期增加指定天数
+     */
+    public static String plusDay(int num, Date newDate, String format) {
+        String enddate = null;
+        SimpleDateFormat sdf = new SimpleDateFormat(format);
+        Date currdate = newDate;
+        Calendar ca = Calendar.getInstance();
+        ca.setTime(currdate);
+        ca.add(Calendar.DATE, num);
+        currdate = ca.getTime();
+        enddate = sdf.format(currdate);
+        return enddate;
+    }
+
+    /**
+     * 几小时前的时间
+     *
+     * @param date
+     * @param hours
+     * @return
+     */
+    public static Date minusTimeByHour(Date date, Integer hours) {
+        Calendar ca = Calendar.getInstance();
+        ca.setTime(date);
+        ca.set(Calendar.HOUR_OF_DAY, ca.get(Calendar.HOUR_OF_DAY) - hours);
+        return ca.getTime();
+    }
+
+    /**
+     * 几个月后的时间
+     *
+     * @param date
+     * @param month
+     * @return
+     */
+    public static Date addTimeByMonth(Date date, Integer month) {
+        Calendar ca = Calendar.getInstance();
+        ca.setTime(date);
+        ca.set(Calendar.MONTH, ca.get(Calendar.MONTH) + month);
+        return ca.getTime();
+    }
+
+    /**
+     * 几小时后的时间
+     *
+     * @param date
+     * @param hours
+     * @return
+     */
+    public static Date addTimeByHour(Date date, Integer hours) {
+        Calendar ca = Calendar.getInstance();
+        ca.setTime(date);
+        ca.set(Calendar.HOUR_OF_DAY, ca.get(Calendar.HOUR_OF_DAY) + hours);
+        return ca.getTime();
+    }
+
+
+    public static Date addTimeByYear(Date date, Integer years) {
+        Calendar ca = Calendar.getInstance();
+        ca.setTime(date);
+        ca.set(Calendar.YEAR, ca.get(Calendar.YEAR) + years);
+        return ca.getTime();
+    }
+
+    /**
+     * 间隔分钟
+     *
+     * @param firstTime
+     * @param secondTime
+     * @return
+     */
+    public static int getMinsBetweenTime(Long firstTime, Long secondTime) {
+        int mins = (int) ((Math.abs(firstTime - secondTime)) / (1000 * 60));
+        return mins;
+    }
+
+    /**
+     * 间隔小时
+     *
+     * @param firstTime
+     * @param secondTime
+     * @return
+     */
+    public static int getHoursBetweenTime(Long firstTime, Long secondTime) {
+        int hours = (int) ((Math.abs(firstTime - secondTime)) / (1000 * 60 * 60));
+        return hours;
+    }
+
+    /**
+     * 间隔天数
+     *
+     * @param firstTime
+     * @param secondTime
+     * @return
+     */
+    public static int getDaysBetweenTime(Long firstTime, Long secondTime) {
+        int days = (int) ((Math.abs(firstTime - secondTime)) / (1000 * 60 * 60 * 24));
+        return days;
+    }
+
+    /**
+     * 获取当前时间所在月的开始时间
+     *
+     * @return
+     */
+    public static Date getMonthFirst(Date date) {
+        Calendar calendar = Calendar.getInstance();
+        calendar.setTime(date);
+        calendar.set(Calendar.DAY_OF_MONTH, 1);
+        Date firstDayOfMonth = calendar.getTime();
+        String format = DateUtils.format(firstDayOfMonth, DateUtils.FORMAT_D_2).concat(" 00:00:00");
+        return DateUtils.parse(format, DateUtils.FORMAT_DT_2);
+
+    }
+
+    /**
+     * 获取本月的结束时间
+     *
+     * @return
+     */
+    public static Date getMonthLast(Date date) {
+        Calendar calendar = Calendar.getInstance();
+        calendar.setTime(date);
+        calendar.set(Calendar.DAY_OF_MONTH, 1);
+        calendar.add(Calendar.MONTH, 1);
+        calendar.add(Calendar.DAY_OF_MONTH, -1);
+        Date lastDayOfMonth = calendar.getTime();
+        String format = DateUtils.format(lastDayOfMonth, DateUtils.FORMAT_D_2).concat(" 23:59:59");
+        return DateUtils.parse(format, DateUtils.FORMAT_DT_2);
+    }
+
+    /**
+     * 时区转换
+     *
+     * @param time           时间字符串
+     * @param pattern        格式 "yyyy-MM-dd HH:mm"
+     * @param nowTimeZone    eg:+8,0,+9,-1 等等
+     * @param targetTimeZone 同nowTimeZone
+     * @return
+     */
+    public static String timeZoneTransfer(String time, String pattern, int nowTimeZone, int targetTimeZone) {
+        if (Preconditions.isBlank(time)) {
+            return Constants.EMPTY_STRING;
+        }
+        SimpleDateFormat simpleDateFormat = new SimpleDateFormat(pattern);
+        simpleDateFormat.setTimeZone(TimeZone.getTimeZone("GMT" + (nowTimeZone >= 0 ? "+" : "") + nowTimeZone));
+        Date date;
+        try {
+            date = simpleDateFormat.parse(time);
+        } catch (ParseException e) {
+            log.error("时间转换出错。", e);
+            return Constants.EMPTY_STRING;
+        }
+        simpleDateFormat.setTimeZone(TimeZone.getTimeZone("GMT" + (targetTimeZone >= 0 ? "+" : "") + targetTimeZone));
+        return simpleDateFormat.format(date);
+    }
+
+    /**
+     * 计算两个时间相差几天几小时几秒
+     *
+     * @param diff
+     * @return
+     */
+    public static String getTimeDiffString(long diff) {
+        StringBuilder sb = new StringBuilder();
+        // 只能精确到日 无法具细到年 月 不能确定一个月具体多少天 不能确定一年具体多少天
+        // 获取日
+        long day = diff / (1000 * 60 * 60 * 24);
+        diff = diff % (1000 * 60 * 60 * 24);
+        if (day > 0) {
+            sb.append(day).append("天");
+        }
+        // 获取时
+        long hour = diff / (1000 * 60 * 60);
+        diff = diff % (1000 * 60 * 60);
+        if (hour > 0) {
+            sb.append(hour).append("时");
+        }
+        // 获取分
+        long min = diff / (1000 * 60);
+        diff = diff % (1000 * 60);
+        if (min > 0) {
+            sb.append(min).append("分");
+        }
+        // 获取秒
+        long sec = diff / 1000;
+        if (sec > 0) {
+            sb.append(sec).append("秒");
+        }
+        return sb.toString();
+    }
+
+    /**
+     * 获取当前系统的年份
+     *
+     * @return
+     */
+    public static Integer getSystemCurrentYear() {
+        Calendar date = Calendar.getInstance();
+        int year = date.get(Calendar.YEAR);
+        return year;
+    }
+
+    /**
+     * 获取当前系统的月份
+     *
+     * @return
+     */
+    public static Integer getSystemCurrentMonth() {
+        Calendar date = Calendar.getInstance();
+        int month = date.get(Calendar.MONTH) + 1;
+        return month;
+    }
+
+    /**
+     * 获取当前系统的季度
+     *
+     * @return
+     */
+    public static int getSystemCurrentQuarter() {
+        Calendar c = Calendar.getInstance();
+        //由于month从0开始,所有这里+1
+        int month = c.get(Calendar.MONTH) + 1;
+        return month % 3 == 0 ? month / 3 : month / 3 + 1;
+    }
+
+    /**
+     * 获取某年第一天日期
+     *
+     * @param year 年份
+     * @return Date 2023-01-01 00:00:00
+     */
+    public static Date getYearFirst(int year) {
+        Calendar calendar = Calendar.getInstance();
+        calendar.clear();
+        calendar.set(Calendar.YEAR, year);
+        Date currYearFirst = calendar.getTime();
+        String nowTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(currYearFirst);
+        log.info("{}年第一天为{}", year, nowTime);
+        return currYearFirst;
+    }
+
+    /**
+     * 获取某年最后一天日期
+     *
+     * @param year 年份
+     * @return Date 2023-12-31 23:59:59
+     */
+    public static Date getYearLast(int year) {
+        Calendar calendar = Calendar.getInstance();
+        calendar.clear();
+        calendar.set(Calendar.YEAR, year);
+        calendar.roll(Calendar.DAY_OF_YEAR, -1);
+        String concat = DateUtils.format(calendar.getTime(), DateUtils.FORMAT_D_2).concat(" 23:59:59");
+        return DateUtils.parse(concat, DateUtils.FORMAT_DT_2);
+    }
+
+    /**
+     * 相差年份
+     *
+     * @param startDate
+     * @param endDate
+     */
+    public static long getYearsDiff(Date startDate, Date endDate) {
+        return DateUtil.betweenYear(startDate, endDate, false);
+
+    }
+
+
+    public static void main(String[] args) {
+//        long yearsDiff = getYearsDiff(DateUtils.parse("2020-03-26", DateUtils.FORMAT_D_2), new Date());
+        log.info("" + DateUtil.endOfYear(new Date()));
+    }
+
+}

+ 69 - 0
hhface-common/src/main/java/cn/hh/common/utils/EnumUtils.java

@@ -0,0 +1,69 @@
+package cn.hh.common.utils;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.util.CollectionUtils;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author shisl
+ * @package cn.hh.common.utils
+ * @class EnumUtils
+ * @date 2023/12/14 上午11:35
+ * @description
+ */
+@Slf4j
+public class EnumUtils {
+    private static final String ENUM_CLASSPATH = "java.lang.Enum";
+
+    public static List<Map<String, Object>> enumToListMap(Class<?> enumClass) {
+        List<Map<String, Object>> resultList = new ArrayList<>();
+        if (!ENUM_CLASSPATH.equals(enumClass.getSuperclass().getCanonicalName())) {
+            return resultList;
+        }
+        // 获取所有public方法
+        Method[] methods = enumClass.getMethods();
+        List<Field> fieldList = new ArrayList<>();
+        // step 1:通过get方法提取字段,所以避免get作为自定义方法的开头,建议使用 ‘find’或其他命名
+        Arrays.stream(methods).map(Method::getName).filter(
+                methodName -> methodName.startsWith("get") && !"getDeclaringClass".equals(methodName) && !"getClass".equals(methodName)
+        ).forEachOrdered(methodName -> {
+            try {
+                Field field = enumClass.getDeclaredField(StringUtils.uncapitalize(methodName.substring(3)));
+                fieldList.add(field);
+            } catch (NoSuchFieldException | SecurityException e) {
+                log.error(e.getMessage(), e);
+            }
+        });
+
+        // step 2:将字段作为key,逐一把枚举值作为value 存入list
+        if (CollectionUtils.isEmpty(fieldList)) {
+            return resultList;
+        }
+
+        Enum[] enums = (Enum[]) enumClass.getEnumConstants();
+        for (Enum anEnum : enums) {
+            Map<String, Object> map = new HashMap<>(fieldList.size());
+            for (Field field : fieldList) {
+                field.setAccessible(true);
+                try {
+                    // 向map集合添加字段名称 和 字段值
+                    map.put(field.getName(), field.get(anEnum));
+                } catch (IllegalArgumentException | IllegalAccessException e) {
+                    log.error(e.getMessage(), e);
+                }
+            }
+            // 将Map添加到集合中
+            resultList.add(map);
+        }
+        return resultList;
+    }
+}
+

+ 323 - 0
hhface-common/src/main/java/cn/hh/common/utils/ExcelUtil.java

@@ -0,0 +1,323 @@
+package cn.hh.common.utils;
+
+
+import com.alibaba.excel.EasyExcelFactory;
+import com.alibaba.excel.ExcelWriter;
+import com.alibaba.excel.context.AnalysisContext;
+import com.alibaba.excel.event.AnalysisEventListener;
+import com.alibaba.excel.metadata.BaseRowModel;
+import com.alibaba.excel.metadata.Sheet;
+import lombok.Data;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * @author shisl
+ * @package cn.hh.common.util
+ * @class ExcelUtil
+ * @date 2023/2/19 上午00:20
+ * @description
+ */
+@Slf4j
+public class ExcelUtil {
+
+    private static Sheet initSheet;
+
+    static {
+        initSheet = new Sheet(1, 0);
+        initSheet.setSheetName("sheet");
+        //设置自适应宽度
+        initSheet.setAutoWidth(Boolean.TRUE);
+    }
+
+    /**
+     * 读取少于1000行数据
+     *
+     * @param filePath 文件绝对路径
+     * @return
+     */
+    public static List<Object> readLessThan1000Row(String filePath) {
+        return readLessThan1000RowBySheet(filePath, null);
+    }
+
+    /**
+     * 读小于1000行数据, 带样式
+     * filePath 文件绝对路径
+     * initSheet :
+     * sheetNo: sheet页码,默认为1
+     * headLineMun: 从第几行开始读取数据,默认为0, 表示从第一行开始读取
+     * clazz: 返回数据List<Object> 中Object的类名
+     */
+    public static List<Object> readLessThan1000RowBySheet(String filePath, Sheet sheet) {
+        if (!StringUtils.hasText(filePath)) {
+            return null;
+        }
+
+        sheet = sheet != null ? sheet : initSheet;
+
+        InputStream fileStream = null;
+        try {
+            fileStream = new FileInputStream(filePath);
+            return EasyExcelFactory.read(fileStream, sheet);
+        } catch (FileNotFoundException e) {
+            log.info("找不到文件或文件路径错误, 文件:{}", filePath);
+        } finally {
+            try {
+                if (fileStream != null) {
+                    fileStream.close();
+                }
+            } catch (IOException e) {
+                log.info("excel文件读取失败, 失败原因:{}", e);
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 读大于1000行数据
+     *
+     * @param filePath 文件觉得路径
+     * @return
+     */
+    public static List<Object> readMoreThan1000Row(String filePath) {
+        return readMoreThan1000RowBySheet(filePath, null);
+    }
+
+    /**
+     * 读大于1000行数据, 带样式
+     *
+     * @param filePath 文件觉得路径
+     * @return
+     */
+    public static List<Object> readMoreThan1000RowBySheet(String filePath, Sheet sheet) {
+        if (!StringUtils.hasText(filePath)) {
+            return null;
+        }
+
+        sheet = sheet != null ? sheet : initSheet;
+
+        InputStream fileStream = null;
+        try {
+            fileStream = new FileInputStream(filePath);
+            ExcelListener excelListener = new ExcelListener();
+            EasyExcelFactory.readBySax(fileStream, sheet, excelListener);
+            return excelListener.getDatas();
+        } catch (FileNotFoundException e) {
+            log.error("找不到文件或文件路径错误, 文件:{}", filePath);
+        } finally {
+            try {
+                if (fileStream != null) {
+                    fileStream.close();
+                }
+            } catch (IOException e) {
+                log.error("excel文件读取失败, 失败原因:{}", e);
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 生成excel
+     *
+     * @param filePath 绝对路径, 如:/home/leon/Downloads/aaa.xlsx
+     * @param data     数据源
+     * @param head     表头
+     */
+    public static void writeBySimple(String filePath, List<List<Object>> data, List<String> head) {
+        writeSimpleBySheet(filePath, data, head, null);
+    }
+
+    /**
+     * 生成excel
+     *
+     * @param filePath 绝对路径, 如:/home/leon/Downloads/aaa.xlsx
+     * @param data     数据源
+     * @param sheet    excel页面样式
+     * @param head     表头
+     */
+    public static void writeSimpleBySheet(String filePath, List<List<Object>> data, List<String> head, Sheet sheet) {
+        sheet = (sheet != null) ? sheet : initSheet;
+
+        if (head != null) {
+            List<List<String>> list = new ArrayList<>();
+            head.forEach(h -> list.add(Collections.singletonList(h)));
+            sheet.setHead(list);
+        }
+
+        OutputStream outputStream = null;
+        ExcelWriter writer = null;
+        try {
+            outputStream = new FileOutputStream(filePath);
+            writer = EasyExcelFactory.getWriter(outputStream);
+            writer.write1(data, sheet);
+        } catch (FileNotFoundException e) {
+            log.error("找不到文件或文件路径错误, 文件:{}", filePath);
+        } finally {
+            try {
+                if (writer != null) {
+                    writer.finish();
+                }
+
+                if (outputStream != null) {
+                    outputStream.close();
+                }
+
+            } catch (IOException e) {
+                log.error("excel文件导出失败, 失败原因:{}", e);
+            }
+        }
+
+    }
+
+    /**
+     * 生成excel
+     *
+     * @param filePath 绝对路径, 如:/home/leon/Downloads/aaa.xlsx
+     * @param data     数据源
+     */
+    public static void writeWithTemplate(String filePath, List<? extends BaseRowModel> data) {
+        writeWithTemplateAndSheet(filePath, data, null);
+    }
+
+    /**
+     * 生成excel
+     *
+     * @param filePath 绝对路径, 如:/home/leon/Downloads/aaa.xlsx
+     * @param data     数据源
+     * @param sheet    excel页面样式
+     */
+    public static void writeWithTemplateAndSheet(String filePath, List<? extends BaseRowModel> data, Sheet sheet) {
+        if (CollectionUtils.isEmpty(data)) {
+            return;
+        }
+
+        sheet = (sheet != null) ? sheet : initSheet;
+        sheet.setClazz(data.get(0).getClass());
+
+        OutputStream outputStream = null;
+        ExcelWriter writer = null;
+        try {
+            outputStream = new FileOutputStream(filePath);
+            writer = EasyExcelFactory.getWriter(outputStream);
+            writer.write(data, sheet);
+        } catch (FileNotFoundException e) {
+            log.error("找不到文件或文件路径错误, 文件:{}", filePath);
+        } finally {
+            try {
+                if (writer != null) {
+                    writer.finish();
+                }
+
+                if (outputStream != null) {
+                    outputStream.close();
+                }
+            } catch (IOException e) {
+                log.error("excel文件导出失败, 失败原因:{}", e);
+            }
+        }
+
+    }
+
+    /**
+     * 生成多Sheet的excel
+     *
+     * @param filePath              绝对路径, 如:/home/leon/Downloads/aaa.xlsx
+     * @param multipleSheetPropetys
+     */
+    public static void writeWithMultipleSheel(String filePath, List<MultipleSheelPropety> multipleSheetPropetys) {
+        if (CollectionUtils.isEmpty(multipleSheetPropetys)) {
+            return;
+        }
+
+        OutputStream outputStream = null;
+        ExcelWriter writer = null;
+        try {
+            outputStream = new FileOutputStream(filePath);
+            writer = EasyExcelFactory.getWriter(outputStream);
+            for (MultipleSheelPropety multipleSheelPropety : multipleSheetPropetys) {
+                Sheet sheet = multipleSheelPropety.getSheet() != null ? multipleSheelPropety.getSheet() : initSheet;
+                if (!CollectionUtils.isEmpty(multipleSheelPropety.getData())) {
+                    sheet.setClazz(multipleSheelPropety.getData().get(0).getClass());
+                }
+                writer.write(multipleSheelPropety.getData(), sheet);
+            }
+
+        } catch (FileNotFoundException e) {
+            log.error("找不到文件或文件路径错误, 文件:{}", filePath);
+        } finally {
+            try {
+                if (writer != null) {
+                    writer.finish();
+                }
+
+                if (outputStream != null) {
+                    outputStream.close();
+                }
+            } catch (IOException e) {
+                log.error("excel文件导出失败, 失败原因:{}", e);
+            }
+        }
+
+    }
+
+
+    /*********************匿名内部类开始,可以提取出去******************************/
+
+    @Data
+    public static class MultipleSheelPropety {
+
+        private List<? extends BaseRowModel> data;
+
+        private Sheet sheet;
+    }
+
+    /**
+     * 解析监听器,
+     * 每解析一行会回调invoke()方法。
+     * 整个excel解析结束会执行doAfterAllAnalysed()方法
+     */
+    @Getter
+    @Setter
+    public static class ExcelListener extends AnalysisEventListener {
+
+        private List<Object> datas = new ArrayList<>();
+
+        /**
+         * 逐行解析
+         * object : 当前行的数据
+         */
+        @Override
+        public void invoke(Object object, AnalysisContext context) {
+            //当前行
+            // context.getCurrentRowNum()
+            if (object != null) {
+                datas.add(object);
+            }
+        }
+
+        /**
+         * 解析完所有数据后会调用该方法
+         */
+        @Override
+        public void doAfterAllAnalysed(AnalysisContext context) {
+            //解析结束销毁不用的资源
+        }
+
+    }
+
+    /************************匿名内部类结束,可以提取出去***************************/
+
+}

+ 82 - 0
hhface-common/src/main/java/cn/hh/common/utils/FileUtils.java

@@ -0,0 +1,82 @@
+package cn.hh.common.utils;
+
+import cn.hutool.core.date.DateUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Date;
+
+/**
+ * @author shisl
+ * @package cn.hh.common.utils
+ * @class FileUtils
+ * @date 2023/10/8 上午10:41
+ * @description
+ */
+@Slf4j
+public class FileUtils {
+
+    public static String pic_base_path = "file/upload/pic";
+    public static String file_base_path = "file/upload/file";
+
+
+    public static String getDirectory(String basePath) {
+        return basePath.concat("/").concat(DateUtil.format(new Date(), "yyyyMMdd"));
+    }
+
+    public static String saveByCopy(String filePath, MultipartFile file) throws IOException {
+        String filename = file.getOriginalFilename();
+        return saveByCopy(filename, filePath, file.getInputStream());
+    }
+
+    public static String saveByCopy2(String filePath, MultipartFile file) throws IOException {
+        String filename = getRandomFileName(file.getOriginalFilename());
+        return saveByCopy(filename, filePath, file.getInputStream());
+    }
+
+    public static String saveByCopy(String filename, String filePath, InputStream inputStream) throws IOException {
+        Path directory = Paths.get(filePath);
+        if (!Files.exists(directory)) {
+            Files.createDirectories(directory);
+        }
+        //如果存在文件就先删除
+        Files.deleteIfExists(directory.resolve(filename));
+        Files.copy(inputStream, directory.resolve(filename));
+        return filePath.concat("/").concat(filename);
+    }
+
+    public static String getRandomFileName(String fileName) {
+        log.info(fileName);
+        String suffix = "";
+        if (StringUtils.isNotEmpty(fileName)) {
+            if (fileName.contains(".")) {
+                String[] arr = fileName.split("\\.");
+                if (arr.length > 1)
+                    suffix = arr[arr.length - 1];
+            }
+        }
+        if (StringUtils.isNotEmpty(suffix)) {
+            return SnowflakeIdWorker.getSnowId() + "." + suffix;
+        }
+        return SnowflakeIdWorker.getSnowId() + ".tmp";
+    }
+
+    public static void mkdir(String filePath) {
+        try {
+            Path directory = Paths.get(filePath);
+            if (!Files.exists(directory)) {
+                Files.createDirectories(directory);
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+
+}

+ 317 - 0
hhface-common/src/main/java/cn/hh/common/utils/IPUtils.java

@@ -0,0 +1,317 @@
+package cn.hh.common.utils;
+
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import javax.servlet.http.HttpServletRequest;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+
+@Slf4j
+public class IPUtils {
+    private final static String UNKNOWN = "unknown";
+    private final static int MAX_LENGTH = 15;
+    private static final String IP_UTILS_FLAG = ",";
+    private static final String LOCALHOST_IP = "0:0:0:0:0:0:0:1";
+    private static final String LOCALHOST_IP1 = "127.0.0.1";
+    private static final String X_ORIGINAL_FORWARDED_FOR = "X-Original-Forwarded-For";
+    private static final String X_FORWARDED_FOR = "X-Forwarded-For";
+    private static final String x_forwarded_for = "x-forwarded-for";
+    private static final String PROXY_CLIENT_IP = "Proxy-Client-IP";
+    private static final String WL_PROXY_CLIENT_IP = "WL-Proxy-Client-IP";
+    private static final String HTTP_CLIENT_IP = "HTTP_CLIENT_IP";
+    private static final String HTTP_X_FORWARDED_FOR = "HTTP_X_FORWARDED_FOR";
+
+    /**
+     * 获取IP地址
+     * 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址
+     * 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址
+     */
+    public static String getIP() {
+        ServletRequestAttributes requestAttributes = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes());
+        if (requestAttributes == null) {
+            return null;
+        }
+        HttpServletRequest request = requestAttributes.getRequest();
+        return getIP(request);
+    }
+
+
+    /**
+     * 获取IP地址
+     * <p>
+     * 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址
+     * 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址
+     */
+    public static String getIP(HttpServletRequest request) {
+        String ip = null;
+        try {
+            //以下两个获取在k8s中,将真实的客户端IP,放到了x-Original-Forwarded-For。而将WAF的回源地址放到了 x-Forwarded-For了。
+            ip = request.getHeader(X_ORIGINAL_FORWARDED_FOR);
+
+            //X-Forwarded-For
+            if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
+                ip = request.getHeader(X_FORWARDED_FOR);
+            }
+            //获取nginx等代理的ip  x-forwarded-for
+            if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
+                ip = request.getHeader(x_forwarded_for);
+            }
+            //Proxy-Client-IP
+            if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
+                ip = request.getHeader(PROXY_CLIENT_IP);
+            }
+
+            //WL-Proxy-Client-IP
+            if (StringUtils.isEmpty(ip) || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
+                ip = request.getHeader(WL_PROXY_CLIENT_IP);
+            }
+
+            //HTTP_CLIENT_IP
+            if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
+                ip = request.getHeader(HTTP_CLIENT_IP);
+            }
+
+            //HTTP_X_FORWARDED_FOR
+            if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
+                ip = request.getHeader(HTTP_X_FORWARDED_FOR);
+            }
+            //兼容k8s集群获取ip
+            if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
+                ip = request.getRemoteAddr();
+                if (LOCALHOST_IP1.equalsIgnoreCase(ip) || LOCALHOST_IP.equalsIgnoreCase(ip)) {
+                    //根据网卡取本机配置的IP
+                    try {
+                        InetAddress iNet = InetAddress.getLocalHost();
+                        ip = iNet.getHostAddress();
+                    } catch (UnknownHostException e) {
+                        log.error("getClientIp error: {}", e.getMessage());
+                    }
+                }
+            }
+        } catch (Exception e) {
+            log.error("IPUtils ERROR ", e);
+        }
+        //使用代理,则获取第一个IP地址
+        if (!StringUtils.isEmpty(ip) && ip.indexOf(IP_UTILS_FLAG) > 0) {
+            ip = ip.substring(0, ip.indexOf(IP_UTILS_FLAG));
+        }
+        return ip;
+    }
+
+    /**
+     * 获取客户端IP
+     *
+     * @param request 请求对象
+     * @return IP地址
+     */
+    public static String getIpAddr(HttpServletRequest request) {
+        if (request == null) {
+            return "unknown";
+        }
+        String ip = request.getHeader("x-forwarded-for");
+        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getHeader("Proxy-Client-IP");
+        }
+        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getHeader("X-Forwarded-For");
+        }
+        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getHeader("WL-Proxy-Client-IP");
+        }
+        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getHeader("X-Real-IP");
+        }
+
+        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getRemoteAddr();
+        }
+
+        return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : getMultistageReverseProxyIp(ip);
+    }
+
+    /**
+     * 检查是否为内部IP地址
+     *
+     * @param ip IP地址
+     * @return 结果
+     */
+    public static boolean internalIp(String ip) {
+        byte[] addr = textToNumericFormatV4(ip);
+        return internalIp(addr) || "127.0.0.1".equals(ip);
+    }
+
+    /**
+     * 检查是否为内部IP地址
+     *
+     * @param addr byte地址
+     * @return 结果
+     */
+    private static boolean internalIp(byte[] addr) {
+        if (Preconditions.isBlank(addr) || addr.length < 2) {
+            return true;
+        }
+        final byte b0 = addr[0];
+        final byte b1 = addr[1];
+        // 10.x.x.x/8
+        final byte SECTION_1 = 0x0A;
+        // 172.16.x.x/12
+        final byte SECTION_2 = (byte) 0xAC;
+        final byte SECTION_3 = (byte) 0x10;
+        final byte SECTION_4 = (byte) 0x1F;
+        // 192.168.x.x/16
+        final byte SECTION_5 = (byte) 0xC0;
+        final byte SECTION_6 = (byte) 0xA8;
+        switch (b0) {
+            case SECTION_1:
+                return true;
+            case SECTION_2:
+                if (b1 >= SECTION_3 && b1 <= SECTION_4) {
+                    return true;
+                }
+            case SECTION_5:
+                switch (b1) {
+                    case SECTION_6:
+                        return true;
+                }
+            default:
+                return false;
+        }
+    }
+
+    /**
+     * 将IPv4地址转换成字节
+     *
+     * @param text IPv4地址
+     * @return byte 字节
+     */
+    public static byte[] textToNumericFormatV4(String text) {
+        if (text.length() == 0) {
+            return null;
+        }
+
+        byte[] bytes = new byte[4];
+        String[] elements = text.split("\\.", -1);
+        try {
+            long l;
+            int i;
+            switch (elements.length) {
+                case 1:
+                    l = Long.parseLong(elements[0]);
+                    if ((l < 0L) || (l > 4294967295L)) {
+                        return null;
+                    }
+                    bytes[0] = (byte) (int) (l >> 24 & 0xFF);
+                    bytes[1] = (byte) (int) ((l & 0xFFFFFF) >> 16 & 0xFF);
+                    bytes[2] = (byte) (int) ((l & 0xFFFF) >> 8 & 0xFF);
+                    bytes[3] = (byte) (int) (l & 0xFF);
+                    break;
+                case 2:
+                    l = Integer.parseInt(elements[0]);
+                    if ((l < 0L) || (l > 255L)) {
+                        return null;
+                    }
+                    bytes[0] = (byte) (int) (l & 0xFF);
+                    l = Integer.parseInt(elements[1]);
+                    if ((l < 0L) || (l > 16777215L)) {
+                        return null;
+                    }
+                    bytes[1] = (byte) (int) (l >> 16 & 0xFF);
+                    bytes[2] = (byte) (int) ((l & 0xFFFF) >> 8 & 0xFF);
+                    bytes[3] = (byte) (int) (l & 0xFF);
+                    break;
+                case 3:
+                    for (i = 0; i < 2; ++i) {
+                        l = Integer.parseInt(elements[i]);
+                        if ((l < 0L) || (l > 255L)) {
+                            return null;
+                        }
+                        bytes[i] = (byte) (int) (l & 0xFF);
+                    }
+                    l = Integer.parseInt(elements[2]);
+                    if ((l < 0L) || (l > 65535L)) {
+                        return null;
+                    }
+                    bytes[2] = (byte) (int) (l >> 8 & 0xFF);
+                    bytes[3] = (byte) (int) (l & 0xFF);
+                    break;
+                case 4:
+                    for (i = 0; i < 4; ++i) {
+                        l = Integer.parseInt(elements[i]);
+                        if ((l < 0L) || (l > 255L)) {
+                            return null;
+                        }
+                        bytes[i] = (byte) (int) (l & 0xFF);
+                    }
+                    break;
+                default:
+                    return null;
+            }
+        } catch (NumberFormatException e) {
+            return null;
+        }
+        return bytes;
+    }
+
+    /**
+     * 获取IP地址
+     *
+     * @return 本地IP地址
+     */
+    public static String getHostIp() {
+        try {
+            return InetAddress.getLocalHost().getHostAddress();
+        } catch (UnknownHostException e) {
+        }
+        return "127.0.0.1";
+    }
+
+    /**
+     * 获取主机名
+     *
+     * @return 本地主机名
+     */
+    public static String getHostName() {
+        try {
+            return InetAddress.getLocalHost().getHostName();
+        } catch (UnknownHostException e) {
+        }
+        return "未知";
+    }
+
+    /**
+     * 从多级反向代理中获得第一个非unknown IP地址
+     *
+     * @param ip 获得的IP地址
+     * @return 第一个非unknown IP地址
+     */
+    public static String getMultistageReverseProxyIp(String ip) {
+        // 多级反向代理检测
+        if (ip != null && ip.indexOf(",") > 0) {
+            final String[] ips = ip.trim().split(",");
+            for (String subIp : ips) {
+                if (false == isUnknown(subIp)) {
+                    ip = subIp;
+                    break;
+                }
+            }
+        }
+        return ip;
+    }
+
+    /**
+     * 检测给定字符串是否为未知,多用于检测HTTP请求相关
+     *
+     * @param checkString 被检测的字符串
+     * @return 是否未知
+     */
+    public static boolean isUnknown(String checkString) {
+        return StringUtils.isBlank(checkString) || "unknown".equalsIgnoreCase(checkString);
+    }
+
+
+}

+ 304 - 0
hhface-common/src/main/java/cn/hh/common/utils/ImageUtil.java

@@ -0,0 +1,304 @@
+package cn.hh.common.utils;
+
+import com.alibaba.fastjson.JSON;
+import lombok.extern.slf4j.Slf4j;
+import net.coobird.thumbnailator.Thumbnails;
+import sun.misc.BASE64Decoder;
+import sun.misc.BASE64Encoder;
+
+import javax.imageio.ImageIO;
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Base64.Encoder;
+
+/**
+ * @author shisl
+ * @package cn.hh.common.util
+ * @class ImageUtil
+ * @date 2022/12/21 下午6:38
+ * @description
+ */
+@Slf4j
+public class ImageUtil {
+
+
+    /**
+     * 本地图片转base64
+     *
+     * @param imgFile 本地地址 (例:"/Users/leon/Desktop/092040.jpg")
+     * @return
+     */
+    public static String getImgFileToBase64(String imgFile) {
+        //将图片文件转化为字节数组字符串,并对其进行Base64编码处理
+        InputStream inputStream = null;
+        byte[] buffer = null;
+        //读取图片字节数组
+        try {
+            inputStream = new FileInputStream(imgFile);
+            int count = 0;
+            while (count == 0) {
+                count = inputStream.available();
+            }
+            buffer = new byte[count];
+            inputStream.read(buffer);
+        } catch (IOException e) {
+            e.printStackTrace();
+        } finally {
+            if (inputStream != null) {
+                try {
+                    // 关闭inputStream流
+                    inputStream.close();
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            }
+        }
+        // 对字节数组Base64编码
+        return new BASE64Encoder().encode(buffer);
+    }
+
+    /**
+     * 压缩base64编码至40K以内
+     *
+     * @param base64Img
+     * @param targetFileSize 举例:图片质量压缩至40k以内 那就是 targetFileSize = 40
+     * @return
+     */
+    public static String resizeImageTo100Kb(String base64Img, long targetFileSize) {
+        try {
+
+            base64Img = base64Img.replace("data:image/png;base64,", "").replace("data:image/jpg;base64,", "").replace("data:image/jpeg;base64,", "");
+
+            BASE64Decoder decoder = new BASE64Decoder();
+            byte[] bytes = decoder.decodeBuffer(base64Img);
+            byte[] returnBytes = compressPicForScale(bytes, targetFileSize);
+            InputStream stream = new ByteArrayInputStream(returnBytes);
+            BufferedImage src = ImageIO.read(stream);
+            BufferedImage output = Thumbnails.of(src).size(src.getWidth(), src.getHeight()).asBufferedImage();
+            String base64 = bufferImage2Base64(output);
+            return base64;
+        } catch (Exception e) {
+            return base64Img;
+        }
+    }
+
+    /**
+     * 图片bufferImage转base64
+     *
+     * @param bufferedImage
+     * @return
+     */
+    public static String bufferImage2Base64(BufferedImage bufferedImage) {
+        byte[] bytes = new byte[0];
+        try {
+            bytes = ImageUtil.bufferedImageToByte(bufferedImage, "png");
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+        byte[] compressBytes = new byte[0];
+        try {
+            compressBytes = ImageUtil.compress(bytes, 1, "JPEG");
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+        Encoder encoder = java.util.Base64.getEncoder();
+        //转换成base64串
+        String base64 = encoder.encodeToString(compressBytes).trim();
+        //删除 \r\n
+        base64 = base64.replaceAll("\n", "").replaceAll("\r", "");
+        return base64;
+
+    }
+
+    /**
+     * bufferedImage转Byte
+     *
+     * @param bufferedImage
+     * @param formatName
+     * @return
+     * @throws IOException
+     */
+    public static byte[] bufferedImageToByte(BufferedImage bufferedImage, String formatName) throws IOException {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        ImageIO.write(bufferedImage, formatName, baos);
+        baos.flush();
+        byte[] imageInByte = baos.toByteArray();
+        baos.close();
+        return imageInByte;
+    }
+
+    /**
+     * 按照 宽高 比例压缩
+     *
+     * @param scale 压缩刻度
+     * @return 压缩后图片数据
+     * @throws IOException 压缩图片过程中出错
+     */
+    public static byte[] compress(byte[] srcImgData, double scale, String imageType) throws IOException {
+        BufferedImage bi = ImageIO.read(new ByteArrayInputStream(srcImgData));
+        // 源图宽度
+        int width = (int) (bi.getWidth() * scale);
+        // 源图高度
+        int height = (int) (bi.getHeight() * scale);
+        Image image = bi.getScaledInstance(width, height, Image.SCALE_SMOOTH);
+        BufferedImage tag = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
+        Graphics g = tag.getGraphics();
+        g.setColor(Color.RED);
+        // 绘制处理后的图
+        g.drawImage(image, 0, 0, null);
+        g.dispose();
+        try {
+            ByteArrayOutputStream bOut = new ByteArrayOutputStream();
+            ImageIO.write(tag, imageType, bOut);
+            byte[] bytes = bOut.toByteArray();
+            //关闭流
+            if (Preconditions.isNotBlank(bOut)) {
+                bOut.close();
+            }
+            return bytes;
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+
+    /**
+     * 根据指定大小压缩图片
+     *
+     * @param imageBytes  源图片字节数组
+     * @param desFileSize 指定图片大小,单位kb
+     * @return 压缩质量后的图片字节数组
+     */
+    public static byte[] compressPicForScale(byte[] imageBytes, long desFileSize) {
+        if (imageBytes == null || imageBytes.length <= 0 || imageBytes.length < desFileSize * 1024) {
+            return null;
+        }
+        long srcSize = imageBytes.length;
+//        double accuracy = getAccuracy(srcSize / 1024);
+        double accuracy = 0.9;
+        try {
+            while (imageBytes.length > desFileSize * 1024) {
+                ByteArrayInputStream inputStream = new ByteArrayInputStream(imageBytes);
+                ByteArrayOutputStream outputStream = new ByteArrayOutputStream(imageBytes.length);
+                Thumbnails.of(inputStream)
+                        .scale(accuracy)
+                        .outputQuality(accuracy)
+                        .toOutputStream(outputStream);
+                imageBytes = outputStream.toByteArray();
+            }
+            log.info("【图片压缩】| 图片原大小={" + srcSize / 1024 + "}kb | 压缩后大小={" + imageBytes.length / 1024 + "}kb | ");
+        } catch (Exception e) {
+            log.error("【图片压缩】msg=图片压缩失败!" + e);
+        }
+        return imageBytes;
+    }
+
+    /**
+     * 自动调节精度(经验数值)
+     *
+     * @param size 源图片大小
+     * @return 图片压缩质量比
+     */
+    private static double getAccuracy(long size) {
+        double accuracy;
+        if (size < 900) {
+            accuracy = 0.85;
+        } else if (size < 2047) {
+            accuracy = 0.6;
+        } else if (size < 3275) {
+            accuracy = 0.44;
+        } else {
+            accuracy = 0.4;
+        }
+        return accuracy;
+
+    }
+
+    public static String urlToBase64(String url) {
+        URL urlfile = null;
+        try {
+            urlfile = new URL(url);
+        } catch (MalformedURLException e) {
+            e.printStackTrace();
+        }
+        byte[] data = null;
+        try {
+            assert urlfile != null;
+            InputStream inputStream = urlfile.openStream();
+            ByteArrayOutputStream swapStream = new ByteArrayOutputStream();
+            byte[] buff = new byte[100];
+            int rc = 0;
+            while ((rc = inputStream.read(buff, 0, 100)) > 0) {
+                swapStream.write(buff, 0, rc);
+            }
+            data = swapStream.toByteArray();
+            swapStream.close();
+            inputStream.close();
+        } catch (Exception e) {
+            e.printStackTrace();
+            log.error("图片",e);
+        }
+        if(data == null){
+            return null;
+        }
+        return new BASE64Encoder().encode(data);
+    }
+
+    /**
+     * base64 转 input流
+     *
+     * @param base64string base64字符串
+     * @return 输入流
+     */
+    public static InputStream base2InputStream(String base64string) {
+        ByteArrayInputStream stream = null;
+        try {
+            BASE64Decoder decoder = new BASE64Decoder();
+            byte[] bytes1 = decoder.decodeBuffer(base64string);
+            stream = new ByteArrayInputStream(bytes1);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return stream;
+    }
+
+    public static BufferedImage base64ToBufferedImage(String base64Str) {
+        try {
+            BufferedImage image = ImageIO.read(base2InputStream(base64Str));
+            int width = image.getWidth();
+            int height = image.getHeight();
+            // 重点,技巧在这个参数BufferedImage.TYPE_BYTE_BINARY
+            BufferedImage binaryImage = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_BINARY);
+            for (int y = 0; y < height; y++) {
+                for (int x = 0; x < width; x++) {
+                    int rgb = image.getRGB(x, y);
+                    binaryImage.setRGB(x, y, rgb);
+                }
+            }
+            return binaryImage;
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    static String qrcode = "iVBORw0KGgoAAAANSUhEUgAAAMgAAADIAQAAAACFI5MzAAABIElEQVR42u2YOxLDIAxEodIxuCmfm3IMKhNphR0nnrSRCjTEQ/xSrAUrFIf5K8Imm/yZ1MCRjpDmCHHIl+iGNP6MxNdKPY607jghIpZVQ+xINSRvpGFSXJJztZ2RM5GVZPLYB5ZEXdKGjqd/LInGQcsrj7pjSVg1iko6qAfqmXrxQ0gG347yq1O4D9JmzyxaMsoOXm52Qg7SAdXM55VRe8JTFp0BofrmEmtSZev1AL243vaBNeHVXsUYpYWjOCIow5Asy/7RoVgTFONMa9njvUOxJhpN7Xs9hA9ynWaSSyktPboh2gXAvtp73lxiTrQSXy0AmhRXpEpnl1Ck3xl1QlZetY3yQ85/ObwTceT24oYsl6A841j79o8h2e8PNrEnL5SoG606fWD4AAAAAElFTkSuQmCC";
+
+    public static void main(String[] args) {
+        try {
+            log.info(JSON.toJSONString(QrCodeUtils.decode(base2InputStream(qrcode))));
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+
+}

+ 45 - 0
hhface-common/src/main/java/cn/hh/common/utils/ListUtils.java

@@ -0,0 +1,45 @@
+package cn.hh.common.utils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author shisl
+ * @package cn.hh.common.utils
+ * @class ListUtils
+ * @date 2023/9/29 下午4:29
+ * @description
+ */
+public class ListUtils {
+
+    /**
+     * @param target          将要被拆分的list
+     * @param targetSplitSize 把将要拆分的list拆分成目标list的个数
+     * @return
+     */
+    public static <T> List<List<T>> averageAssign(List<T> target, int targetSplitSize) {
+        List<List<T>> resultList = new ArrayList<>();
+        //先求每组平均分多少个,如果不够分补足,最后一个少分
+        int avgSize = target.size() / targetSplitSize;
+        //求出最后一组的个数
+        int lastArraySize = target.size() - avgSize * (targetSplitSize - 1);
+        int k = 0;  //取出list中元素的计数器
+        for (int i = 0; i < targetSplitSize; i++) {
+            List<T> list = new ArrayList<T>();
+            //如果不是最后一组则按照平均大小拆分目标list
+            if (i != (targetSplitSize - 1)) {
+                for (int j = 0; j < avgSize; j++) {
+                    list.add(target.get(k));
+                    k++;
+                }
+            } else {
+                for (int j = 0; j < lastArraySize; j++) {
+                    list.add(target.get(k));
+                    k++;
+                }
+            }
+            resultList.add(list);
+        }
+        return resultList;
+    }
+}

+ 18 - 0
hhface-common/src/main/java/cn/hh/common/utils/PasswordUtils.java

@@ -0,0 +1,18 @@
+package cn.hh.common.utils;
+
+import cn.hh.common.config.Constants;
+import org.springframework.util.DigestUtils;
+
+/**
+ * @description: PasswordUtils <br>
+ * @date: 2021/5/20 16:30 <br>
+ * @author: PWB <br>
+ */
+public class PasswordUtils {
+
+    public static String buildPw(String password) {
+        return DigestUtils.md5DigestAsHex(Constants.SALT_PRE.concat(password).concat(Constants.SALT).getBytes());
+    }
+
+
+}

+ 67 - 0
hhface-common/src/main/java/cn/hh/common/utils/Preconditions.java

@@ -0,0 +1,67 @@
+package cn.hh.common.utils;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author shisl
+ * @package cn.hh.common.util
+ * @class Preconditions
+ * @date 2022/12/19 下午4:51
+ * @description
+ */
+public class Preconditions {
+
+    /**
+     * check if object is null for List Map String
+     *
+     * @param t
+     * @param <T>
+     * @return
+     */
+    public static <T> boolean isBlank(T t) {
+
+        boolean result = false;
+
+        if (t == null) {
+            return true;
+        }
+
+        if (t instanceof List) {
+            if (((List) t).size() == 0) {
+                return true;
+            }
+        } else if (t instanceof Map) {
+            if (((Map) t).size() == 0) {
+                return true;
+            }
+        } else if (t instanceof Object[]) {
+            if (((Object[]) t).length == 0) {
+                return true;
+            }
+        } else if (t instanceof String) {
+            int strLen;
+
+            strLen = ((String) t).length();
+            if (strLen == 0) {
+                return true;
+            }
+
+            for (int i = 0; i < strLen; i++) {
+                if ((!Character.isWhitespace(((String) t).charAt(i)))) {
+                    return false;
+                }
+            }
+
+            return true;
+        }
+
+        return result;
+    }
+
+    public static <T> boolean isNotBlank(T t) {
+        return !isBlank(t);
+    }
+
+
+}

+ 168 - 0
hhface-common/src/main/java/cn/hh/common/utils/QrCodeUtils.java

@@ -0,0 +1,168 @@
+package cn.hh.common.utils;
+
+import cn.hh.common.constants.CommonConstants;
+import com.google.zxing.BarcodeFormat;
+import com.google.zxing.BinaryBitmap;
+import com.google.zxing.DecodeHintType;
+import com.google.zxing.EncodeHintType;
+import com.google.zxing.LuminanceSource;
+import com.google.zxing.MultiFormatReader;
+import com.google.zxing.MultiFormatWriter;
+import com.google.zxing.Result;
+import com.google.zxing.client.j2se.BufferedImageLuminanceSource;
+import com.google.zxing.common.BitMatrix;
+import com.google.zxing.common.HybridBinarizer;
+import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+
+import javax.imageio.ImageIO;
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Hashtable;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * @author shisl
+ * @package cn.hh.common.util
+ * @class QrCodeUtils
+ * @date 2023/1/14 下午2:47
+ * @description 二维码工具类
+ */
+@Slf4j
+public class QrCodeUtils {
+
+    // 二维码颜色
+    private static final int BLACK = 0xFF000000;
+
+    private static final int WHITE = 0xFFFFFFFF;
+
+
+    /**
+     * 根据内容,生成指定宽高、指定格式的二维码图片
+     *
+     * @param text   内容
+     * @param width  宽
+     * @param height 高
+     * @return 生成的二维码图片路径
+     * @throws Exception
+     */
+    public static BufferedImage generateQrCode(String text, int width, int height)
+            throws Exception {
+        BufferedImage bufferedImage = writeToFile(generateQrCodeBitMatrix(text, width, height));
+        return bufferedImage;
+    }
+
+
+    public static BitMatrix generateQrCodeBitMatrix(String text, int width, int height)
+            throws Exception {
+        Hashtable<EncodeHintType, Object> hints = new Hashtable<>();
+        // 指定编码格式
+        hints.put(EncodeHintType.CHARACTER_SET, CommonConstants.UTF8);
+        // 指定纠错等级
+        hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L);
+        // 白边大小,取值范围0~4
+        hints.put(EncodeHintType.MARGIN, 0);
+        BitMatrix bitMatrix = new MultiFormatWriter().encode(text, BarcodeFormat.QR_CODE, width, height, hints);
+        return bitMatrix;
+    }
+
+
+    /**
+     * 生成二维码图片
+     *
+     * @param matrix
+     * @throws IOException
+     */
+    public static BufferedImage writeToFile(BitMatrix matrix) {
+        BufferedImage image = toBufferedImage(matrix);
+        return image;
+    }
+
+    /**
+     * @param matrix
+     * @return
+     */
+    private static BufferedImage toBufferedImage(BitMatrix matrix) {
+        int width = matrix.getWidth();
+        int height = matrix.getHeight();
+        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
+        for (int x = 0; x < width; x++) {
+            for (int y = 0; y < height; y++) {
+                image.setRGB(x, y, matrix.get(x, y) ? BLACK : WHITE);
+            }
+        }
+        return image;
+    }
+
+
+    /**
+     * 解析二维码图像
+     *
+     * @param base64Str
+     * @return 二维码内容
+     */
+    public static QRResult decode(String base64Str) {
+        return decode(ImageUtil.base2InputStream(base64Str));
+    }
+
+    /**
+     * 解析二维码图像
+     * <p>
+     * return 二维码内容
+     */
+    /**
+     * 流图片解码
+     */
+    public static QRResult decode(InputStream input) {
+        BufferedImage image;
+        try {
+            if (null == input) {
+                return new QRResult("得到的文件不存在!", 300);
+            }
+            image = ImageIO.read(input);
+            LuminanceSource source = new BufferedImageLuminanceSource(image);
+            BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
+            Map<DecodeHintType, Object> hints = new LinkedHashMap<DecodeHintType, Object>();
+            // 解码设置编码方式为:utf-8,
+            hints.put(DecodeHintType.CHARACTER_SET, "utf-8");
+            //优化精度
+            hints.put(DecodeHintType.TRY_HARDER, Boolean.TRUE);
+            //复杂模式,开启PURE_BARCODE模式
+            hints.put(DecodeHintType.PURE_BARCODE, Boolean.TRUE);
+            Result result = new MultiFormatReader().decode(bitmap, hints);
+            String txt = result.getText();
+            return new QRResult("成功解码!", 200, txt);
+        } catch (Exception e) {
+            log.error("解码失败。", e);
+            return new QRResult("解码失败,请确认的你二维码是否正确,或者图片有多个二维码!", 500);
+        }
+    }
+
+    /**
+     * 返回值处理
+     */
+    @Data
+    public static class QRResult {
+        public QRResult(String message, int status) {
+            this.message = message;
+            this.status = status;
+            this.txt = "";
+        }
+
+        public QRResult(String message, int status, String txt) {
+            this.message = message;
+            this.status = status;
+            this.txt = txt;
+        }
+
+        //解码内容
+        private String txt; //返回的消息内容
+        private String message; //返回的状态码,200:成功,500:错误
+        private int status;
+    }
+
+
+}

+ 168 - 0
hhface-common/src/main/java/cn/hh/common/utils/RandomUtils.java

@@ -0,0 +1,168 @@
+package cn.hh.common.utils;
+
+import java.math.BigDecimal;
+import java.util.Random;
+
+/**
+ * @author shisl
+ * @package cn.hh.common.utils
+ * @class RandomUtils
+ * @date 2024/2/28 上午11:53
+ * @description
+ */
+public class RandomUtils {
+    public static final String ALL_CHAR = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
+    public static final String LETTER_CHAR = "abcdefghijkllmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
+    public static final String NUMBER_CHAR = "0123456789";
+
+    /**
+     * 返回一个定长的随机字符串(只包含大小写字母、数字)
+     *
+     * @param length 随机字符串长度
+     * @return 随机字符串
+     */
+    public static String generateString(int length) {
+        StringBuffer sb = new StringBuffer();
+        Random random = new Random();
+        for (int i = 0; i < length; i++) {
+            sb.append(ALL_CHAR.charAt(random.nextInt(ALL_CHAR.length())));
+        }
+        return sb.toString();
+    }
+
+    /**
+     * 返回一个定长的随机纯字母字符串(只包含大小写字母)
+     *
+     * @param length 随机字符串长度
+     * @return 随机字符串
+     */
+    public static String generateMixString(int length) {
+        StringBuffer sb = new StringBuffer();
+        Random random = new Random();
+        for (int i = 0; i < length; i++) {
+            sb.append(LETTER_CHAR.charAt(random.nextInt(LETTER_CHAR.length())));
+        }
+        return sb.toString();
+    }
+
+    /**
+     * 返回一个定长的随机纯大写字母字符串(只包含大小写字母)
+     *
+     * @param length 随机字符串长度
+     * @return 随机字符串
+     */
+    public static String generateLowerString(int length) {
+        return generateMixString(length).toLowerCase();
+    }
+
+    /**
+     * 返回一个定长的随机纯小写字母字符串(只包含大小写字母)
+     *
+     * @param length 随机字符串长度
+     * @return 随机字符串
+     */
+    public static String generateUpperString(int length) {
+        return generateMixString(length).toUpperCase();
+    }
+
+    /**
+     * 生成一个定长的纯0字符串
+     *
+     * @param length 字符串长度
+     * @return 纯0字符串
+     */
+    public static String generateZeroString(int length) {
+        StringBuffer sb = new StringBuffer();
+        for (int i = 0; i < length; i++) {
+            sb.append('0');
+        }
+        return sb.toString();
+    }
+
+    /**
+     * 根据数字生成一个定长的字符串,长度不够前面补0
+     *
+     * @param num        数字
+     * @param fixedLenth 字符串长度
+     * @return 定长的字符串
+     */
+    public static String toFixedLengthString(long num, int fixedLenth) {
+        StringBuffer sb = new StringBuffer();
+        String strNum = String.valueOf(num);
+        if (fixedLenth - strNum.length() >= 0) {
+            sb.append(generateZeroString(fixedLenth - strNum.length()));
+        } else {
+            throw new RuntimeException("将数字" + num + "转化为长度为" + fixedLenth
+                    + "的字符串发生异常!");
+        }
+        sb.append(strNum);
+        return sb.toString();
+    }
+
+    /**
+     * 每次生成的len位数都不相同
+     *
+     * @param param
+     * @return 定长的数字
+     */
+    public static int getNotSimple(int[] param, int len) {
+        Random rand = new Random();
+        for (int i = param.length; i > 1; i--) {
+            int index = rand.nextInt(i);
+            int tmp = param[index];
+            param[index] = param[i - 1];
+            param[i - 1] = tmp;
+        }
+        int result = 0;
+        for (int i = 0; i < len; i++) {
+            result = result * 10 + param[i];
+        }
+        return result;
+    }
+
+
+    /**
+     * 获取随机的几位数字
+     *
+     * @param length
+     * @return
+     */
+    public static String generateNumberString(int length) {
+        StringBuffer sb = new StringBuffer();
+        Random random = new Random();
+        for (int i = 0; i < length; i++) {
+            sb.append(NUMBER_CHAR.charAt(random.nextInt(NUMBER_CHAR.length())));
+        }
+        return sb.toString();
+    }
+
+    /**
+     * @param number
+     * @param decimal
+     * @return
+     */
+    public static String round(double number, int decimal) {
+        return new BigDecimal(number).setScale(decimal, BigDecimal.ROUND_HALF_UP).toString();
+    }
+
+
+    /**
+     * max >= min
+     *
+     * @param min
+     * @param max
+     * @return 返回min(包括)和max(包括)之间的整数
+     */
+    public static int random(int min, int max) {
+        if (max > min) {
+            return new Random().nextInt(max + 1 - min) + min;
+        } else {
+            return min;
+        }
+    }
+
+    public static void main(String[] args) {
+        System.out.println(generateString(16).toLowerCase());
+    }
+
+}

+ 53 - 0
hhface-common/src/main/java/cn/hh/common/utils/RequestUriUtils.java

@@ -0,0 +1,53 @@
+package cn.hh.common.utils;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author shisl
+ * @package cn.hh.common.utils
+ * @class RequestUriUtils
+ * @date 2023/12/10 下午6:02
+ * @description 请求路径工具类
+ */
+public class RequestUriUtils {
+
+    /**
+     * 将路径参数转换成Map对象,如果路径参数出现重复参数名,将以最后的参数值为准
+     * @param uri 传入的携带参数的路径
+     * @return
+     */
+    public static Map<String, String> getParams(String uri) {
+        Map<String, String> params = new HashMap<>(10);
+
+        int idx = uri.indexOf("?");
+        if (idx != -1) {
+            String[] paramsArr = uri.substring(idx + 1).split("&");
+
+            for (String param : paramsArr) {
+                idx = param.indexOf("=");
+                params.put(param.substring(0, idx), param.substring(idx + 1));
+            }
+        }
+
+        return params;
+    }
+
+    /**
+     * 获取URI中参数以外部分路径
+     * @param uri
+     * @return
+     */
+    public static String getBasePath(String uri) {
+        if (uri == null || uri.isEmpty())
+            return null;
+
+        int idx = uri.indexOf("?");
+        if (idx == -1)
+            return uri;
+
+        return uri.substring(0, idx);
+    }
+
+}
+

+ 117 - 0
hhface-common/src/main/java/cn/hh/common/utils/Sm4Utils.java

@@ -0,0 +1,117 @@
+package cn.hh.common.utils;
+
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.crypto.SecureUtil;
+import cn.hutool.crypto.symmetric.SymmetricCrypto;
+import com.alibaba.fastjson.JSONObject;
+import lombok.extern.slf4j.Slf4j;
+
+import java.nio.charset.Charset;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author shisl
+ * @package cn.hh.common.utils
+ * @class Sm4Utils
+ * @date 2024/2/28 上午11:51
+ * @description
+ */
+@Slf4j
+public class Sm4Utils {
+
+    /**
+     * 加密
+     *
+     * @param privateKey 管理员提供
+     * @param str
+     * @return
+     */
+    public static String encrypt(final String privateKey, final String str) {
+        if (StrUtil.isBlank((CharSequence) str)) {
+            return "";
+        }
+        SymmetricCrypto sm4 = new SymmetricCrypto("SM4/ECB/PKCS5Padding", privateKey.getBytes());
+        String sm4Encrypt = sm4.encryptHex(str, Charset.forName("UTF-8"));
+        return sm4Encrypt;
+    }
+
+    /**
+     * 解密
+     *
+     * @param privateKey 管理员提供
+     * @param str
+     * @return
+     */
+    public static String decrypt(final String privateKey, final String str) {
+        if (StrUtil.isBlank((CharSequence) str)) {
+            return null;
+        }
+        try {
+            SymmetricCrypto sm4 = new SymmetricCrypto("SM4/ECB/PKCS5Padding", privateKey.getBytes());
+            String sm4Decrypt = sm4.decryptStr(str, Charset.forName("UTF-8"));
+            return sm4Decrypt;
+        } catch (Exception e) {
+            log.error("解密失败", e);
+            return null;
+        }
+    }
+
+    /**
+     * 验证签名
+     *
+     * @param appKey
+     * @param appSecret
+     * @param sign
+     * @param timestamp
+     * @return
+     */
+    public static Boolean checkSign(String appKey, String appSecret, String sign, String timestamp) {
+        if (Preconditions.isBlank(sign) || Preconditions.isBlank(timestamp)) {
+            return Boolean.FALSE;
+        }
+
+        String dbSign = SecureUtil.md5(new StringBuilder().append(appKey)
+                .append(appSecret)
+                .append(timestamp).toString()).toLowerCase();
+        if (!dbSign.equals(sign)) {
+            return Boolean.FALSE;
+        }
+        return Boolean.TRUE;
+    }
+
+    /**
+     * 获取签名
+     *
+     * @param appKey
+     * @param appSecret
+     * @param timestamp
+     * @return
+     */
+    public static String getSign(String appKey, String appSecret, String timestamp) {
+        String sign = SecureUtil.md5(new StringBuilder().append(appKey)
+                .append(appSecret)
+                .append(timestamp).toString()).toLowerCase();
+        return sign;
+    }
+
+
+    public static void main(String[] args) {
+        //基础信息
+        Map<String, Object> map = new HashMap<>();
+//        map.put("idNumber", "430923198812097216");
+//        map.put("name", "谌涛");
+//        map.put("phone", "");
+        map.put("sn", "ZP241900D001");
+
+        //5d1c738387789ed0d1c531d277ab5638a55a8879
+        String encrypt = Sm4Utils.encrypt("eqvcylnkvypytpmw", JSONObject.toJSONString(map));
+        System.out.println(encrypt);
+        String decrypt = Sm4Utils.decrypt("eqvcylnkvypytpmw", "c4052c938b2a1feb06152fa038416951fe51ec1b07ff4461e3beec1b9c21144426e33f7fd578af3656c6d832e09e7c93a4793056712d571cec76b18004beadc8131819a830411546a3e7558a22a77615c196e31a4b56d276564792ae28480427da27a67649cfdadeac24b4badb4156fc15ad82b46b8582db9441f536a65097ecb541fc764f3940dcaa7c0a4504683eaed0a47b337cdc429e64436fe2ed1e65be1db3e983e50c32e7ae5e57aaae821e42d29a36caed8658a5bcc5a351e1d542e0f4c86705217126945e76612046fc8ae3e1de16819b2da59930a59ccad47c53b3017c174fd9bcd2f223f0857e15494c43641c249ebac8b6de51d014e1006cffc267ee53927cd89171c2793456c786673e62104318010e9184993b34c3468c3497ce544bb328d22e3b6587f1ffb6b1cebe");
+        System.out.println(decrypt);
+        String timestamp = String.valueOf(System.currentTimeMillis());
+        System.out.println(timestamp);
+        System.out.println(Sm4Utils.getSign("hhsLh3Jx0dB", "80116bec9b3ae44e2831bf8805d2bca337bc68a6", timestamp));
+
+    }
+}

+ 203 - 0
hhface-common/src/main/java/cn/hh/common/utils/SnowflakeIdWorker.java

@@ -0,0 +1,203 @@
+package cn.hh.common.utils;
+
+/**
+ * @description: * Twitter_Snowflake<br>
+ * SnowFlake的结构如下(每部分用-分开):<br>
+ * 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000 <br>
+ * 1位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0<br>
+ * 41位时间截(毫秒级),注意,41位时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 开始时间截)
+ * 得到的值),这里的的开始时间截,一般是我们的id生成器开始使用的时间,由我们程序来指定的(如下下面程序IdWorker类的startTime属性)。41位的时间截,可以使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69<br>
+ * 10位的数据机器位,可以部署在1024个节点,包括5位dataCenterId和5位workerId<br>
+ * 12位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号<br>
+ * 加起来刚好64位,为一个Long型。<br>
+ * SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高,经测试,SnowFlake每秒能够产生26万ID左右。
+ * @date: 2020/7/16 16:17 <br>
+ * @since: 1.0 <br>
+ */
+public class SnowflakeIdWorker {
+
+
+    // ==============================Fields===========================================
+    /**
+     * 开始时间截 (2020-07-07)
+     */
+    private static final long trench = 1594887982462L;
+
+    /**
+     * 机器id所占的位数
+     */
+    private static final long workerIdBits = 5L;
+
+    /**
+     * 数据标识id所占的位数
+     */
+    private static final long dataCenterIdBits = 5L;
+
+    /**
+     * 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数)
+     */
+    private static final long maxWorkerId = ~(-1L << workerIdBits);
+
+    /**
+     * 支持的最大数据标识id,结果是31
+     */
+    private static final long maxDataCenterId = ~(-1L << dataCenterIdBits);
+
+    /**
+     * 序列在id中占的位数
+     */
+    private static final long sequenceBits = 12L;
+
+    /**
+     * 机器ID向左移12位
+     */
+    private static final long workerIdShift = sequenceBits;
+
+    /**
+     * 数据标识id向左移17位(12+5)
+     */
+    private static final long dataCenterIdShift = sequenceBits + workerIdBits;
+
+    /**
+     * 时间截向左移22位(5+5+12)
+     */
+    private static final long timestampLeftShift = sequenceBits + workerIdBits + dataCenterIdBits;
+
+    /**
+     * 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095)
+     */
+    private static final long sequenceMask = ~(-1L << sequenceBits);
+
+    /**
+     * 工作机器ID(0~31)
+     */
+    private long workerId;
+
+    /**
+     * 数据中心ID(0~31)
+     */
+    private long dataCenterId;
+
+    /**
+     * 毫秒内序列(0~4095)
+     */
+    private long sequence = 0L;
+
+    /**
+     * 上次生成ID的时间截
+     */
+    private long lastTimestamp = -1L;
+
+    //==============================Constructors=====================================
+    private static SnowflakeIdWorker instance;
+
+    /**
+     * 构造函数
+     *
+     * @param workerId     工作ID (0~31)
+     * @param dataCenterId 数据中心ID (0~31)
+     */
+    private SnowflakeIdWorker(long workerId, long dataCenterId) {
+        if (workerId > maxWorkerId || workerId < 0) {
+            throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
+        }
+        if (dataCenterId > maxDataCenterId || dataCenterId < 0) {
+            throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDataCenterId));
+        }
+        this.workerId = workerId;
+        this.dataCenterId = dataCenterId;
+    }
+
+    public static long getSnowId() {
+        return getSnowId(0, 0);
+    }
+
+    public static long getSnowId(long workerId, long dataCenterId) {
+        return getInstance(workerId, dataCenterId).nextId();
+    }
+
+    public static SnowflakeIdWorker getInstance(long workerId, long dataCenterId) {
+        if (instance == null) {//先判断是否为null 后上锁进行初始化
+            synchronized (SnowflakeIdWorker.class) {
+                if (instance == null)//将对象上锁之后再次判断 是否有别的线程初始化了
+                    instance = new SnowflakeIdWorker(workerId, dataCenterId);
+            }
+        }
+        return instance;
+    }
+    // ==============================Methods==========================================
+
+    /**
+     * 获得下一个ID (该方法是线程安全的)
+     *
+     * @return SnowflakeId
+     */
+    public synchronized long nextId() {
+        long timestamp = timeGen();
+
+        //如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
+        if (timestamp < lastTimestamp) {
+            throw new RuntimeException(
+                    String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
+        }
+
+        //如果是同一时间生成的,则进行毫秒内序列
+        if (lastTimestamp == timestamp) {
+            sequence = (sequence + 1) & sequenceMask;
+            //毫秒内序列溢出
+            if (sequence == 0) {
+                //阻塞到下一个毫秒,获得新的时间戳
+                timestamp = tilNextMillis(lastTimestamp);
+            }
+        }
+        //时间戳改变,毫秒内序列重置
+        else {
+            sequence = 0L;
+        }
+
+        //上次生成ID的时间截
+        lastTimestamp = timestamp;
+
+        //移位并通过或运算拼到一起组成64位的ID
+        return ((timestamp - trench) << timestampLeftShift) //
+                | (dataCenterId << dataCenterIdShift) //
+                | (workerId << workerIdShift) //
+                | sequence;
+    }
+
+    /**
+     * 阻塞到下一个毫秒,直到获得新的时间戳
+     *
+     * @param lastTimestamp 上次生成ID的时间截
+     * @return 当前时间戳
+     */
+    protected long tilNextMillis(long lastTimestamp) {
+        long timestamp = timeGen();
+        while (timestamp <= lastTimestamp) {
+            timestamp = timeGen();
+        }
+        return timestamp;
+    }
+
+    /**
+     * 返回以毫秒为单位的当前时间
+     *
+     * @return 当前时间(毫秒)
+     */
+    protected long timeGen() {
+        return System.currentTimeMillis();
+    }
+
+    //==============================Test=============================================
+
+    /*public static void main(String[] args) {
+        SnowflakeIdWorker idWorker = new SnowflakeIdWorker(0, 0);
+        System.out.println(System.currentTimeMillis());
+        for (int i = 0; i < 2; i++) {
+            long id = idWorker.nextId();
+            System.out.println(Long.toBinaryString(id));
+            System.out.println(id);
+        }
+    }*/
+
+}

+ 26 - 0
hhface-middleware/DockerFile

@@ -0,0 +1,26 @@
+#jdk基础镜像源
+FROM openjdk:8-bullseye
+
+#设定时区
+ENV TZ=Asia/Shanghai
+RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
+
+RUN echo "deb http://mirrors.aliyun.com/debian/ buster main non-free contrib" > /etc/apt/sources.list \
+&& echo "deb-src http://mirrors.aliyun.com/debian/ buster main non-free contrib" >> /etc/apt/sources.list
+
+RUN apt update && apt install -y curl
+
+#设置当前目录,没有的话会自动新建
+WORKDIR /data
+#将本地文件复制到容器/data目录
+COPY *.jar /data/
+
+#暴露服务端口
+EXPOSE 8761
+
+#设置初始的java启动参数,后续可以在服务启动的环境变量里面覆盖
+#ENV JAVA_OPT="-Xmx1g -Xms1g -Xmn256m -XX:PermSize=512m"
+
+#java启动命令,可以加参数
+#替换正确的jar包名
+CMD ["sh","-c","java -jar hhface-middleware.jar"]

+ 84 - 0
hhface-middleware/pom.xml

@@ -0,0 +1,84 @@
+<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <groupId>com.yx.face</groupId>
+        <artifactId>nm-yqk</artifactId>
+        <version>1.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>hhface-middleware</artifactId>
+    <dependencies>
+        <dependency>
+            <groupId>com.yx.face</groupId>
+            <artifactId>hhface-common</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-freemarker</artifactId>
+        </dependency>
+
+        <!-- websocket相关 -->
+        <!-- https://mvnrepository.com/artifact/io.netty/netty-all -->
+        <dependency>
+            <groupId>io.netty</groupId>
+            <artifactId>netty-all</artifactId>
+            <version>4.1.45.Final</version>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <finalName>hhface-middleware</finalName>
+        <!--加上这个才能打包时将静态资源打进去-->
+        <resources>
+            <resource>
+                <directory>${basedir}/src/main/webapp</directory>
+                <targetPath>META-INF/resources</targetPath>
+                <includes>
+                    <include>**/**</include>
+                </includes>
+            </resource>
+            <resource>
+                <directory>src/main/resources</directory>
+                <includes>
+                    <include>**/*.*</include>
+                </includes>
+                <filtering>true</filtering>
+            </resource>
+            <resource>
+                <directory>src/main/java</directory>
+                <includes>
+                    <include>**/*.xml</include>
+                    <include>**/*.yml</include>
+                    <include>**/*.properties</include>
+                </includes>
+            </resource>
+        </resources>
+
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <!--<configuration>
+                    <fork>true</fork>
+                </configuration>-->
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <version>3.2</version>
+                <configuration>
+                    <fork>true</fork>
+                    <source>1.8</source>
+                    <target>1.8</target>
+                    <verbose>true</verbose>
+                    <encoding>UTF-8</encoding>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>

+ 27 - 0
hhface-middleware/src/main/java/cn/hh/hhface/HhFaceMiddlewareApplication.java

@@ -0,0 +1,27 @@
+package cn.hh.hhface;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.scheduling.annotation.EnableAsync;
+
+/**
+ * @author shisl
+ * @package cn.hh.hhface
+ * @class ManagementPlatformApplication
+ * @date 2023/12/7 下午2:12
+ * @description
+ */
+@Slf4j
+@EnableAsync
+@ComponentScan(basePackages = {"cn.hh.common", "cn.hh.hhface"})
+@SpringBootApplication
+public class HhFaceMiddlewareApplication {
+
+    public static void main(String[] args) {
+        SpringApplication.run(HhFaceMiddlewareApplication.class, args);
+    }
+
+
+}

+ 22 - 0
hhface-middleware/src/main/java/cn/hh/hhface/bean/ChunkPO.java

@@ -0,0 +1,22 @@
+package cn.hh.hhface.bean;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+/**
+ * 大文件上传下载使用
+ */
+@Data
+public class ChunkPO {
+   Integer chunkId;
+   String md5;
+   Integer index;
+
+   public ChunkPO() {
+   }
+
+   public ChunkPO(String md5, Integer index) {
+      this.md5 = md5;
+      this.index = index;
+   }
+}

+ 25 - 0
hhface-middleware/src/main/java/cn/hh/hhface/bean/FilePO.java

@@ -0,0 +1,25 @@
+package cn.hh.hhface.bean;
+
+import lombok.Data;
+
+/**
+ * 大文件上传下载使用
+ */
+@Data
+public class FilePO {
+    Integer fileId;
+    String md5;
+    String name;
+    Long size;
+
+    public FilePO(String name, String md5, Long size) {
+        this.md5 = md5;
+        this.name = name;
+        this.size = size;
+    }
+
+    public FilePO() {
+
+    }
+
+}

+ 41 - 0
hhface-middleware/src/main/java/cn/hh/hhface/bean/account/AccountDto.java

@@ -0,0 +1,41 @@
+package cn.hh.hhface.bean.account;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * @author shisl
+ * @package cn.hh.hhface.bean.account
+ * @class AccountDto
+ * @date 2023/12/10 下午1:30
+ * @description
+ */
+@Data
+public class AccountDto implements Serializable {
+
+    private Long id;
+
+    @ApiModelProperty(value = "登录用户名")
+    private String username;
+
+    @ApiModelProperty(value = "登录密码")
+    private String password;
+
+    @ApiModelProperty(value = "账号类型 0-admin 1-普通用户")
+    private Integer type;
+
+    @ApiModelProperty(value = "管理员姓名")
+    private String nickname;
+
+    @ApiModelProperty(value = "联系人手机号")
+    private String phone;
+
+    @ApiModelProperty(value = "公司名称")
+    private String company;
+
+
+}

+ 36 - 0
hhface-middleware/src/main/java/cn/hh/hhface/bean/account/AccountLoginVo.java

@@ -0,0 +1,36 @@
+package cn.hh.hhface.bean.account;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * @author shisl
+ * @package cn.hh.hhface.bean.account
+ * @class AccountLoginVo
+ * @date 2023/12/7 下午3:45
+ * @description
+ */
+@Data
+public class AccountLoginVo implements Serializable {
+
+    @ApiModelProperty("登录用户id")
+    private Long accountId;
+
+    @ApiModelProperty("登录用户名")
+    private String username;
+
+    @ApiModelProperty("管理员名称")
+    private String nickname;
+
+    @ApiModelProperty("公司名称")
+    private String company;
+
+    @ApiModelProperty("token")
+    private String token;
+
+    @ApiModelProperty("账号等级 0-admin 1-管理员")
+    private Integer type;
+
+}

+ 47 - 0
hhface-middleware/src/main/java/cn/hh/hhface/bean/account/AccountPageDto.java

@@ -0,0 +1,47 @@
+package cn.hh.hhface.bean.account;
+
+import cn.hh.common.bean.BaseBean;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * @author shisl
+ * @package cn.hh.hhface.bean.account
+ * @class AccountDto
+ * @date 2023/12/10 下午1:30
+ * @description
+ */
+@Data
+public class AccountPageDto implements Serializable {
+
+    private Long accountId;
+
+    @ApiModelProperty(value = "登录用户名")
+    private String username;
+
+    @ApiModelProperty(value = "管理员姓名")
+    private String nickname;
+
+    @ApiModelProperty(value = "联系人手机号")
+    private String phone;
+
+    @ApiModelProperty(value = "公司名称")
+    private String company;
+
+    @ApiModelProperty(value = "开始时间")
+    private Date startTime;
+
+    @ApiModelProperty(value = "结束时间")
+    private Date endTime;
+
+    @ApiModelProperty(value = "第几页 起始页1")
+    private Integer pageNumber = 1;
+
+    @ApiModelProperty(value = "每页条数")
+    private Integer pageSize = 10;
+
+
+}

+ 64 - 0
hhface-middleware/src/main/java/cn/hh/hhface/bean/account/AccountVo.java

@@ -0,0 +1,64 @@
+package cn.hh.hhface.bean.account;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * @author shisl
+ * @package cn.hh.hhface.bean.account
+ * @class AccountVo
+ * @date 2023/12/10 下午1:30
+ * @description
+ */
+@Data
+public class AccountVo implements Serializable {
+
+    private Long id;
+
+    @ApiModelProperty(value = "登录用户名")
+    private String username;
+
+    @ApiModelProperty(value = "登录密码")
+    private String password;
+
+    @ApiModelProperty(value = "账号类型 0-admin 1-普通用户")
+    private Integer type;
+
+    @ApiModelProperty(value = "管理员姓名")
+    private String nickname;
+
+    @ApiModelProperty(value = "联系人手机号")
+    private String phone;
+
+    @ApiModelProperty(value = "公司名称")
+    private String company;
+
+    @ApiModelProperty(value = "创建时间")
+    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+
+    /**
+     * appId 32位 9805f4fb52a90a6941935fc3290673f4
+     */
+    private String appId;
+
+    /**
+     * 权限校验的appKey
+     */
+    private String appKey;
+    /**
+     * 权限校验的appSecret
+     */
+    private String appSecret;
+
+    /**
+     * 权限校验的privateKey 16位随机数 包含字母和数据
+     */
+    private String privateKey;
+
+
+}

+ 53 - 0
hhface-middleware/src/main/java/cn/hh/hhface/bean/apk/AppApkDto.java

@@ -0,0 +1,53 @@
+package cn.hh.hhface.bean.apk;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * @author shisl
+ * @package cn.hh.hhface.bean.apk
+ * @class AppApkDto
+ * @date 2023/12/12 下午5:19
+ * @description
+ */
+@Data
+public class AppApkDto implements Serializable {
+
+    private Long id;
+
+    @ApiModelProperty(value = "绑定账号", required = true)
+    private List<Long> accountIds;
+
+    @ApiModelProperty(value = "应用类型 1:K8小程序容器 AppTypeEnum", required = true)
+    private Integer appType;
+
+    @ApiModelProperty(value = "安装包名称", required = true)
+    private String apkTitle;
+
+    @ApiModelProperty(value = "安装包版本", required = true)
+    private String apkVersion;
+
+    @ApiModelProperty(value = "文件md5 file表f_md5", required = true)
+    private String fileMd5;
+
+    @ApiModelProperty(value = "备注")
+    private String remarks;
+
+    @ApiModelProperty(value = "安装包的地址")
+    private String apkPath;
+
+    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+
+    @ApiModelProperty(value = "包名")
+    private String packageName;
+
+    @ApiModelProperty(value = "创建者账号")
+    private Long accountId;
+
+}

+ 51 - 0
hhface-middleware/src/main/java/cn/hh/hhface/bean/apk/AppApkEditDto.java

@@ -0,0 +1,51 @@
+package cn.hh.hhface.bean.apk;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * @author shisl
+ * @package cn.hh.hhface.bean.apk
+ * @class AppApkEditDto
+ * @date 2023/12/29 下午5:58
+ * @description
+ */
+@Data
+public class AppApkEditDto {
+
+    private Long id;
+
+    @ApiModelProperty(value = "创建者账号")
+    private Long accountId;
+
+    @ApiModelProperty(value = "创建者账号")
+    private List<Long> accountIds;
+
+    @ApiModelProperty(value = "应用类型 1:K8小程序容器 AppTypeEnum", required = true)
+    private Integer appType;
+
+    @ApiModelProperty(value = "安装包名称", required = true)
+    private String apkTitle;
+
+    @ApiModelProperty(value = "安装包版本", required = true)
+    private String apkVersion;
+
+    @ApiModelProperty(value = "文件md5 file表f_md5", required = true)
+    private String fileMd5;
+
+    @ApiModelProperty(value = "备注")
+    private String remarks;
+
+    @ApiModelProperty(value = "安装包的地址")
+    private String apkPath;
+
+    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+
+    @ApiModelProperty(value = "包名")
+    private String packageName;
+}

+ 47 - 0
hhface-middleware/src/main/java/cn/hh/hhface/bean/apk/AppApkPageDto.java

@@ -0,0 +1,47 @@
+package cn.hh.hhface.bean.apk;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * @author shisl
+ * @package cn.hh.hhface.bean.apk
+ * @class AppApkPageDto
+ * @date 2023/12/12 下午5:20
+ * @description
+ */
+@Data
+public class AppApkPageDto implements Serializable {
+
+    @ApiModelProperty(value = "账号")
+    private Long accountId;
+
+    @ApiModelProperty(value = "当前执行查询的账号")
+    private Long ownerAccountId;
+
+    @ApiModelProperty(value = "安装包名称")
+    private String apkTitle;
+
+    @ApiModelProperty(value = "安装包版本")
+    private String apkVersion;
+
+    @ApiModelProperty(value = "开始时间")
+    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date startTime;
+
+    @ApiModelProperty(value = "结束时间")
+    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date endTime;
+
+    @ApiModelProperty(value = "第几页 起始页1")
+    private Integer pageNumber = 1;
+
+    @ApiModelProperty(value = "每页条数")
+    private Integer pageSize = 10;
+
+
+}

+ 73 - 0
hhface-middleware/src/main/java/cn/hh/hhface/bean/apk/AppApkVo.java

@@ -0,0 +1,73 @@
+package cn.hh.hhface.bean.apk;
+
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * @author shisl
+ * @package cn.hh.hhface.bean.apk
+ * @class AppApkVo
+ * @date 2023/12/12 下午5:20
+ * @description
+ */
+@Data
+public class AppApkVo implements Serializable {
+
+    private Long id;
+
+    private Long accountId;
+
+    @ApiModelProperty(value = "安装包名称")
+    private String apkTitle;
+
+    @ApiModelProperty(value = "安装包版本")
+    private String apkVersion;
+
+    @ApiModelProperty(value = "账号名称")
+    private String accountName;
+
+    @ApiModelProperty(value = "应用类型 1:K8小程序容器 AppTypeEnum")
+    private Integer appType;
+
+    @ApiModelProperty(value = "安装包的地址")
+    private String apkPath;
+
+    @ApiModelProperty(value = "备注")
+    private String remarks;
+
+    @ApiModelProperty(value = "文件md5 file表f_md5")
+    private String fileMd5;
+
+    @ApiModelProperty(value = "文件id file表f_id")
+    private Integer fileId;
+
+    @ApiModelProperty(value = "文件name file表f_name")
+    private String fileName;
+
+    @ApiModelProperty(value = "文件大小 file表f_size")
+    private Long fileSize;
+
+    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+
+    @ApiModelProperty(value = "包名")
+    private String packageName;
+
+    /**
+     * 安装包绑定的账号id [1,2,3,4,5,6]
+     */
+    private String accountIdJson;
+
+    private List<Long> accountIds;
+    private List<String> accountNames;
+
+    @ApiModelProperty(value = "是否可编辑/删除 true/false")
+    private Boolean isOwner = Boolean.FALSE;
+
+}

+ 18 - 0
hhface-middleware/src/main/java/cn/hh/hhface/bean/device/DeviceAppRestartDecryptDto.java

@@ -0,0 +1,18 @@
+package cn.hh.hhface.bean.device;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * @author shisl
+ * @package cn.hh.hhface.bean.device
+ * @class DeviceAppRestartDecryptDto
+ * @date 2024/2/28 上午11:54
+ * @description
+ */
+@Data
+public class DeviceAppRestartDecryptDto implements Serializable {
+
+    private String sn;
+}

+ 30 - 0
hhface-middleware/src/main/java/cn/hh/hhface/bean/device/DeviceBizDataDto.java

@@ -0,0 +1,30 @@
+package cn.hh.hhface.bean.device;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * @author shisl
+ * @package cn.hh.hhface.bean.log
+ * @class DeviceBizDataDto
+ * @date 2023/12/19 下午3:07
+ * @description
+ */
+@Data
+public class DeviceBizDataDto implements Serializable {
+
+    @ApiModelProperty(value = "设备sn")
+    private String sn;
+
+    @ApiModelProperty(value = "业务应用在线状态 0:离线 1:在线")
+    private Integer bizAppOnline;
+
+    @ApiModelProperty(value = "业务应用版本")
+    private String bizAppVersion;
+
+    @ApiModelProperty(value = "业务应用包名")
+    private String bizAppApkName;
+
+}

+ 24 - 0
hhface-middleware/src/main/java/cn/hh/hhface/bean/device/DeviceCloseDto.java

@@ -0,0 +1,24 @@
+package cn.hh.hhface.bean.device;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * @author shisl
+ * @package cn.hh.hhface.bean.device
+ * @class DeviceRebootDto
+ * @date 2023/12/10 下午4:35
+ * @description
+ */
+@Data
+public class DeviceCloseDto implements Serializable {
+
+    @ApiModelProperty(value = "设备编码")
+    private String sn;
+
+    @ApiModelProperty(value = "业务应用包名")
+    private String bizAppApkName;
+
+}

+ 26 - 0
hhface-middleware/src/main/java/cn/hh/hhface/bean/device/DeviceDto.java

@@ -0,0 +1,26 @@
+package cn.hh.hhface.bean.device;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * @author shisl
+ * @package cn.hh.hhface.bean.device
+ * @class DeviceDto
+ * @date 2023/12/10 下午4:35
+ * @description
+ */
+@Data
+public class DeviceDto implements Serializable {
+
+    private Long id;
+
+    @ApiModelProperty(value = "账户id")
+    private Long accountId;
+
+    @ApiModelProperty(value = "设备编码")
+    private String sn;
+
+}

+ 28 - 0
hhface-middleware/src/main/java/cn/hh/hhface/bean/device/DeviceFilePathDto.java

@@ -0,0 +1,28 @@
+package cn.hh.hhface.bean.device;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * @author shisl
+ * @package cn.hh.hhface.bean.device
+ * @class DeviceFilePathDto
+ * @date 2024/1/3 下午4:54
+ * @description
+ */
+@Data
+public class DeviceFilePathDto implements Serializable {
+
+    private Long id;
+
+    private Long accountId;
+
+    @ApiModelProperty(value = "文件路径名称")
+    private String pathTitle;
+
+    @ApiModelProperty(value = "文件路径地址")
+    private String pathUri;
+
+}

+ 27 - 0
hhface-middleware/src/main/java/cn/hh/hhface/bean/device/DeviceFilePathPageDto.java

@@ -0,0 +1,27 @@
+package cn.hh.hhface.bean.device;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * @author shisl
+ * @package cn.hh.hhface.bean.device
+ * @class DeviceFilePathDto
+ * @date 2024/1/3 下午4:54
+ * @description
+ */
+@Data
+public class DeviceFilePathPageDto implements Serializable {
+
+    @ApiModelProperty(value = "文件路径名称")
+    private String pathTitle;
+
+    @ApiModelProperty(value = "第几页 起始页1")
+    private Integer pageNumber = 1;
+
+    @ApiModelProperty(value = "每页条数")
+    private Integer pageSize = 10;
+
+}

+ 35 - 0
hhface-middleware/src/main/java/cn/hh/hhface/bean/device/DeviceFilePathVo.java

@@ -0,0 +1,35 @@
+package cn.hh.hhface.bean.device;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * @author shisl
+ * @package cn.hh.hhface.bean.device
+ * @class DeviceFilePathVo
+ * @date 2024/1/3 下午4:53
+ * @description
+ */
+@Data
+public class DeviceFilePathVo implements Serializable {
+
+    private Long id;
+
+    private Long accountId;
+
+    @ApiModelProperty(value = "文件路径名称")
+    private String pathTitle;
+
+    @ApiModelProperty(value = "文件路径地址")
+    private String pathUri;
+
+    @ApiModelProperty(value = "创建时间")
+    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+
+
+}

+ 28 - 0
hhface-middleware/src/main/java/cn/hh/hhface/bean/device/DeviceHeartTrackingDto.java

@@ -0,0 +1,28 @@
+package cn.hh.hhface.bean.device;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * @author shisl
+ * @package cn.hh.hhface.bean.device
+ * @class DeviceHeartTrackingDto
+ * @date 2024/1/2 下午12:00
+ * @description
+ */
+@Data
+public class DeviceHeartTrackingDto implements Serializable {
+
+    @ApiModelProperty(value = "设备编号")
+    private String sn;
+
+    @ApiModelProperty(value = "业务应用版本")
+    private String bizAppVersion;
+
+    @ApiModelProperty(value = "业务应用包名")
+    private String bizAppApkName;
+
+
+}

+ 19 - 0
hhface-middleware/src/main/java/cn/hh/hhface/bean/device/DeviceInfoDecryptDto.java

@@ -0,0 +1,19 @@
+package cn.hh.hhface.bean.device;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * @author shisl
+ * @package cn.hh.hhface.bean.device
+ * @class DeviceInfoDecryptDto
+ * @date 2024/2/28 上午11:54
+ * @description
+ */
+@Data
+public class DeviceInfoDecryptDto implements Serializable {
+
+    private String sn;
+
+}

+ 58 - 0
hhface-middleware/src/main/java/cn/hh/hhface/bean/device/DeviceInfoVo.java

@@ -0,0 +1,58 @@
+package cn.hh.hhface.bean.device;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * @author shisl
+ * @package cn.hh.hhface.bean.device
+ * @class DeviceInfoVo
+ * @date 2023/12/10 下午4:35
+ * @description
+ */
+@Data
+public class DeviceInfoVo implements Serializable {
+
+    @ApiModelProperty(value = "设备编码")
+    private String sn;
+
+    @ApiModelProperty(value = "IP地址")
+    private String ipAddress;
+
+    @ApiModelProperty(value = "OMC在线状态 0:离线 1:在线")
+    private Integer omcOnline;
+
+    @ApiModelProperty(value = "业务应用在线状态 0:离线 1:在线")
+    private Integer bizAppOnline;
+
+    @ApiModelProperty(value = "固件版本号")
+    private String firmwareVersion;
+
+    @ApiModelProperty(value = "系统版本")
+    private String sysVersion;
+
+    @ApiModelProperty(value = "业务应用版本")
+    private String bizAppVersion;
+
+    @ApiModelProperty(value = "业务应用包名")
+    private String bizAppApkName;
+
+    @ApiModelProperty(value = "设备型号")
+    private String deviceModel;
+
+    @ApiModelProperty(value = "ota应用版本号")
+    private String otaAppVersion;
+
+    @ApiModelProperty(value = "ota应用包名")
+    private String otaAppApkName;
+
+    @ApiModelProperty(value = "最后一次tracking心跳时间")
+    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date lastTrackingTime;
+
+
+}

+ 55 - 0
hhface-middleware/src/main/java/cn/hh/hhface/bean/device/DevicePageDto.java

@@ -0,0 +1,55 @@
+package cn.hh.hhface.bean.device;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * @author shisl
+ * @package cn.hh.hhface.bean.device
+ * @class DeviceDto
+ * @date 2023/12/10 下午4:35
+ * @description
+ */
+@Data
+public class DevicePageDto implements Serializable {
+
+    private Long id;
+
+    @ApiModelProperty(value = "账户id")
+    private Long accountId;
+
+    @ApiModelProperty(value = "设备编码")
+    private String sn;
+
+    @ApiModelProperty(value = "设备型号")
+    private String deviceModel;
+
+    @ApiModelProperty(value = "ota应用包名")
+    private String otaAppApkName;
+
+    @ApiModelProperty(value = "业务应用包名")
+    private String bizAppApkName;
+
+    @ApiModelProperty(value = "OMC在线状态 0:离线 1:在线")
+    private Integer omcOnline;
+
+    @ApiModelProperty(value = "业务应用在线状态 0:离线 1:在线")
+    private Integer bizAppOnline;
+
+    @ApiModelProperty(value = "开始时间")
+    private Date startTime;
+
+    @ApiModelProperty(value = "结束时间")
+    private Date endTime;
+
+    @ApiModelProperty(value = "第几页 起始页1")
+    private Integer pageNumber = 1;
+
+    @ApiModelProperty(value = "每页条数")
+    private Integer pageSize = 10;
+
+
+}

+ 70 - 0
hhface-middleware/src/main/java/cn/hh/hhface/bean/device/DeviceVo.java

@@ -0,0 +1,70 @@
+package cn.hh.hhface.bean.device;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * @author shisl
+ * @package cn.hh.hhface.bean.device
+ * @class DeviceVo
+ * @date 2023/12/10 下午4:35
+ * @description
+ */
+@Data
+public class DeviceVo implements Serializable {
+
+    private Long id;
+
+    @ApiModelProperty(value = "账户id")
+    private Long accountId;
+
+    @ApiModelProperty(value = "所属账号")
+    private String accountUsername;
+
+    @ApiModelProperty(value = "设备编码")
+    private String sn;
+
+    @ApiModelProperty(value = "IP地址")
+    private String ipAddress;
+
+    @ApiModelProperty(value = "OMC在线状态 0:离线 1:在线")
+    private Integer omcOnline;
+
+    @ApiModelProperty(value = "业务应用在线状态 0:离线 1:在线")
+    private Integer bizAppOnline;
+
+    @ApiModelProperty(value = "固件版本号")
+    private String firmwareVersion;
+
+    @ApiModelProperty(value = "系统版本")
+    private String sysVersion;
+
+    @ApiModelProperty(value = "业务应用版本")
+    private String bizAppVersion;
+
+    @ApiModelProperty(value = "业务应用包名")
+    private String bizAppApkName;
+
+    @ApiModelProperty(value = "设备型号")
+    private String deviceModel;
+
+    @ApiModelProperty(value = "ota应用版本号")
+    private String otaAppVersion;
+
+    @ApiModelProperty(value = "ota应用包名")
+    private String otaAppApkName;
+
+    @ApiModelProperty(value = "创建时间")
+    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+
+    @ApiModelProperty(value = "最后一次tracking心跳时间")
+    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date lastTrackingTime;
+
+
+}

+ 28 - 0
hhface-middleware/src/main/java/cn/hh/hhface/bean/device/DownloadLogDto.java

@@ -0,0 +1,28 @@
+package cn.hh.hhface.bean.device;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * @author shisl
+ * @package cn.hh.hhface.bean.device
+ * @class DownloadLogDto
+ * @date 2023/12/22 下午3:54
+ * @description
+ */
+@Data
+public class DownloadLogDto  implements Serializable {
+
+    @ApiModelProperty(value = "设备sn编号", required = true)
+    private String sn;
+
+    @ApiModelProperty(value = "日志类型 Logcat/Error/Crash", required = true)
+    private String logType;
+
+    @ApiModelProperty(value = "日志列表")
+    private List<String> logUris;
+
+}

+ 30 - 0
hhface-middleware/src/main/java/cn/hh/hhface/bean/device/FetchLogDto.java

@@ -0,0 +1,30 @@
+package cn.hh.hhface.bean.device;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * @author shisl
+ * @package cn.hh.hhface.bean.device
+ * @class FetchLogDto
+ * @date 2023/12/10 下午5:38
+ * @description
+ */
+@Data
+public class FetchLogDto implements Serializable {
+
+    @ApiModelProperty(value = "设备sn编号", required = true)
+    private String sn;
+
+    @ApiModelProperty(value = "日志类型 Logcat/Error/Crash", required = false)
+    private String logType;
+
+    @ApiModelProperty(value = "文件路径地址", required = true)
+    private String pathUri;
+
+//    @ApiModelProperty(value = "日志列表")
+//    private List<String> logUris;
+
+}

+ 21 - 0
hhface-middleware/src/main/java/cn/hh/hhface/bean/device/SendZfb5YearMsgDto.java

@@ -0,0 +1,21 @@
+package cn.hh.hhface.bean.device;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * @author shisl
+ * @package cn.hh.hhface.bean.device
+ * @class SendZfb5YearMsgDto
+ * @date 2024/1/12 上午11:32
+ * @description
+ */
+@Data
+public class SendZfb5YearMsgDto implements Serializable {
+
+    private String days;
+    private String sn;
+
+
+}

+ 31 - 0
hhface-middleware/src/main/java/cn/hh/hhface/bean/device/Sm4EncryptDto.java

@@ -0,0 +1,31 @@
+package cn.hh.hhface.bean.device;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * @author shisl
+ * @package cn.hh.hhface.bean.device
+ * @class Sm4EncryptDto
+ * @date 2024/2/28 上午11:02
+ * @description
+ */
+@Data
+public class Sm4EncryptDto implements Serializable {
+
+    @ApiModelProperty("应用id唯一标识")
+    private String appId;
+
+    @ApiModelProperty("加密内容 {\"sn\":\"\"}")
+    private String bizContent;
+
+    @ApiModelProperty("时间戳 毫秒")
+    private Long reqTimestamp;
+
+    @ApiModelProperty(value = "签名md5(appKey+appSecret+timestamp) 小写32位")
+    private String sign;
+
+
+}

+ 25 - 0
hhface-middleware/src/main/java/cn/hh/hhface/bean/heart/HeartBeatBase.java

@@ -0,0 +1,25 @@
+package cn.hh.hhface.bean.heart;
+
+import com.alibaba.fastjson.annotation.JSONField;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * @author shisl
+ * @package cn.hh.hhface.bean.heart
+ * @class HeartBeatBase
+ * @date 2024/7/25 下午1:19
+ * @description
+ */
+@Data
+public class HeartBeatBase implements Serializable {
+
+    //设备编号
+    @JsonProperty(value = "SN")
+    @JSONField(name = "SN")
+    private String SN;
+
+
+}

+ 16 - 0
hhface-middleware/src/main/java/cn/hh/hhface/bean/heart/OfflineStatusPushReq.java

@@ -0,0 +1,16 @@
+package cn.hh.hhface.bean.heart;
+
+import lombok.Data;
+
+/**
+ * @author shisl
+ * @package cn.hh.hhface.bean.heart
+ * @class OfflineStatusPushReq
+ * @date 2024/7/25 下午1:20
+ * @description
+ */
+@Data
+public class OfflineStatusPushReq extends HeartBeatBase {
+
+
+}

+ 41 - 0
hhface-middleware/src/main/java/cn/hh/hhface/bean/heart/OnlineStatusPushDataReq.java

@@ -0,0 +1,41 @@
+package cn.hh.hhface.bean.heart;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * @author shisl
+ * @package cn.hh.hhface.bean.heart
+ * @class OnlineStatusPushDataReq
+ * @date 2024/7/25 下午1:21
+ * @description
+ */
+@Data
+public class OnlineStatusPushDataReq implements Serializable {
+
+    /**
+     * "companyName": "",
+     *         "devIP": "10.139.46.63",
+     *         "devName": "6019",
+     *         "doorStatus": false,
+     *         "location": "",
+     *         "orgid": "888888",
+     *         "qrCodePlatform": "",
+     *         "swipeCardCount": 30454,
+     *         "userCount": 1226,
+     *         "version": "3.1.84.230224.HH.84"
+     */
+    private String companyName;
+    private String devIP;
+    private String doorStatus;
+    private String location;
+    private String orgid;
+    private String qrCodePlatform;
+    private String swipeCardCount;
+    private String userCount;
+    private String version;
+
+
+
+}

+ 38 - 0
hhface-middleware/src/main/java/cn/hh/hhface/bean/heart/OnlineStatusPushReq.java

@@ -0,0 +1,38 @@
+package cn.hh.hhface.bean.heart;
+
+import lombok.Data;
+
+/**
+ * @author shisl
+ * @package cn.hh.hhface.bean.heart
+ * @class OnlineStatusPushReq
+ * @date 2024/7/25 下午1:20
+ * @description
+ */
+@Data
+public class OnlineStatusPushReq extends HeartBeatBase {
+
+    /**
+     * {
+     *         "SN": "225F002004600529",
+     *             "data": {
+     *         "companyName": "",
+     *                 "devIP": "10.139.46.63",
+     *                 "devName": "6019",
+     *                 "doorStatus": false,
+     *                 "location": "",
+     *                 "orgid": "888888",
+     *                 "qrCodePlatform": "",
+     *                 "swipeCardCount": 30454,
+     *                 "userCount": 1226,
+     *                 "version": "3.1.84.230224.HH.84"
+     *     },
+     *         "orgid": "888888",
+     *             "type": "baseInfo"
+     *     }
+     */
+    private String orgid="888888";
+    private String type = "baseInfo";
+    private OnlineStatusPushDataReq data;
+
+}

+ 52 - 0
hhface-middleware/src/main/java/cn/hh/hhface/bean/isssue/WebsocketMsgReq.java

@@ -0,0 +1,52 @@
+package cn.hh.hhface.bean.isssue;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * @author shisl
+ * @package cn.hh.hhface.bean.isssue
+ * @class WebsocketMsgReq
+ * @date 2024/9/14 上午10:46
+ * @description
+ */
+@Data
+public class WebsocketMsgReq implements Serializable {
+
+    /**
+     * 设备编号 客户端必要参数
+     */
+    private String sn;
+    /**
+     * 消息来源 <br />
+     * TQ:塘栖<br />
+     * TQSERVER:塘栖服务端<br />
+     * HHFACE:HHFACE客户端
+     */
+    private String source;
+    /**
+     * 业务操作类型:<br />
+     * ISSUE_ALL: 全量下发 <br />
+     * ISSUE_INCREMENT:增量下发<br />
+     * FACE_DELETED:人脸删除<br />
+     * FACE_DELETED_ALL:人脸全部删除<br />
+     * ISSUE_CALLBACK:人员下发回调<br />
+     * FACE_DELETED_ALL_CALLBACK:人脸全部删除回调<br />
+     * FACE_DELETED_CALLBACK:人员删除毁掉<br />
+     * ISSUE_DEVICE_USER_REPORT:设备人员数据上报
+     * ISSUE_DEVICE_FETCH:获取未完成下发列表<br />
+     */
+    private String op;
+    /**
+     * 消息内容
+     */
+    private String msg;
+
+    /**
+     * 数据
+     */
+    private Object data;
+
+
+}

+ 37 - 0
hhface-middleware/src/main/java/cn/hh/hhface/bean/log/AppLogDto.java

@@ -0,0 +1,37 @@
+package cn.hh.hhface.bean.log;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * @author shisl
+ * @package cn.hh.hhface.bean.log
+ * @class AppLogDto
+ * @date 2023/12/19 上午11:16
+ * @description
+ */
+@Data
+public class AppLogDto implements Serializable {
+
+    @ApiModelProperty(value = "验证token")
+    private String token;
+
+    @ApiModelProperty(value = "设备sn")
+    private String sn;
+
+    @ApiModelProperty(value = "日志类型Logcat/Error/Crash")
+    private String logType;
+
+    @ApiModelProperty(value = "日志标签")
+    private String logTag;
+
+    @ApiModelProperty(value = "日志内容")
+    private String logContent;
+
+    @ApiModelProperty(value = "上报时间")
+    private Date pushTime;
+
+}

+ 43 - 0
hhface-middleware/src/main/java/cn/hh/hhface/bean/log/AppLogPageDto.java

@@ -0,0 +1,43 @@
+package cn.hh.hhface.bean.log;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * @author shisl
+ * @package cn.hanghui.outapi.platform.tenant.entity.dto
+ * @class AppLogDto
+ * @date 2023/12/19 上午11:16
+ * @description
+ */
+@Data
+public class AppLogPageDto implements Serializable {
+
+    private Long accountId;
+
+    @ApiModelProperty(value = "设备sn")
+    private String sn;
+
+    @ApiModelProperty(value = "日志类型Logcat/Error/Crash")
+    private String logType;
+
+    @ApiModelProperty(value = "日志标签")
+    private String logTag;
+
+    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date startTime;
+
+    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date endTime;
+
+    @ApiModelProperty(value = "第几页 起始页1")
+    private Integer pageNumber = 1;
+
+    @ApiModelProperty(value = "每页条数")
+    private Integer pageSize = 10;
+
+}

+ 35 - 0
hhface-middleware/src/main/java/cn/hh/hhface/bean/log/AppLogReportDto.java

@@ -0,0 +1,35 @@
+package cn.hh.hhface.bean.log;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * @author shisl
+ * @package cn.hh.hhface.bean.log
+ * @class AppLogDto
+ * @date 2023/12/19 下午3:07
+ * @description
+ */
+@Data
+public class AppLogReportDto implements Serializable {
+
+    @ApiModelProperty(value = "设备sn")
+    private String sn;
+
+    @ApiModelProperty(value = "日志类型Logcat/Error/Crash")
+    private String logType;
+
+    @ApiModelProperty(value = "日志标签")
+    private String logTag;
+
+    @ApiModelProperty(value = "日志内容")
+    private String logContent;
+
+    /**
+     * 回传发送者
+     */
+    private String sender;
+
+}

+ 41 - 0
hhface-middleware/src/main/java/cn/hh/hhface/bean/log/AppLogVo.java

@@ -0,0 +1,41 @@
+package cn.hh.hhface.bean.log;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * @author shisl
+ * @package cn.hh.hhface.bean.log
+ * @class AppLogDto
+ * @date 2023/12/19 上午11:16
+ * @description
+ */
+@Data
+public class AppLogVo implements Serializable {
+
+    private Long id;
+
+    @ApiModelProperty(value = "设备sn")
+    private String sn;
+
+    @ApiModelProperty(value = "日志类型Logcat/Error/Crash")
+    private String logType;
+
+    @ApiModelProperty(value = "日志标签")
+    private String logTag;
+
+    @ApiModelProperty(value = "日志内容")
+    private String logContent;
+
+    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss.SSS")
+    private Date pushTime;
+
+    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss.SSS")
+    private Date createTime;
+
+
+}

+ 32 - 0
hhface-middleware/src/main/java/cn/hh/hhface/bean/task/AppApkDeviceTaskDto.java

@@ -0,0 +1,32 @@
+package cn.hh.hhface.bean.task;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * @author shisl
+ * @package cn.hh.hhface.bean.task
+ * @class AppApkDeviceTaskDto
+ * @date 2023/12/13 下午1:47
+ * @description
+ */
+@Data
+public class AppApkDeviceTaskDto {
+
+    @ApiModelProperty(value = "账号id")
+    private Long accountId;
+
+    @ApiModelProperty(value = "安装包id")
+    private Long appApkId;
+
+    @ApiModelProperty(value = "设备号集合")
+    private List<String> sns;
+
+    @ApiModelProperty(value = "升级类型 1:强制 / 2:静默 UpgradeTypeEnum ")
+    private Integer upgradeType;
+
+
+
+}

+ 22 - 0
hhface-middleware/src/main/java/cn/hh/hhface/bean/task/AppApkDeviceTaskErrorVo.java

@@ -0,0 +1,22 @@
+package cn.hh.hhface.bean.task;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+/**
+ * @author shisl
+ * @package cn.hh.hhface.bean.task
+ * @class AppApkDeviceTaskErrorVo
+ * @date 2023/12/13 下午3:09
+ * @description
+ */
+@Data
+public class AppApkDeviceTaskErrorVo {
+
+    @ApiModelProperty(value = "设备编号")
+    private String sn;
+
+    @ApiModelProperty(value = "错误信息")
+    private String errorMsg;
+
+}

+ 50 - 0
hhface-middleware/src/main/java/cn/hh/hhface/bean/task/AppApkDeviceTaskPageDto.java

@@ -0,0 +1,50 @@
+package cn.hh.hhface.bean.task;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * @author shisl
+ * @package cn.hh.hhface.bean.task
+ * @class AppApkDeviceTaskPageDto
+ * @date 2023/12/13 下午1:47
+ * @description
+ */
+@Data
+public class AppApkDeviceTaskPageDto {
+
+    @ApiModelProperty(value = "账号id")
+    private Long accountId;
+
+    @ApiModelProperty(value = "设备号")
+    private String sn;
+
+    @ApiModelProperty(value = "安装包名称")
+    private String apkTitle;
+
+    @ApiModelProperty(value = "设备状态 0:离线 1:在线")
+    private Integer deviceOnline;
+
+    @ApiModelProperty(value = "升级类型 1:强制 / 2:静默 UpgradeTypeEnum")
+    private Integer upgradeType;
+
+    @ApiModelProperty(value = "安装任务状态 0:等待中(设备离线 可取消) 1:下载中(可取消) 2:等待安装(静默升级 可取消) 3:安装中 4:安装成功 5:安装失败  6:已取消")
+    private Integer status;
+
+    @ApiModelProperty(value = "开始时间")
+    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date startTime;
+
+    @ApiModelProperty(value = "结束时间")
+    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date endTime;
+
+    @ApiModelProperty(value = "第几页 起始页1")
+    private Integer pageNumber = 1;
+
+    @ApiModelProperty(value = "每页条数")
+    private Integer pageSize = 10;
+}

+ 22 - 0
hhface-middleware/src/main/java/cn/hh/hhface/bean/task/AppApkDeviceTaskReq.java

@@ -0,0 +1,22 @@
+package cn.hh.hhface.bean.task;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+/**
+ * @author shisl
+ * @package cn.hh.hhface.bean.task
+ * @class AppApkDeviceTaskReq
+ * @date 2023/12/13 下午1:48
+ * @description
+ */
+@Data
+public class AppApkDeviceTaskReq {
+
+    @ApiModelProperty(value = "授权token")
+    private String token;
+
+    @ApiModelProperty(value = "设备编号")
+    private String sn;
+
+}

+ 73 - 0
hhface-middleware/src/main/java/cn/hh/hhface/bean/task/AppApkDeviceTaskVo.java

@@ -0,0 +1,73 @@
+package cn.hh.hhface.bean.task;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * @author shisl
+ * @package cn.hh.hhface.bean.task
+ * @class AppApkDeviceTaskVo
+ * @date 2023/12/13 下午1:48
+ * @description
+ */
+@Data
+public class AppApkDeviceTaskVo {
+
+    private Long id;
+
+    @ApiModelProperty(value = "账号id")
+    private Long accountId;
+
+    @ApiModelProperty(value = "账号名称")
+    private String accountName;
+
+    @ApiModelProperty(value = "设备号")
+    private String sn;
+
+    @ApiModelProperty(value = "设备状态 0:离线 1:在线")
+    private Integer deviceOnline;
+
+    @ApiModelProperty(value = "安装包id")
+    private Long appApkId;
+
+    @ApiModelProperty(value = "安装包名称")
+    private String apkTitle;
+
+    @ApiModelProperty(value = "安装包版本")
+    private String apkVersion;
+
+    @ApiModelProperty(value = "安装包的地址")
+    private String apkPath;
+
+    @ApiModelProperty(value = "应用类型 1:K8小程序容器 AppTypeEnum")
+    private Integer appType;
+
+    @ApiModelProperty(value = "升级类型 1:强制 / 2:静默 UpgradeTypeEnum")
+    private Integer upgradeType;
+
+    /**
+     * TaskStatusEnum
+     */
+    @ApiModelProperty(value = "安装任务状态 0:等待中(设备离线 可取消) 1:下载中(可取消) 2:等待安装(静默升级 可取消) 3:安装中 4:安装成功 5:安装失败  6:已取消")
+    private Integer status;
+
+    @ApiModelProperty(value = "设备请求时间")
+    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date requestTime;
+
+    @ApiModelProperty(value = "设备安装完成时间")
+    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date installTime;
+
+    @ApiModelProperty(value = "创建时间")
+    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+
+    @ApiModelProperty(value = "包名")
+    private String packageName;
+
+
+}

+ 39 - 0
hhface-middleware/src/main/java/cn/hh/hhface/bean/task/DeviceTaskStatusVo.java

@@ -0,0 +1,39 @@
+package cn.hh.hhface.bean.task;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * @author shisl
+ * @package cn.hh.hhface.bean.task
+ * @class DeviceTaskStatusVo
+ * @date 2023/12/13 下午1:48
+ * @description
+ */
+@Data
+public class DeviceTaskStatusVo implements Serializable {
+
+    private String sn;
+
+    private Long taskId;
+
+    /**
+     * 安装任务状态 0:等待中(设备离线 可取消) 1:下载中(可取消) 2:等待安装(静默升级 可取消) 3:安装中 4:安装成功 5:安装失败  6:已取消
+     * TaskStatusEnum
+     */
+    private Integer status;
+
+    /**
+     * 回传发送者
+     */
+    private String sender;
+
+    /**
+     * 下载进度0-100
+     * TaskStatusEnum
+     */
+    private Integer downloadPercent = 0;
+
+
+}

+ 24 - 0
hhface-middleware/src/main/java/cn/hh/hhface/bean/websocket/WebsocketSenderDto.java

@@ -0,0 +1,24 @@
+package cn.hh.hhface.bean.websocket;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * @author shisl
+ * @package cn.hh.hhface.bean.websocket
+ * @class WebsocketSenderDto
+ * @date 2023/12/27 下午3:20
+ * @description
+ */
+@Data
+public class WebsocketSenderDto implements Serializable {
+
+    private String sn;
+
+    /**
+     * 回传发送者
+     */
+    private String sender;
+
+}

+ 17 - 0
hhface-middleware/src/main/java/cn/hh/hhface/config/Authority.java

@@ -0,0 +1,17 @@
+package cn.hh.hhface.config;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target({ElementType.TYPE, ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Authority {
+    //
+    boolean required() default true;
+
+    int[] value() default {0};
+
+
+}

+ 47 - 0
hhface-middleware/src/main/java/cn/hh/hhface/config/NioWebSocketChannelInitializer.java

@@ -0,0 +1,47 @@
+package cn.hh.hhface.config;
+
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.socket.SocketChannel;
+import io.netty.handler.codec.http.HttpObjectAggregator;
+import io.netty.handler.codec.http.HttpServerCodec;
+import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.handler.codec.string.StringEncoder;
+import io.netty.handler.stream.ChunkedWriteHandler;
+import io.netty.handler.timeout.ReadTimeoutHandler;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author shisl
+ * @package cn.hh.hhface.config
+ * @class NioWebSocketChannelInitializer
+ * @date 2023/12/10 下午6:05
+ * @description WebSocket通道连接初始化
+ */
+@Component
+public class NioWebSocketChannelInitializer extends ChannelInitializer<SocketChannel> {
+
+    @Autowired
+    private WebSocketProperties webSocketProperties;
+    @Autowired
+    private NioWebSocketHandler nioWebSocketHandler;
+
+    @Override
+    protected void initChannel(SocketChannel socketChannel) {
+        socketChannel.pipeline()
+                .addLast(new HttpServerCodec()) //用于解析HTTP请求和响应
+                .addLast(new ChunkedWriteHandler())
+                .addLast(new HttpObjectAggregator(8192)) //用于将HTTP消息的多个部分聚合成一个完整的HTTP消息。
+                .addLast(nioWebSocketHandler) //WebSocket连接数据接收处理类
+//                .addLast(new StringDecoder())
+//                .addLast(new StringEncoder())
+                //设置2min的超时时间,如果某个通道5min内未发送信号,则抛出异常删除当前通道
+                .addLast(new ReadTimeoutHandler(2, TimeUnit.MINUTES))
+                //可以对整个WebSocket通信进行初始化(当Http请求中有升级为WebSocket的请求时),以及握手处理
+                .addLast(new WebSocketServerProtocolHandler(webSocketProperties.getPath(), null, true, 2147483647));
+    }
+}
+

+ 71 - 0
hhface-middleware/src/main/java/cn/hh/hhface/config/NioWebSocketChannelPool.java

@@ -0,0 +1,71 @@
+package cn.hh.hhface.config;
+
+import io.netty.channel.Channel;
+import io.netty.channel.group.ChannelGroup;
+import io.netty.channel.group.DefaultChannelGroup;
+import io.netty.util.concurrent.GlobalEventExecutor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.util.Iterator;
+
+/**
+ * @author shisl
+ * @package cn.hh.hhface.config
+ * @class NioWebSocketChannelPool
+ * @date 2023/12/10 下午6:00
+ * @description WebSocket连接通道池
+ */
+@Slf4j
+@Component
+public class NioWebSocketChannelPool {
+
+    private final ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
+
+    /**
+     * 新增一个客户端通道
+     *
+     * @param channel
+     */
+    public void addChannel(Channel channel) {
+        channels.add(channel);
+    }
+
+    /**
+     * 移除一个客户端连接通道
+     *
+     * @param channel
+     */
+    public void removeChannel(Channel channel) {
+        channels.remove(channel);
+    }
+
+    /**
+     * 获取指定通道
+     *
+     * @param channelId
+     * @return
+     */
+    public Channel findChannel(String channelId) {
+        Iterator var5 = channels.iterator();
+        while (var5.hasNext()) {
+            Channel c = (Channel) var5.next();
+            if (c.id().asShortText().equals(channelId)) {
+                return c;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 获取当前连接数
+     *
+     * @return
+     */
+    public int getConnectCount() {
+        return channels.size();
+    }
+
+
+}
+

+ 309 - 0
hhface-middleware/src/main/java/cn/hh/hhface/config/NioWebSocketHandler.java

@@ -0,0 +1,309 @@
+package cn.hh.hhface.config;
+
+import cn.hh.common.constants.CommonConstants;
+import cn.hh.common.restful.BusinessException;
+import cn.hh.common.utils.Preconditions;
+import cn.hh.common.utils.RequestUriUtils;
+import cn.hh.hhface.service.WebsocketService;
+import cn.hutool.crypto.digest.DigestUtil;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import io.netty.channel.ChannelHandler.Sharable;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.SimpleChannelInboundHandler;
+import io.netty.handler.codec.http.FullHttpRequest;
+import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame;
+import io.netty.handler.codec.http.websocketx.PingWebSocketFrame;
+import io.netty.handler.codec.http.websocketx.PongWebSocketFrame;
+import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
+import io.netty.handler.codec.http.websocketx.WebSocketFrame;
+import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker;
+import io.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+/**
+ * @author shisl
+ * @package cn.hh.hhface.config
+ * @class NioWebSocketHandler
+ * @date 2023/12/10 下午6:03
+ * @description WebSocket连接数据接收处理类
+ */
+@Slf4j
+@Sharable
+@Component
+public class NioWebSocketHandler extends SimpleChannelInboundHandler<WebSocketFrame> {
+
+    @Autowired
+    private NioWebSocketChannelPool webSocketChannelPool;
+    @Autowired
+    private WebSocketProperties webSocketProperties;
+    @Resource
+    private RedisTemplate<String, String> redisTemplate;
+    @Resource
+    private WebsocketService websocketService;
+
+    @Override
+    public void channelActive(ChannelHandlerContext ctx) throws Exception {
+        log.info("客户端连接:{}", ctx.channel().id());
+        webSocketChannelPool.addChannel(ctx.channel());
+        super.channelActive(ctx);
+    }
+
+    @Override
+    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
+        log.info("客户端断开连接:{}", ctx.channel().id());
+        webSocketChannelPool.removeChannel(ctx.channel());
+        super.channelInactive(ctx);
+    }
+
+    @Override
+    public void channelReadComplete(ChannelHandlerContext ctx) {
+        ctx.channel().flush();
+    }
+
+    @Override
+    protected void channelRead0(ChannelHandlerContext ctx, WebSocketFrame frame) {
+        // 根据请求数据类型进行分发处理
+        if (frame instanceof PingWebSocketFrame) {
+            pingWebSocketFrameHandler(ctx, (PingWebSocketFrame) frame);
+        } else if (frame instanceof TextWebSocketFrame) {
+            textWebSocketFrameHandler(ctx, (TextWebSocketFrame) frame);
+        } else if (frame instanceof CloseWebSocketFrame) {
+            closeWebSocketFrameHandler(ctx, (CloseWebSocketFrame) frame);
+        }
+    }
+
+    @Override
+    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
+//        log.info("客户端请求数据类型:{}", msg.getClass());
+        if (msg instanceof FullHttpRequest) {
+            fullHttpRequestHandler(ctx, (FullHttpRequest) msg);
+        }
+        super.channelRead(ctx, msg);
+    }
+
+    @Override
+    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
+        log.error("socket连接异常:{}", cause.getMessage());
+        Map<String, Object> data = new HashMap<>();
+        data.put("code", 400);
+        data.put("msg", cause.getMessage());
+        ctx.channel().writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(data)));
+        ctx.channel().close();
+    }
+
+    /**
+     * 处理连接请求,客户端WebSocket发送握手包时会执行这一次请求
+     *
+     * @param ctx
+     * @param request
+     */
+    private void fullHttpRequestHandler(ChannelHandlerContext ctx, FullHttpRequest request) {
+        String uri = request.uri();
+        Map<String, String> params = RequestUriUtils.getParams(uri);
+        log.info("客户端请求参数:{}", params);
+        String errorMsg = null;
+        String token = params.get(CommonConstants.WEBSOCKET_CONNECT_PC);
+        if (Preconditions.isBlank(params.get(CommonConstants.WEBSOCKET_CONNECT_APP)) && Preconditions.isBlank(token)) {
+            errorMsg = "参数有误,无权访问websocket资源";
+        }
+
+        //消息来源
+        String source = params.get(CommonConstants.WEBSOCKET_CONNECT_SOURCE);
+        if (Preconditions.isNotBlank(source) && !Arrays.asList("TQ", "TQSERVER", "HHFACE").contains(source)) {
+            errorMsg = "source参数有误,无权访问websocket资源";
+        }
+
+        // 判断请求路径是否跟配置中的一致
+        if (Preconditions.isBlank(errorMsg) && webSocketProperties.getPath().equals(RequestUriUtils.getBasePath(uri))) {
+            // 因为有可能携带了参数,导致客户端一直无法返回握手包,因此在校验通过后,重置请求路径
+            request.setUri(webSocketProperties.getPath());
+            //放入缓存
+            String sn = params.get(CommonConstants.WEBSOCKET_CONNECT_APP);
+            String channelId = ctx.channel().id().asShortText();
+            //APP客户端
+            if (Preconditions.isNotBlank(sn)) {
+                //放入缓存 保存sn和通道的关系
+                String cacheChannelKey = Preconditions.isNotBlank(source) ? String.format(CommonConstants.WEBSOCKET_SN_SOURCE_KEY, source, sn) : String.format(CommonConstants.WEBSOCKET_SN_KEY, sn);
+                redisTemplate.opsForValue().set(cacheChannelKey, channelId, 365, TimeUnit.DAYS);
+            }
+
+            //PC端
+            if (Preconditions.isNotBlank(token)) {
+                String wpKey = Preconditions.isNotBlank(source) ? CommonConstants.WEBSOCKET_PC_SOURCE_KEY : CommonConstants.WEBSOCKET_PC_KEY;
+                String wptKey = Preconditions.isNotBlank(source) ? String.format(CommonConstants.WEBSOCKET_PC_TOKEN_SOURCE_KEY, source, DigestUtil.md5Hex(token)) : String.format(CommonConstants.WEBSOCKET_PC_TOKEN_KEY, DigestUtil.md5Hex(token));
+                //放入缓存
+                handleTokenCache(channelId, wpKey, wptKey);
+            }
+        } else {
+            log.error(errorMsg);
+            //先建立临时连接
+            WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(uri, null, false);
+            WebSocketServerHandshaker handshaker = wsFactory.newHandshaker(request);
+            if (handshaker == null) {
+                WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());
+            } else {
+                handshaker.handshake(ctx.channel(), request);
+                ctx.channel().writeAndFlush(new TextWebSocketFrame("临时连接建立成功"));
+            }
+            throw new BusinessException(errorMsg);
+        }
+    }
+
+    //PC放入缓存
+    private void handleTokenCache(String channelId, String wpKey, String wptKey) {
+        List<String> channels = new ArrayList<>();
+        //放入缓存
+        if (redisTemplate.hasKey(wpKey)) {
+            String s = redisTemplate.opsForValue().get(wpKey);
+            List<String> redisChannels = JSONObject.parseArray(s, String.class);
+            if (!redisChannels.contains(channelId)) {
+                channels.add(channelId);
+            }
+            //处理webSocketChannelPool中不存在的channel
+            List<String> filterChannels = redisChannels.stream().filter(item -> Preconditions.isNotBlank(webSocketChannelPool.findChannel(item))).collect(Collectors.toList());
+            if (Preconditions.isNotBlank(filterChannels)) {
+                channels.addAll(filterChannels);
+            }
+
+            if (Preconditions.isNotBlank(channels)) {
+                redisTemplate.opsForValue().set(wpKey, JSONObject.toJSONString(channels), 15, TimeUnit.DAYS);
+            }
+        } else {
+            channels.add(channelId);
+            redisTemplate.opsForValue().set(wpKey, JSONObject.toJSONString(channels), 15, TimeUnit.DAYS);
+        }
+        //保存token和通道对应关系
+        redisTemplate.opsForValue().set(wptKey, channelId, 7, TimeUnit.DAYS);
+    }
+
+    /**
+     * 客户端发送断开请求处理
+     *
+     * @param ctx
+     * @param frame
+     */
+    private void closeWebSocketFrameHandler(ChannelHandlerContext ctx, CloseWebSocketFrame frame) {
+        log.info("客户端发送断开请求:{}", frame.reasonText());
+        String channelId = ctx.channel().id().asShortText();
+        if (redisTemplate.hasKey(CommonConstants.WEBSOCKET_PC_KEY)) {
+            String s = redisTemplate.opsForValue().get(CommonConstants.WEBSOCKET_PC_KEY);
+            List<String> channels = JSONObject.parseArray(s, String.class);
+            channels = channels.stream().filter(item -> !channelId.equals(item)).collect(Collectors.toList());
+            redisTemplate.opsForValue().set(CommonConstants.WEBSOCKET_PC_KEY, JSONObject.toJSONString(channels), 15, TimeUnit.DAYS);
+        }
+        if (redisTemplate.hasKey(CommonConstants.WEBSOCKET_PC_SOURCE_KEY)) {
+            String s = redisTemplate.opsForValue().get(CommonConstants.WEBSOCKET_PC_SOURCE_KEY);
+            List<String> channels = JSONObject.parseArray(s, String.class);
+            channels = channels.stream().filter(item -> !channelId.equals(item)).collect(Collectors.toList());
+            redisTemplate.opsForValue().set(CommonConstants.WEBSOCKET_PC_SOURCE_KEY, JSONObject.toJSONString(channels), 15, TimeUnit.DAYS);
+        }
+        ctx.close();
+    }
+
+    /**
+     * 创建连接之后,客户端发送的消息都会在这里处理
+     *
+     * @param ctx
+     * @param frame
+     */
+    private void textWebSocketFrameHandler(ChannelHandlerContext ctx, TextWebSocketFrame frame) {
+        // 客户端发送过来的内容不进行业务处理,原样返回
+        log.info("客户端发送的消息内容:{}", frame.text());
+        try {
+            JSONObject object = JSONObject.parseObject(frame.text());
+            //来源  PC / APP
+            String source = object.getString(CommonConstants.WEBSOCKET_SOURCE);
+            //TODO 处理pc端发送过来的websocket消息
+            if (CommonConstants.WEBSOCKET_SOURCE_PC.equals(source)) {
+                // TODO 暂时没有直接使用
+                ctx.channel().writeAndFlush(frame.retain());
+            }
+            //TODO 处理APP端发送过来的websocket消息
+            if (CommonConstants.WEBSOCKET_SOURCE_APP.equals(source)) {
+                handleAppWebsocketMsg(object, frame.text());
+            }
+
+            // TODO 来源--塘栖PC端
+            if (CommonConstants.WEBSOCKET_SOURCE_TQ_PC.equals(source)) {
+                ctx.channel().writeAndFlush(frame.retain());
+            }
+            //TODO 来源--塘栖服务端
+            if (CommonConstants.WEBSOCKET_SOURCE_TQ_SERVER.equals(source)) {
+                handleTqServerWebsocketMsg(object, frame.text());
+            }
+            //TODO 来源--HHFACE端
+            if (CommonConstants.WEBSOCKET_SOURCE_HHFACE_APP.equals(source)) {
+                handleHhFaceAppWebsocketMsg(object, frame.text());
+            }
+        } catch (Exception e) {
+            ctx.channel().writeAndFlush(frame.retain());
+        }
+    }
+
+    //TODO 来源--HHFACE端
+    private void handleHhFaceAppWebsocketMsg(JSONObject object, String text) {
+        String op = object.getString(CommonConstants.WEBSOCKET_OP);
+        if (Preconditions.isNotBlank(op)) {
+            if (op.equals(CommonConstants.WEBSOCKET_OP_TRACKING)) { //HHFACE心跳包
+                handleHHFaceHeartTracking(object, text, op);
+                return;
+            }
+            websocketService.handleHhFaceAppWebsocketOp(object, text, op);
+        } else {
+            //没有操作类型 全部广播📢
+            websocketService.sendHhFaceAppWebsocketMsg2AllTq(text);
+        }
+    }
+
+    //HHFACE心跳包
+    private void handleHHFaceHeartTracking(JSONObject object, String text, String op) {
+        object.put("code", 200);
+        object.put("msg", "success");
+        object.put("source", CommonConstants.WEBSOCKET_SOURCE_TQ_SERVER);
+        websocketService.handleTqServerWebsocketOp(object, text, op);
+    }
+
+    //TODO 来源--塘栖服务端
+    private void handleTqServerWebsocketMsg(JSONObject object, String text) {
+        String op = object.getString(CommonConstants.WEBSOCKET_OP);
+        if (Preconditions.isNotBlank(op)) {
+            websocketService.handleTqServerWebsocketOp(object, text, op);
+        }
+    }
+
+    private void handleAppWebsocketMsg(JSONObject object, String text) {
+        //PC操作 close关闭应用  reboot重启设备  upgrade升级
+        //APP操作 heart心跳在离线 downloadProcessing下载进度
+        String op = object.getString(CommonConstants.WEBSOCKET_OP);
+        if (Preconditions.isNotBlank(op)) {
+            websocketService.handleAppWebsocketOp(object, text, op);
+        } else {
+            //没有操作类型 全部广播📢
+            websocketService.sendAppWebsocketMsg2AllPc(text);
+        }
+    }
+
+    /**
+     * 处理客户端心跳包
+     *
+     * @param ctx
+     * @param frame
+     */
+    private void pingWebSocketFrameHandler(ChannelHandlerContext ctx, PingWebSocketFrame frame) {
+        log.info("客户端心跳:{}", frame.content());
+        ctx.channel().writeAndFlush(new PongWebSocketFrame(frame.content().retain()));
+    }
+}

+ 75 - 0
hhface-middleware/src/main/java/cn/hh/hhface/config/NioWebSocketServer.java

@@ -0,0 +1,75 @@
+package cn.hh.hhface.config;
+
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelOption;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.nio.NioServerSocketChannel;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+/**
+ * @author shisl
+ * @package cn.hh.hhface.config
+ * @class NioWebSocketServer
+ * @date 2023/12/10 下午6:06
+ * @description Netty服务端
+ */
+@Slf4j
+@Component
+public class NioWebSocketServer implements InitializingBean, DisposableBean {
+
+    @Autowired
+    private WebSocketProperties webSocketProperties;
+    @Autowired
+    private NioWebSocketChannelInitializer webSocketChannelInitializer;
+
+    private EventLoopGroup bossGroup;
+    private EventLoopGroup workGroup;
+    private ChannelFuture channelFuture;
+
+    @Override
+    public void afterPropertiesSet() throws Exception {
+        try {
+            bossGroup = new NioEventLoopGroup();
+            workGroup = new NioEventLoopGroup();
+
+            ServerBootstrap serverBootstrap = new ServerBootstrap();
+            serverBootstrap.option(ChannelOption.SO_BACKLOG, 2048)
+                    .group(bossGroup, workGroup)
+                    .channel(NioServerSocketChannel.class)
+                    .localAddress(webSocketProperties.getPort())
+                    .childHandler(webSocketChannelInitializer);
+
+            channelFuture = serverBootstrap.bind().sync();
+        } finally {
+            if (channelFuture != null && channelFuture.isSuccess()) {
+                log.info("Netty server startup on port: {} (websocket) with context path '{}'", webSocketProperties.getPort(), webSocketProperties.getPath());
+            } else {
+                log.error("Netty server startup failed.");
+                if (bossGroup != null)
+                    bossGroup.shutdownGracefully().sync();
+                if (workGroup != null)
+                    workGroup.shutdownGracefully().sync();
+            }
+        }
+    }
+
+    @Override
+    public void destroy() throws Exception {
+        log.info("Shutting down Netty server...");
+        if (bossGroup != null)
+            bossGroup.shutdownGracefully().sync();
+        if (workGroup != null)
+            workGroup.shutdownGracefully().sync();
+        if (channelFuture != null)
+            channelFuture.channel().closeFuture().syncUninterruptibly();
+        log.info("Netty server shutdown.");
+    }
+
+}
+

+ 30 - 0
hhface-middleware/src/main/java/cn/hh/hhface/config/PrintBanner.java

@@ -0,0 +1,30 @@
+package cn.hh.hhface.config;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.context.event.ApplicationReadyEvent;
+import org.springframework.context.ApplicationListener;
+import org.springframework.stereotype.Component;
+
+/**
+ * @author shisl
+ * @package cn.hh.hhface.config
+ * @class PrintBanner
+ * @date 2023/12/15 下午3:20
+ * @description
+ */
+@Component
+@Slf4j
+public class PrintBanner implements ApplicationListener<ApplicationReadyEvent> {
+
+    String readyString = "\n" +
+            "┓ ┓ ┏          • ┓ ┓┓           \n" +
+            "┣┓┣┓╋┏┓┏┏┓  ┏┳┓┓┏┫┏┫┃┏┓┓┏┏┏┓┏┓┏┓\n" +
+            "┛┗┛┗┛┗┻┗┗   ┛┗┗┗┗┻┗┻┗┗ ┗┻┛┗┻┛ ┗ \n" +
+            "                                \n";
+
+    @Override
+    public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) {
+        // 输出banner长字符串
+        log.info(readyString);
+    }
+}

+ 94 - 0
hhface-middleware/src/main/java/cn/hh/hhface/config/RedisConfig.java

@@ -0,0 +1,94 @@
+package cn.hh.hhface.config;
+
+import cn.hh.common.config.RedisConfiguration;
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.PropertyAccessor;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.Data;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.cache.CacheManager;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.cache.RedisCacheConfiguration;
+import org.springframework.data.redis.cache.RedisCacheManager;
+import org.springframework.data.redis.cache.RedisCacheWriter;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
+import org.springframework.data.redis.serializer.RedisSerializer;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author shisl
+ * @package cn.hh.party.building.management.config
+ * @class RedisConfig
+ * @date 2022/12/19 下午5:20
+ * @description
+ */
+@Data
+@Configuration
+public class RedisConfig {
+
+    @Autowired
+    private LettuceConnectionFactory lettuceConnectionFactory;
+
+    @Bean(name = "redisTemplate")
+    @Qualifier(value = "redisTemplate")
+    public RedisTemplate<String, String> redisTemplateJackson() {
+        StringRedisTemplate template = new StringRedisTemplate(lettuceConnectionFactory);
+        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
+        ObjectMapper om = new ObjectMapper();
+        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
+        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
+        jackson2JsonRedisSerializer.setObjectMapper(om);
+        template.setValueSerializer(jackson2JsonRedisSerializer);
+        template.afterPropertiesSet();
+
+        // Long类型不可以会出现异常信息
+        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
+        template.setKeySerializer(redisSerializer);
+        template.setHashKeySerializer(redisSerializer);
+
+        return template;
+    }
+
+    @Bean(name = "redisTemplateCommon")
+    @Qualifier(value = "redisTemplateCommon")
+    public RedisTemplate<String, String> redisTemplateCommon() {
+        StringRedisTemplate template = new StringRedisTemplate(lettuceConnectionFactory);
+        return template;
+    }
+
+    /**
+     * @param redisConnectionFactory
+     * @return
+     */
+    @Bean
+    public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
+        return new RedisCacheManager(
+                RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory),
+
+                // 默认策略1天,未配置的 key 会使用这个
+                RedisConfiguration.getRedisCacheConfigurationWithDays(1L),
+
+                // 指定 key 策略
+                this.getRedisCacheConfigurationMap()
+        );
+    }
+
+    private Map<String, RedisCacheConfiguration> getRedisCacheConfigurationMap() {
+        Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<String, RedisCacheConfiguration>();
+        //TODO。。。
+        redisCacheConfigurationMap.put("redisKey", RedisConfiguration.getRedisCacheConfigurationWithHours(2L));
+
+
+        return redisCacheConfigurationMap;
+    }
+
+}

+ 88 - 0
hhface-middleware/src/main/java/cn/hh/hhface/config/RequestInterceptor.java

@@ -0,0 +1,88 @@
+package cn.hh.hhface.config;
+
+import cn.hh.common.restful.BusinessException;
+import cn.hh.common.restful.RestCode;
+import cn.hh.common.utils.IPUtils;
+import cn.hh.hhface.jwt.JwtService;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Component;
+import org.springframework.web.method.HandlerMethod;
+import org.springframework.web.servlet.HandlerInterceptor;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.lang.reflect.Method;
+
+/**
+ * @author shisl
+ * @package cn.hh.hhface.config
+ * @class RequestInterceptor
+ * @date 2022/12/20 上午10:32
+ * @description
+ */
+@Slf4j
+@Component
+public class RequestInterceptor implements HandlerInterceptor {
+
+    private static String HEADER_TOKEN = "Authorization";
+
+    @Override
+    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+        //防止线程池环境下 ThreadLocal未被清理掉
+        log.info("【=========preHandle  admin 管理员权限认证 begin==========】");
+        log.info("管理员 IP={} : 请求 path={}", IPUtils.getIP(request), request.getServletPath());
+
+        // 如果不是映射到方法直接通过
+        if (!handler.getClass().isAssignableFrom(HandlerMethod.class)) {
+            return true;
+        }
+        HandlerMethod handlerMethod = (HandlerMethod) handler;
+        Method method = handlerMethod.getMethod();
+        //检查是否有Authority注释
+        Authority annotation = method.getAnnotation(Authority.class);
+        if (annotation != null) {
+            if (!annotation.required()) {
+                return true;
+            }
+        } else {
+            annotation = handlerMethod.getBean().getClass().getAnnotation(Authority.class);
+            if (annotation != null) {
+                if (!annotation.required()) {
+                    return true;
+                }
+            }
+        }
+
+        //检查有没有需要用户权限的注解
+        String token = request.getHeader(HEADER_TOKEN);
+        if (StringUtils.isEmpty(token)) {
+            throw new BusinessException(RestCode.UNAUTHORIZED);
+        }
+        // 执行认证
+        String accountId = JwtService.getUserId(token);
+        if (StringUtils.isEmpty(accountId)) {
+            throw new BusinessException(RestCode.UNAUTHORIZED);
+        }
+
+        if (annotation == null) {
+            return true;
+        }
+
+        int[] needAuth = annotation.value();
+        if (needAuth.length == 0) {
+            return true;
+        }
+        if (needAuth[0] == 0) {
+            return true;
+        }
+        throw new BusinessException(RestCode.UNAUTHORIZED);
+    }
+
+    @Override
+    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
+
+    }
+
+
+}

+ 82 - 0
hhface-middleware/src/main/java/cn/hh/hhface/config/SwaggerConfig.java

@@ -0,0 +1,82 @@
+package cn.hh.hhface.config;
+
+import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j;
+import io.swagger.models.auth.In;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import springfox.documentation.builders.ApiInfoBuilder;
+import springfox.documentation.builders.PathSelectors;
+import springfox.documentation.builders.RequestHandlerSelectors;
+import springfox.documentation.builders.RequestParameterBuilder;
+import springfox.documentation.schema.ScalarType;
+import springfox.documentation.service.ApiInfo;
+import springfox.documentation.service.ApiKey;
+import springfox.documentation.service.ParameterType;
+import springfox.documentation.service.RequestParameter;
+import springfox.documentation.service.SecurityScheme;
+import springfox.documentation.spi.DocumentationType;
+import springfox.documentation.spring.web.plugins.Docket;
+import springfox.documentation.swagger2.annotations.EnableSwagger2;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * @author shisl
+ * @package cn.hh.hhface.config
+ * @class SwaggerConfig
+ * @date 2022/12/19 下午10:59
+ * @description
+ */
+@Configuration
+@EnableSwagger2
+@EnableKnife4j
+public class SwaggerConfig {
+
+    @Value("${swagger.enable}")
+    private boolean enable;
+
+    @Bean
+    public Docket createRestApi() {
+
+        return new Docket(DocumentationType.SWAGGER_2)
+                .enable(enable)
+                .securitySchemes(securitySchemes())
+                .globalRequestParameters(globalRequestParameters())
+                .useDefaultResponseMessages(false)
+                .apiInfo(apiInfo())
+                .groupName("1.X版本")
+                .select()
+                .apis(RequestHandlerSelectors.basePackage("cn.hh.hhface.controller"))
+                .paths(PathSelectors.any())
+                .build();
+    }
+
+    private List<RequestParameter> globalRequestParameters() {
+        RequestParameterBuilder parameterBuilder = new RequestParameterBuilder()
+                //每次请求加载header
+                .in(ParameterType.HEADER)
+                //头标签
+                .name("Authorization")
+                .required(false)
+                .query(param -> param.model(model -> model.scalarModel(ScalarType.STRING)));
+        return Collections.singletonList(parameterBuilder.build());
+    }
+
+    private ApiInfo apiInfo() {
+        return new ApiInfoBuilder()
+                .title("业务中台服务")
+                .description("后台服务接口")
+                .termsOfServiceUrl("http://localhost:9086//middle-management/doc.html#/home")
+                .version("1.0")
+                .build();
+    }
+
+
+    private List<SecurityScheme> securitySchemes() {
+        return Arrays.asList(new ApiKey("Authorization", "令牌", In.HEADER.name()));
+    }
+
+}

+ 124 - 0
hhface-middleware/src/main/java/cn/hh/hhface/config/WebAppConfig.java

@@ -0,0 +1,124 @@
+package cn.hh.hhface.config;
+
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.MapperFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import com.fasterxml.jackson.databind.ser.std.NumberSerializer;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.converter.HttpMessageConverter;
+import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
+import org.springframework.web.servlet.config.annotation.CorsRegistry;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
+
+import java.math.BigDecimal;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * @author shisl
+ * @package cn.hh.hhface.config
+ * @class WebAppConfig
+ * @date 2022/12/20 上午10:26
+ * @description
+ */
+@Configuration
+public class WebAppConfig extends WebMvcConfigurationSupport {
+
+    /**
+     * 上传地址
+     */
+    @Value("${file.upload.path}")
+    private String filePath;
+    /**
+     * 显示相对地址
+     */
+    @Value("${file.upload.path.relative}")
+    private String fileRelativePath;
+
+    private final RequestInterceptor requestInterceptor;
+
+    public WebAppConfig(RequestInterceptor requestInterceptor) {
+        this.requestInterceptor = requestInterceptor;
+    }
+
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        registry.addInterceptor(requestInterceptor).addPathPatterns("/**").excludePathPatterns(excludePaths());
+        super.addInterceptors(registry);
+    }
+
+    public List<String> excludePaths() {
+        // 实际项目你可以写任意代码来查询这些path
+        return Arrays.asList(
+                "**/swagger-ui.html",
+                "/swagger-resources/**",
+                "/webjars/**",
+                "/v2/**",
+                "/swagger-ui.html/**",
+                "/doc.html/**",
+                "/error",
+                "/favicon.ico",
+                "/excel/**",
+                "/api/v1/file/**",
+                "/api/v1/download/**",
+                "/api/v1/upload/**",
+                "/api/v1/device/heart/tracking",
+                "/api/v1/device/offline/tracking",
+                "/api/v1/device/zfb5year/msg/send",
+                "/api/v1/app/log/push",
+                "/api/v1/device/restart",
+                "/api/v1/device/info/get"
+        );
+    }
+
+    @Override
+    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
+        registry.addResourceHandler(fileRelativePath).addResourceLocations("file:" + filePath);
+        registry.addResourceHandler("/**").addResourceLocations("classpath:/static/");
+        // 这里使用的是 knife4j,默认是 doc.html
+        registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/");
+        registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
+        registry.addResourceHandler("/back/**").addResourceLocations("classpath:/META-INF/resources/back/");
+        registry.addResourceHandler("/excel/**").addResourceLocations("classpath:/META-INF/resources/excel/");
+        super.addResourceHandlers(registry);
+    }
+
+    @Override
+    protected void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
+        MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
+        ObjectMapper objectMapper = new ObjectMapper();
+        SimpleModule simpleModule = new SimpleModule();
+        /**
+         *  将Long,BigInteger,BigDecimal序列化的时候,转化为String
+         */
+        simpleModule.addSerializer(BigDecimal.class, NumberSerializer.instance);
+        objectMapper.registerModule(simpleModule);
+        objectMapper.configure(MapperFeature.PROPAGATE_TRANSIENT_MARKER, true);
+        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+        objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
+        messageConverter.setObjectMapper(objectMapper);
+        converters.add(messageConverter);
+    }
+
+    @Override
+    public void addCorsMappings(CorsRegistry registry) {
+        registry.addMapping("/**")
+                .allowedOrigins("*")
+                .allowedMethods("GET", "POST", "OPTIONS", "DELETE", "PUT")
+                .allowedHeaders("*")
+                .exposedHeaders("Access-Control-Allow-Headers",
+                        "Access-Control-Allow-Methods",
+                        "Access-Control-Allow-Origin",
+                        "Access-Control-Max-Age",
+                        "X-Frame-Options")
+                .allowCredentials(false)
+                .maxAge(3600);
+    }
+
+
+}

+ 24 - 0
hhface-middleware/src/main/java/cn/hh/hhface/config/WebSocketProperties.java

@@ -0,0 +1,24 @@
+package cn.hh.hhface.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+/**
+ * @author shisl
+ * @package cn.hh.hhface.config
+ * @class WebSocketProperties
+ * @date 2023/12/10 下午5:59
+ * @description WebSocketProperties配置类
+ */
+@Data
+@Component
+@ConfigurationProperties(prefix = "websocket.netty")
+public class WebSocketProperties {
+
+    private Integer port = 19089; // 监听端口
+    private String path = "/hhfacews"; // 请求路径
+    private Integer boss = 4; // 2 bossGroup线程数
+    private Integer work = 4; // 2 workGroup线程数
+
+}

+ 37 - 0
hhface-middleware/src/main/java/cn/hh/hhface/controller/HhFaceIssueController.java

@@ -0,0 +1,37 @@
+package cn.hh.hhface.controller;
+
+import cn.hh.common.restful.RestResponse;
+import cn.hh.common.restful.RestResult;
+import cn.hh.hhface.bean.isssue.WebsocketMsgReq;
+import cn.hh.hhface.service.WebsocketService;
+import com.alibaba.fastjson.JSONObject;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * @author shisl
+ * @package cn.hh.hhface.controller
+ * @class HhFaceIssueController
+ * @date 2024/9/14 上午10:43
+ * @description hhface下发api 接收结构为websocket文本消息
+ */
+@RestController
+@RequestMapping("/api/v1/hhface/issue")
+public class HhFaceIssueController {
+
+    @Autowired
+    private WebsocketService websocketService;
+
+    @ApiOperation("hhface下发api 接收结构为websocket文本消息")
+    @PostMapping("/message/send")
+    public RestResult<Boolean> sendMessage(@RequestBody WebsocketMsgReq msgReq) {
+        websocketService.handleTqServerWebsocketOp(JSONObject.parseObject(JSONObject.toJSONString(msgReq)), null, null);
+        return RestResponse.ok(Boolean.TRUE);
+    }
+
+
+}

+ 103 - 0
hhface-middleware/src/main/java/cn/hh/hhface/jwt/JwtService.java

@@ -0,0 +1,103 @@
+package cn.hh.hhface.jwt;
+
+import cn.hh.common.restful.BusinessException;
+import cn.hh.common.restful.RestCode;
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.JWTVerifier;
+import com.auth0.jwt.algorithms.Algorithm;
+import com.auth0.jwt.exceptions.JWTDecodeException;
+import com.auth0.jwt.exceptions.JWTVerificationException;
+import com.auth0.jwt.interfaces.DecodedJWT;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.time.DateUtils;
+import org.springframework.stereotype.Service;
+import org.springframework.util.DigestUtils;
+
+import java.util.Date;
+
+
+/**
+ * @author shisl
+ * @package cn.hh.party.building.management.jwt
+ * @class JwtService
+ * @date 2022/12/20 上午10:37
+ * @description
+ */
+@Slf4j
+@Service
+public class JwtService {
+
+    private final static String SALT = "YcYp65l;uq234pwb;iIjHg872S-ajfL.khp636";
+
+    /**
+     * 校验token是否正确
+     *
+     * @param token 密钥
+     * @param phone 用户的密码
+     */
+    public static void verify(String token, String userId, String phone) {
+        try {
+            String secret = DigestUtils.md5DigestAsHex(phone.concat(SALT).getBytes());
+            Algorithm algorithm = Algorithm.HMAC256(secret);
+            JWTVerifier verifier = JWT.require(algorithm)
+                    .withClaim("userId", userId)
+                    .build();
+            verifier.verify(token);
+        } catch (JWTVerificationException e) {
+            throw new BusinessException(RestCode.UNAUTHORIZED.getCode(), RestCode.UNAUTHORIZED.getMsg());
+        }
+    }
+
+    /**
+     * 获得token中的信息无需secret解密也能获得
+     *
+     * @return token中包含的用户名
+     */
+    public static String getUserId(String token) {
+        try {
+            DecodedJWT jwt = JWT.decode(token);
+            return jwt.getClaim("userId").asString();
+        } catch (JWTDecodeException e) {
+            throw new BusinessException(RestCode.UNAUTHORIZED.getCode(), RestCode.UNAUTHORIZED.getMsg());
+        }
+    }
+
+    /**
+     * 生成签名,5min后过期
+     *
+     * @param userId 用户Id
+     * @param password  用户的密码
+     * @return 加密的token
+     */
+    public static String sign(String userId, String password) {
+        return sign(userId, password, getExpireTime());
+    }
+
+
+    public static String sign(String userId, String password, Date expireTime) {
+        String secret = DigestUtils.md5DigestAsHex(password.concat(SALT).getBytes());
+        Algorithm algorithm = Algorithm.HMAC256(secret);
+        return JWT.create()
+                .withClaim("userId", userId)
+                .withExpiresAt(expireTime)
+                .withIssuedAt(new Date())
+                .sign(algorithm);
+    }
+
+    /**
+     * token是否过期
+     *
+     * @return true:过期
+     */
+    public static boolean isTokenExpired(String token) {
+        Date now = new Date(System.currentTimeMillis());
+        DecodedJWT jwt = JWT.decode(token);
+        return jwt.getExpiresAt().before(now);
+    }
+
+    private static Date getExpireTime() {
+        return DateUtils.addYears(new Date(), 1);
+    }
+
+
+}

+ 409 - 0
hhface-middleware/src/main/java/cn/hh/hhface/service/WebsocketService.java

@@ -0,0 +1,409 @@
+package cn.hh.hhface.service;
+
+import cn.hh.common.constants.CommonConstants;
+import cn.hh.common.enumeration.UpgradeTypeEnum;
+import cn.hh.common.utils.Preconditions;
+import cn.hh.hhface.bean.FilePO;
+import cn.hh.hhface.bean.device.DeviceBizDataDto;
+import cn.hh.hhface.bean.device.DeviceVo;
+import cn.hh.hhface.bean.device.DownloadLogDto;
+import cn.hh.hhface.bean.device.FetchLogDto;
+import cn.hh.hhface.bean.log.AppLogReportDto;
+import cn.hh.hhface.bean.task.DeviceTaskStatusVo;
+import cn.hh.hhface.bean.websocket.WebsocketSenderDto;
+import cn.hh.hhface.config.NioWebSocketChannelPool;
+import cn.hutool.http.HttpUtil;
+import com.alibaba.fastjson.JSONObject;
+import io.netty.channel.Channel;
+import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.List;
+
+/**
+ * @author shisl
+ * @package cn.hh.hhface.service
+ * @class WebsockerService
+ * @date 2023/12/13 下午4:04
+ * @description
+ */
+@Service
+@Slf4j
+public class WebsocketService {
+
+    @Resource
+    private RedisTemplate<String, String> redisTemplate;
+
+    @Resource
+    private NioWebSocketChannelPool webSocketChannelPool;
+
+    @Value("${websocket.httpApiSwitch}")
+    private Boolean httpApiSwitch;
+
+    @Value("${websocket.httpApi}")
+    private String httpApiUrl;
+
+    /**
+     * 关闭应用
+     *
+     * @param sn
+     */
+    public void close(String sn, String md5Token, String bizAppApkName) {
+        String toChannelId = redisTemplate.opsForValue().get(String.format(CommonConstants.WEBSOCKET_SN_KEY, sn));
+        if (Preconditions.isBlank(toChannelId)) {
+            log.error("设备【{}】未连接websocket服务", sn);
+            return;
+        }
+        Channel toChannel = webSocketChannelPool.findChannel(toChannelId);
+        if (Preconditions.isBlank(toChannel)) {
+            log.error("设备【{}】未连接websocket服务", sn);
+            return;
+        }
+        //获取发送者
+        String fromChannelId = null;
+        if (Preconditions.isNotBlank(md5Token)) {
+            fromChannelId = redisTemplate.opsForValue().get(String.format(CommonConstants.WEBSOCKET_PC_TOKEN_KEY, md5Token));
+        }        //TODO 组装业务参数
+        JSONObject obj = new JSONObject();
+        obj.put("sn", sn);
+        obj.put("packageName", bizAppApkName);
+        obj.put(CommonConstants.WEBSOCKET_SOURCE, CommonConstants.WEBSOCKET_SOURCE_PC);
+        obj.put(CommonConstants.WEBSOCKET_OP, CommonConstants.WEBSOCKET_OP_CLOSE_APP);
+        obj.put("msg", "关闭应用");
+        obj.put("sender", fromChannelId);
+        log.info("PC端发送的消息内容:{}", obj.toJSONString());
+        toChannel.writeAndFlush(new TextWebSocketFrame(obj.toJSONString()));
+    }
+
+    /**
+     * 卸载应用
+     *
+     * @param sn
+     * @param md5Token
+     * @param bizAppApkName
+     */
+    public void uninstall(String sn, String md5Token, String bizAppApkName) {
+        String toChannelId = redisTemplate.opsForValue().get(String.format(CommonConstants.WEBSOCKET_SN_KEY, sn));
+        if (Preconditions.isBlank(toChannelId)) {
+            log.error("设备【{}】未连接websocket服务", sn);
+            return;
+        }
+        Channel toChannel = webSocketChannelPool.findChannel(toChannelId);
+        if (Preconditions.isBlank(toChannel)) {
+            log.error("设备【{}】未连接websocket服务", sn);
+            return;
+        }
+        //获取发送者
+        String fromChannelId = null;
+        if (Preconditions.isNotBlank(md5Token)) {
+            fromChannelId = redisTemplate.opsForValue().get(String.format(CommonConstants.WEBSOCKET_PC_TOKEN_KEY, md5Token));
+        }        //TODO 组装业务参数
+        JSONObject obj = new JSONObject();
+        obj.put("sn", sn);
+        obj.put("packageName", bizAppApkName);
+        obj.put(CommonConstants.WEBSOCKET_SOURCE, CommonConstants.WEBSOCKET_SOURCE_PC);
+        obj.put(CommonConstants.WEBSOCKET_OP, CommonConstants.WEBSOCKET_OP_UNINSTALL_APP);
+        obj.put("msg", "卸载应用");
+        obj.put("sender", fromChannelId);
+        log.info("PC端发送的消息内容:{}", obj.toJSONString());
+        toChannel.writeAndFlush(new TextWebSocketFrame(obj.toJSONString()));
+    }
+
+    /**
+     * 重启设备
+     *
+     * @param sn
+     */
+    public void reboot(String sn, String md5Token) {
+        String toChannelId = redisTemplate.opsForValue().get(String.format(CommonConstants.WEBSOCKET_SN_KEY, sn));
+        if (Preconditions.isBlank(toChannelId)) {
+            log.error("设备【{}】未连接websocket服务", sn);
+            return;
+        }
+        Channel toChannel = webSocketChannelPool.findChannel(toChannelId);
+        if (Preconditions.isBlank(toChannel)) {
+            log.error("设备【{}】未连接websocket服务", sn);
+            return;
+        }
+        //获取发送者
+        String fromChannelId = null;
+        if (Preconditions.isNotBlank(md5Token)) {
+            fromChannelId = redisTemplate.opsForValue().get(String.format(CommonConstants.WEBSOCKET_PC_TOKEN_KEY, md5Token));
+        }
+        //TODO 组装业务参数
+        JSONObject obj = new JSONObject();
+        obj.put("sn", sn);
+        obj.put("sender", fromChannelId);
+        obj.put(CommonConstants.WEBSOCKET_SOURCE, CommonConstants.WEBSOCKET_SOURCE_PC);
+        obj.put(CommonConstants.WEBSOCKET_OP, CommonConstants.WEBSOCKET_OP_REBOOT);
+        obj.put("msg", "重启设备");
+        log.info("PC端发送的消息内容:{}", obj.toJSONString());
+        toChannel.writeAndFlush(new TextWebSocketFrame(obj.toJSONString()));
+    }
+
+
+    /**
+     * 停止下载
+     *
+     * @param sn
+     */
+    public void stopDownload(String sn, Long taskId, String md5Token) {
+        String toChannelId = redisTemplate.opsForValue().get(String.format(CommonConstants.WEBSOCKET_SN_KEY, sn));
+        if (Preconditions.isBlank(toChannelId)) {
+            log.error("设备【{}】未连接websocket服务", sn);
+            return;
+        }
+        Channel toChannel = webSocketChannelPool.findChannel(toChannelId);
+        if (Preconditions.isBlank(toChannel)) {
+            log.error("设备【{}】未连接websocket服务", sn);
+            return;
+        }
+        //获取发送者
+        String fromChannelId = null;
+        if (Preconditions.isNotBlank(md5Token)) {
+            fromChannelId = redisTemplate.opsForValue().get(String.format(CommonConstants.WEBSOCKET_PC_TOKEN_KEY, md5Token));
+        }
+        //TODO 组装业务参数
+        JSONObject obj = new JSONObject();
+        obj.put("sn", sn);
+        obj.put("taskId", taskId);
+        obj.put(CommonConstants.WEBSOCKET_SOURCE, CommonConstants.WEBSOCKET_SOURCE_PC);
+        obj.put(CommonConstants.WEBSOCKET_OP, CommonConstants.WEBSOCKET_OP_STOP_DOWNLOAD);
+        obj.put("msg", "停止下载");
+        obj.put("sender", fromChannelId);
+        log.info("PC端发送的消息内容:{}", obj.toJSONString());
+        toChannel.writeAndFlush(new TextWebSocketFrame(obj.toJSONString()));
+
+    }
+
+    /**
+     * 取消安装升级
+     *
+     * @param sn
+     * @param taskId
+     * @param md5Token
+     */
+    public void cancelUpgrade(String apkPath, String packageName, String versionName, String sn, Long taskId, String md5Token) {
+        String toChannelId = redisTemplate.opsForValue().get(String.format(CommonConstants.WEBSOCKET_SN_KEY, sn));
+        if (Preconditions.isBlank(toChannelId)) {
+            log.error("设备【{}】未连接websocket服务", sn);
+            return;
+        }
+        Channel toChannel = webSocketChannelPool.findChannel(toChannelId);
+        if (Preconditions.isBlank(toChannel)) {
+            log.error("设备【{}】未连接websocket服务", sn);
+            return;
+        }
+        //获取发送者
+        String fromChannelId = null;
+        if (Preconditions.isNotBlank(md5Token)) {
+            fromChannelId = redisTemplate.opsForValue().get(String.format(CommonConstants.WEBSOCKET_PC_TOKEN_KEY, md5Token));
+        }
+        //TODO 组装业务参数
+        JSONObject obj = new JSONObject();
+        obj.put("sn", sn);
+        obj.put("taskId", taskId);
+        obj.put("filePath", apkPath);
+        obj.put("packageName", packageName);
+        obj.put("versionName", versionName);
+        obj.put(CommonConstants.WEBSOCKET_SOURCE, CommonConstants.WEBSOCKET_SOURCE_PC);
+        obj.put(CommonConstants.WEBSOCKET_OP, CommonConstants.WEBSOCKET_OP_CANCEL_UPGRADE);
+        obj.put("msg", "取消安装升级");
+        obj.put("sender", fromChannelId);
+        log.info("PC端发送的消息内容:{}", obj.toJSONString());
+        toChannel.writeAndFlush(new TextWebSocketFrame(obj.toJSONString()));
+
+    }
+
+    /**
+     * 日志抓取
+     *
+     * @param dto
+     */
+    public void fetchLog(FetchLogDto dto, String md5Token) {
+        String sn = dto.getSn();
+        String toChannelId = redisTemplate.opsForValue().get(String.format(CommonConstants.WEBSOCKET_SN_KEY, sn));
+        if (Preconditions.isBlank(toChannelId)) {
+            log.error("设备【{}】未连接websocket服务", sn);
+            return;
+        }
+        Channel toChannel = webSocketChannelPool.findChannel(toChannelId);
+        if (Preconditions.isBlank(toChannel)) {
+            log.error("设备【{}】未连接websocket服务", sn);
+            return;
+        }
+        //获取发送者
+        String fromChannelId = null;
+        if (Preconditions.isNotBlank(md5Token)) {
+            fromChannelId = redisTemplate.opsForValue().get(String.format(CommonConstants.WEBSOCKET_PC_TOKEN_KEY, md5Token));
+        }
+        //TODO 组装业务参数
+        JSONObject obj = new JSONObject();
+        obj.put("sn", sn);
+        obj.put(CommonConstants.WEBSOCKET_SOURCE, CommonConstants.WEBSOCKET_SOURCE_PC);
+        obj.put(CommonConstants.WEBSOCKET_OP, CommonConstants.WEBSOCKET_OP_FETCH_LOG);
+        obj.put("logType", dto.getLogType());
+        obj.put("logDirPath", dto.getPathUri());
+        obj.put("sender", fromChannelId);
+        obj.put("msg", "拉取日志");
+        log.info("PC端发送的消息内容:{}", obj.toJSONString());
+        toChannel.writeAndFlush(new TextWebSocketFrame(obj.toJSONString()));
+    }
+
+    /**
+     * 下载日志
+     *
+     * @param dto
+     */
+    public void downloadLog(DownloadLogDto dto, String md5Token) {
+        String sn = dto.getSn();
+        String toChannelId = redisTemplate.opsForValue().get(String.format(CommonConstants.WEBSOCKET_SN_KEY, sn));
+        if (Preconditions.isBlank(toChannelId)) {
+            log.error("设备【{}】未连接websocket服务", sn);
+            return;
+        }
+        Channel toChannel = webSocketChannelPool.findChannel(toChannelId);
+        if (Preconditions.isBlank(toChannel)) {
+            log.error("设备【{}】未连接websocket服务", sn);
+            return;
+        }
+        //获取发送者
+        String fromChannelId = null;
+        if (Preconditions.isNotBlank(md5Token)) {
+            fromChannelId = redisTemplate.opsForValue().get(String.format(CommonConstants.WEBSOCKET_PC_TOKEN_KEY, md5Token));
+        }
+        //TODO 组装业务参数
+        JSONObject obj = new JSONObject();
+        obj.put("sn", sn);
+        obj.put(CommonConstants.WEBSOCKET_SOURCE, CommonConstants.WEBSOCKET_SOURCE_PC);
+        obj.put(CommonConstants.WEBSOCKET_OP, CommonConstants.WEBSOCKET_OP_DOWNLOAD_LOG);
+        obj.put("logType", dto.getLogType());
+        obj.put("sender", fromChannelId);
+        obj.put("paths", dto.getLogUris());
+        obj.put("msg", "下载日志");
+        log.info("PC端发送的消息内容:{}", obj.toJSONString());
+        toChannel.writeAndFlush(new TextWebSocketFrame(obj.toJSONString()));
+
+    }
+
+    /**
+     * 处理接收到的app websocket消息
+     *
+     * @param object
+     * @param text
+     * @param op
+     */
+    public void handleAppWebsocketOp(JSONObject object, String text, String op) {
+        String sn = object.getString(CommonConstants.WEBSOCKET_CONNECT_APP);
+        if (Preconditions.isBlank(sn)) {
+            log.error("设备号为空");
+            return;
+        }
+
+        if (CommonConstants.WEBSOCKET_OP_FETCH_LOG.equals(op) || CommonConstants.WEBSOCKET_OP_DOWNLOAD_LOG.equals(op)) { //拉取日志 下载日志
+            //发送websocket给pc端 WebsocketSenderDto
+            WebsocketSenderDto sender = object.toJavaObject(WebsocketSenderDto.class);
+            if (Preconditions.isNotBlank(sender.getSender())) {
+                sendAppWebsocketMsg2Pc(sender.getSender(), text);
+            } else {
+                sendAppWebsocketMsg2AllPc(text);
+            }
+            return;
+        }
+
+    }
+
+    private void sendAppWebsocketMsg2Pc(String channelId, String text) {
+        Channel channel = webSocketChannelPool.findChannel(channelId);
+        if (Preconditions.isBlank(channel)) {
+            log.error("此PC端未连接websocket服务");
+            return;
+        }
+        channel.writeAndFlush(new TextWebSocketFrame(text));
+    }
+
+    public void sendAppWebsocketMsg2AllPc(String text) {
+        if (!redisTemplate.hasKey(CommonConstants.WEBSOCKET_PC_KEY)) {
+            log.error("未找到连接websocket服务的PC端");
+            return;
+        }
+        String s = redisTemplate.opsForValue().get(CommonConstants.WEBSOCKET_PC_KEY);
+        List<String> channels = JSONObject.parseArray(s, String.class);
+        if (Preconditions.isBlank(channels)) {
+            log.error("未找到连接websocket服务的PC端");
+            return;
+        }
+
+        for (String channelId : channels) {
+            Channel channel = webSocketChannelPool.findChannel(channelId);
+            if (Preconditions.isBlank(channel)) {
+                log.error("此PC端未连接websocket服务");
+                continue;
+            }
+            channel.writeAndFlush(new TextWebSocketFrame(text));
+        }
+    }
+
+
+    //处理塘栖服务端发送来的消息
+    public void handleTqServerWebsocketOp(JSONObject object, String text, String op) {
+        String sn = object.getString(CommonConstants.WEBSOCKET_CONNECT_APP);
+        if (Preconditions.isBlank(sn)) {
+            log.error("设备号为空");
+            return;
+        }
+        String toChannelId = redisTemplate.opsForValue().get(String.format(CommonConstants.WEBSOCKET_SN_SOURCE_KEY, "HHFACE", sn));
+        if (Preconditions.isBlank(toChannelId)) {
+            log.error("设备【{}】未连接websocket服务", sn);
+            return;
+        }
+        Channel toChannel = webSocketChannelPool.findChannel(toChannelId);
+        if (Preconditions.isBlank(toChannel)) {
+            log.error("设备【{}】未连接websocket服务", sn);
+            return;
+        }
+        toChannel.writeAndFlush(new TextWebSocketFrame(object.toJSONString()));
+    }
+
+    //TODO 处理HHFace发送来的消息 以及http协议请求
+    public void handleHhFaceAppWebsocketOp(JSONObject object, String text, String op) {
+        if (httpApiSwitch) {
+            String respStr = HttpUtil.post(httpApiUrl, object.toJSONString(), 60000);
+            log.info("--->http请求callback api接口:{}", respStr);
+//        RestResult result = JSONObject.parseObject(respStr, RestResult.class);
+            return;
+        }
+        sendHhFaceAppWebsocketMsg2AllTq(text);
+    }
+
+    //向所有塘栖平台广播消息
+    public void sendHhFaceAppWebsocketMsg2AllTq(String text) {
+        if (!redisTemplate.hasKey(CommonConstants.WEBSOCKET_PC_SOURCE_KEY)) {
+            log.error("未找到连接websocket服务的PC端");
+            return;
+        }
+        String s = redisTemplate.opsForValue().get(CommonConstants.WEBSOCKET_PC_SOURCE_KEY);
+        List<String> channels = JSONObject.parseArray(s, String.class);
+        if (Preconditions.isBlank(channels)) {
+            log.error("未找到连接websocket服务的PC端");
+            return;
+        }
+
+        for (String channelId : channels) {
+            Channel channel = webSocketChannelPool.findChannel(channelId);
+            if (Preconditions.isBlank(channel)) {
+                log.error("此PC端未连接websocket服务");
+                continue;
+            }
+            channel.writeAndFlush(new TextWebSocketFrame(text));
+        }
+
+    }
+
+
+}

+ 81 - 0
hhface-middleware/src/main/resources/application-dev.yml

@@ -0,0 +1,81 @@
+server:
+  port: 9089
+  servlet:
+    context-path: /hhface-middleware
+    session:
+      persistent: false
+  undertow:
+    threads:
+      worker: 4000
+      io: 400
+
+spring:
+  application:
+    name: hhface-middleware
+  jackson:
+    date-format: yyyy-MM-dd HH:mm:ss
+    # default-property-inclusion: non_null
+    time-zone: GMT+8
+  thymeleaf:
+    cache: false   #设置为false,否则会有缓存,导致页面没法及时看到更新后的效果
+  messages:
+    basename: message/messages_zh_CN
+    encoding: UTF-8
+  servlet:
+    multipart:
+      enabled: true
+      max-file-size: 50MB
+      max-request-size: 50MB
+  redis:
+    host: 127.0.0.1
+    port: 6379
+    password: ''
+    timeout: 10000
+    database: 8
+    lettuce:
+      pool:
+        min-idle: 10
+        max-wait: 1000
+        max-idle: 10
+        max-active: 30
+      shutdown-timeout: 10000
+
+logging:
+  file:
+    name: ./logs/${spring.application.name}.log
+  level:
+    com.yixin.review: info
+    com.yx.face.dao: debug
+  logback:
+    rollingpolicy:
+      max-history: 10
+      max-file-size: 10MB
+
+##``````````````````````````````````` 在线文档 配置``````````````````````````````````````````````````````````````###
+swagger:
+  enable: true
+knife4j:
+  enable: true
+  basic:
+    enable: true
+    ## Basic认证用户名
+    username: hhface
+    ## Basic认证密码
+    password: hhface
+
+##``````````````````````````````````` 上传文件 配置````````````````````````````````###
+file.upload.path: file/
+file.upload.picPath: file/
+file.upload.path.relative: /file/**
+web.address: http://192.168.77.61:9089/hhface-middleware/
+
+##``````````````````````````````````` websocket 配置````````````````````````````````###
+websocket:
+  netty:
+    #监听端口
+    port: 19089
+    #请求路径
+    path: /hhfacews
+  httpApiSwitch: false
+  httpApi: https://test.hz-hanghui.com:18890/yx-fyzd/api/v1/hhface/issue/message/callback
+

+ 80 - 0
hhface-middleware/src/main/resources/application-prod.yml

@@ -0,0 +1,80 @@
+server:
+  port: 9089
+  servlet:
+    context-path: /hhface-middleware
+    session:
+      persistent: false
+  undertow:
+    threads:
+      worker: 4000
+      io: 400
+
+spring:
+  application:
+    name: hhface-middleware
+  jackson:
+    date-format: yyyy-MM-dd HH:mm:ss
+    # default-property-inclusion: non_null
+    time-zone: GMT+8
+  thymeleaf:
+    cache: false   #设置为false,否则会有缓存,导致页面没法及时看到更新后的效果
+  messages:
+    basename: message/messages_zh_CN
+    encoding: UTF-8
+  servlet:
+    multipart:
+      enabled: true
+      max-file-size: 50MB
+      max-request-size: 50MB
+  redis:
+    host: 127.0.0.1
+    port: 6379
+    password: ''
+    timeout: 10000
+    database: 8
+    lettuce:
+      pool:
+        min-idle: 10
+        max-wait: 1000
+        max-idle: 10
+        max-active: 30
+      shutdown-timeout: 10000
+
+logging:
+  file:
+    name: ./logs/${spring.application.name}.log
+  level:
+    com.yixin.review: info
+    com.yx.face.dao: debug
+  logback:
+    rollingpolicy:
+      max-history: 10
+      max-file-size: 10MB
+
+##``````````````````````````````````` 在线文档 配置``````````````````````````````````````````````````````````````###
+swagger:
+  enable: false
+knife4j:
+  enable: false
+  basic:
+    enable: false
+    ## Basic认证用户名
+    username: hhface
+    ## Basic认证密码
+    password: hhface
+
+##``````````````````````````````````` 上传文件 配置````````````````````````````````###
+file.upload.path: file/
+file.upload.picPath: file/
+file.upload.path.relative: /file/**
+web.address: https://hhomc.hz-hanghui.com:8096/hhface-middleware/
+
+##``````````````````````````````````` websocket 配置````````````````````````````````###
+websocket:
+  netty:
+    #监听端口
+    port: 19089
+    #请求路径
+    path: /hhfacews
+  httpApiSwitch: false
+  httpApi: http://192.168.1.32:9100/yx-fyzd/api/v1/hhface/issue/message/callback

+ 80 - 0
hhface-middleware/src/main/resources/application-test.yml

@@ -0,0 +1,80 @@
+server:
+  port: 9089
+  servlet:
+    context-path: /hhface-middleware
+    session:
+      persistent: false
+  undertow:
+    threads:
+      worker: 4000
+      io: 400
+
+spring:
+  application:
+    name: hhface-middleware
+  jackson:
+    date-format: yyyy-MM-dd HH:mm:ss
+    # default-property-inclusion: non_null
+    time-zone: GMT+8
+  thymeleaf:
+    cache: false   #设置为false,否则会有缓存,导致页面没法及时看到更新后的效果
+  messages:
+    basename: message/messages_zh_CN
+    encoding: UTF-8
+  servlet:
+    multipart:
+      enabled: true
+      max-file-size: 50MB
+      max-request-size: 50MB
+  redis:
+    host: localhost
+    port: 6379
+    password: 117521
+    timeout: 10000
+    database: 8
+    lettuce:
+      pool:
+        min-idle: 10
+        max-wait: 1000
+        max-idle: 10
+        max-active: 30
+      shutdown-timeout: 10000
+
+logging:
+  file:
+    name: ./logs/${spring.application.name}.log
+  level:
+    com.yixin.review: info
+    com.yx.face.dao: debug
+  logback:
+    rollingpolicy:
+      max-history: 10
+      max-file-size: 10MB
+
+##``````````````````````````````````` 在线文档 配置``````````````````````````````````````````````````````````````###
+swagger:
+  enable: true
+knife4j:
+  enable: true
+  basic:
+    enable: true
+    ## Basic认证用户名
+    username: hhface
+    ## Basic认证密码
+    password: hhface
+
+##``````````````````````````````````` 上传文件 配置````````````````````````````````###
+file.upload.path: file/
+file.upload.picPath: file/
+file.upload.path.relative: /file/**
+web.address: https://test.hz-hanghui.com:18890/hhface-middleware/
+
+##``````````````````````````````````` websocket 配置````````````````````````````````###
+websocket:
+  netty:
+    #监听端口
+    port: 19089
+    #请求路径
+    path: /hhfacews
+  httpApiSwitch: false
+  httpApi: http://127.0.0.1:9100/yx-fyzd/api/v1/hhface/issue/message/callback

+ 3 - 0
hhface-middleware/src/main/resources/application.yml

@@ -0,0 +1,3 @@
+spring:
+  profiles:
+    active: dev

+ 2 - 0
pom.xml

@@ -21,6 +21,8 @@
         <module>yqk-common</module>
         <module>yqk-job</module>
         <module>yqk-linkage</module>
+        <module>hhface-common</module>
+        <module>hhface-middleware</module>
     </modules>
     <!--maven依赖-->
     <properties>