فهرست منبع

初始化项目

mac 7 ماه پیش
کامیت
430de4b394
100فایلهای تغییر یافته به همراه6725 افزوده شده و 0 حذف شده
  1. 63 0
      .gitignore
  2. 87 0
      kxs-common/kxs-common-core/pom.xml
  3. 42 0
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/config/JacksonConfiguration.java
  4. 62 0
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/config/RedisTemplateConfiguration.java
  5. 20 0
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/config/RestTemplateConfiguration.java
  6. 55 0
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/config/TaskExecutorConfiguration.java
  7. 55 0
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/config/WebMvcConfiguration.java
  8. 65 0
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/constant/CacheConstants.java
  9. 113 0
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/constant/CommonConstants.java
  10. 130 0
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/constant/SecurityConstants.java
  11. 30 0
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/constant/ServiceNameConstants.java
  12. 37 0
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/constant/enums/DictTypeEnum.java
  13. 50 0
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/constant/enums/ErrorTypeEnum.java
  14. 37 0
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/constant/enums/LoginTypeEnum.java
  15. 39 0
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/constant/enums/MachineTypeEnum.java
  16. 42 0
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/constant/enums/MenuTypeEnum.java
  17. 38 0
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/constant/enums/UserStatusEnum.java
  18. 21 0
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/exception/AesDecodeException.java
  19. 36 0
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/exception/CheckedException.java
  20. 176 0
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/exception/GlobalBizExceptionHandler.java
  21. 20 0
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/exception/GlobalCustomerException.java
  22. 21 0
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/exception/ValidateCodeException.java
  23. 53 0
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/jackson/SkyJavaTimeModule.java
  24. 86 0
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/util/AssertUtil.java
  25. 96 0
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/util/ClassUtils.java
  26. 79 0
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/util/R.java
  27. 639 0
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/util/RedisUtils.java
  28. 271 0
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/util/RetOps.java
  29. 95 0
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/util/SpringContextHolder.java
  30. 53 0
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/util/SysUtils.java
  31. 110 0
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/util/TotalDateUtil.java
  32. 155 0
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/util/WebUtils.java
  33. 29 0
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/valid/NormMonth.java
  34. 29 0
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/valid/Phone.java
  35. 29 0
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/valid/Sn.java
  36. 40 0
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/valid/impl/NormMonthValidator.java
  37. 39 0
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/valid/impl/PhoneValidator.java
  38. 39 0
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/valid/impl/SnValidator.java
  39. 7 0
      kxs-common/kxs-common-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  40. 14 0
      kxs-common/kxs-common-core/src/main/resources/banner.txt
  41. 143 0
      kxs-common/kxs-common-core/src/main/resources/logback-spring.xml
  42. 104 0
      kxs-common/kxs-common-excel/pom.xml
  43. 100 0
      kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/ExcelHandlerConfiguration.java
  44. 95 0
      kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/ResponseExcelAutoConfiguration.java
  45. 35 0
      kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/annotation/EnumFiledConvert.java
  46. 10 0
      kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/annotation/ExcelLine.java
  47. 42 0
      kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/annotation/RequestExcel.java
  48. 105 0
      kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/annotation/ResponseExcel.java
  49. 41 0
      kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/annotation/Sheet.java
  50. 46 0
      kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/aop/DynamicNameAspect.java
  51. 85 0
      kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/aop/RequestExcelArgumentResolver.java
  52. 84 0
      kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/aop/ResponseExcelReturnValueHandler.java
  53. 21 0
      kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/config/ExcelConfigProperties.java
  54. 70 0
      kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/config/ExcelSystemFeignClientConfiguration.java
  55. 42 0
      kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/config/FileProperties.java
  56. 130 0
      kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/converters/EasyExcelConvert.java
  57. 75 0
      kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/converters/ImportAsyncInfo.java
  58. 61 0
      kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/converters/LocalDateStringConverter.java
  59. 93 0
      kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/converters/LocalDateTimeStringConverter.java
  60. 47 0
      kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/enhance/DefaultWriterBuilderEnhancer.java
  61. 41 0
      kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/enhance/WriterBuilderEnhancer.java
  62. 315 0
      kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/handler/AbstractSheetWriteHandler.java
  63. 70 0
      kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/handler/CustomWidthStyleStrategyHandler.java
  64. 73 0
      kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/handler/DefaultAnalysisEventListener.java
  65. 26 0
      kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/handler/ListAnalysisEventListener.java
  66. 129 0
      kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/handler/ManySheetWriteHandler.java
  67. 68 0
      kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/handler/SheetWriteHandler.java
  68. 121 0
      kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/handler/SingleSheetWriteHandler.java
  69. 22 0
      kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/head/HeadGenerator.java
  70. 29 0
      kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/head/HeadMeta.java
  71. 61 0
      kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/head/I18nHeaderCellWriteHandler.java
  72. 25 0
      kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/http/RemoteSysService.java
  73. 15 0
      kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/kit/ExcelException.java
  74. 40 0
      kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/kit/Validators.java
  75. 20 0
      kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/processor/NameProcessor.java
  76. 40 0
      kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/processor/NameSpelExpressionProcessor.java
  77. 41 0
      kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/vo/ErrorMessage.java
  78. 1 0
      kxs-common/kxs-common-excel/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  79. 56 0
      kxs-common/kxs-common-idempotent/pom.xml
  80. 69 0
      kxs-common/kxs-common-idempotent/src/main/java/com/kxs/common/idempotent/IdempotentAutoConfiguration.java
  81. 51 0
      kxs-common/kxs-common-idempotent/src/main/java/com/kxs/common/idempotent/annotation/Idempotent.java
  82. 142 0
      kxs-common/kxs-common-idempotent/src/main/java/com/kxs/common/idempotent/aspect/IdempotentAspect.java
  83. 38 0
      kxs-common/kxs-common-idempotent/src/main/java/com/kxs/common/idempotent/config/RedisProperties.java
  84. 33 0
      kxs-common/kxs-common-idempotent/src/main/java/com/kxs/common/idempotent/exception/IdempotentException.java
  85. 66 0
      kxs-common/kxs-common-idempotent/src/main/java/com/kxs/common/idempotent/expression/ExpressionResolver.java
  86. 23 0
      kxs-common/kxs-common-idempotent/src/main/java/com/kxs/common/idempotent/expression/KeyResolver.java
  87. 1 0
      kxs-common/kxs-common-idempotent/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  88. 54 0
      kxs-common/kxs-common-log/pom.xml
  89. 35 0
      kxs-common/kxs-common-log/src/main/java/com/kxs/common/log/LogAutoConfiguration.java
  90. 28 0
      kxs-common/kxs-common-log/src/main/java/com/kxs/common/log/annotation/SysLog.java
  91. 81 0
      kxs-common/kxs-common-log/src/main/java/com/kxs/common/log/aspect/SysLogAspect.java
  92. 39 0
      kxs-common/kxs-common-log/src/main/java/com/kxs/common/log/config/SkyLogProperties.java
  93. 18 0
      kxs-common/kxs-common-log/src/main/java/com/kxs/common/log/event/SysLogEvent.java
  94. 22 0
      kxs-common/kxs-common-log/src/main/java/com/kxs/common/log/event/SysLogEventSource.java
  95. 76 0
      kxs-common/kxs-common-log/src/main/java/com/kxs/common/log/event/SysLogListener.java
  96. 34 0
      kxs-common/kxs-common-log/src/main/java/com/kxs/common/log/init/ApplicationLoggerInitializer.java
  97. 37 0
      kxs-common/kxs-common-log/src/main/java/com/kxs/common/log/util/LogTypeEnum.java
  98. 123 0
      kxs-common/kxs-common-log/src/main/java/com/kxs/common/log/util/SysLogUtils.java
  99. 30 0
      kxs-common/kxs-common-log/src/main/resources/META-INF/spring-configuration-metadata.json
  100. 1 0
      kxs-common/kxs-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

+ 63 - 0
.gitignore

@@ -0,0 +1,63 @@
+### gradle ###
+.gradle
+/build/
+!gradle/wrapper/gradle-wrapper.jar
+
+### STS ###
+.settings/
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+bin/
+db/
+data/
+target/
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+rebel.xml
+
+### NetBeans ###
+nbproject/private/
+build/
+nbbuild/
+nbdist/
+.nb-gradle/
+
+### maven ###
+target/
+*.war
+*.ear
+*.zip
+*.tar
+*.tar.gz
+*.versionsBackup
+*.yml
+### vscode ###
+.vscode
+yarn.lock
+
+### logs ###
+/logs/
+*.log
+
+### temp ignore ###
+*.cache
+*.diff
+*.patch
+*.tmp
+*.java~
+*.properties~
+*.xml~
+
+### system ignore ###
+.DS_Store
+Thumbs.db
+Servers
+.metadata

+ 87 - 0
kxs-common/kxs-common-core/pom.xml

@@ -0,0 +1,87 @@
+<?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">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>com.kxs</groupId>
+        <artifactId>kxs-common</artifactId>
+        <version>1.1.0</version>
+    </parent>
+
+    <artifactId>kxs-common-core</artifactId>
+    <packaging>jar</packaging>
+
+    <description>kxs 公共工具类核心包</description>
+
+    <properties>
+        <maven.compiler.source>17</maven.compiler.source>
+        <maven.compiler.target>17</maven.compiler.target>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+
+    <dependencies>
+        <!--hutool-->
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-core</artifactId>
+        </dependency>
+        <!--redis-->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-redis</artifactId>
+        </dependency>
+        <!--server-api-->
+        <dependency>
+            <groupId>jakarta.servlet</groupId>
+            <artifactId>jakarta.servlet-api</artifactId>
+        </dependency>
+        <!--hibernate-validator-->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-validation</artifactId>
+        </dependency>
+        <!--json模块-->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-json</artifactId>
+        </dependency>
+        <!--oauth server 依赖-->
+        <dependency>
+            <groupId>org.springframework.security</groupId>
+            <artifactId>spring-security-core</artifactId>
+        </dependency>
+
+        <!--阿里云SLS日志包-->
+        <dependency>
+            <groupId>com.google.protobuf</groupId>
+            <artifactId>protobuf-java</artifactId>
+            <version>2.5.0</version>
+        </dependency>
+        <dependency>
+            <groupId>com.aliyun.openservices</groupId>
+            <artifactId>aliyun-log-logback-appender</artifactId>
+            <version>0.1.18</version>
+        </dependency>
+
+
+        <!--spring-webmvc-->
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-webmvc</artifactId>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <configuration>
+                    <source>14</source>
+                    <target>14</target>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+</project>

+ 42 - 0
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/config/JacksonConfiguration.java

@@ -0,0 +1,42 @@
+package com.kxs.common.core.config;
+
+import cn.hutool.core.date.DatePattern;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import com.kxs.common.core.jackson.SkyJavaTimeModule;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.AutoConfigureBefore;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
+import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
+import org.springframework.context.annotation.Bean;
+
+import java.time.ZoneId;
+import java.util.Locale;
+import java.util.TimeZone;
+
+/**
+ * JacksonConfig 配置一些序列化场景,雪花id转字符串等 长数字精度丢失问题
+ *
+ * @author 没秃顶的码农
+ * @date 2023/11/08
+ */
+@AutoConfiguration
+@ConditionalOnClass(ObjectMapper.class)
+@AutoConfigureBefore(JacksonAutoConfiguration.class)
+public class JacksonConfiguration {
+
+	@Bean
+	@ConditionalOnMissingBean
+	public Jackson2ObjectMapperBuilderCustomizer customizer() {
+		return builder -> {
+			builder.locale(Locale.CHINA);
+			builder.timeZone(TimeZone.getTimeZone(ZoneId.systemDefault()));
+			builder.simpleDateFormat(DatePattern.NORM_DATETIME_PATTERN);
+			builder.serializerByType(Long.class, ToStringSerializer.instance);
+			builder.modules(new SkyJavaTimeModule());
+		};
+	}
+
+}

+ 62 - 0
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/config/RedisTemplateConfiguration.java

@@ -0,0 +1,62 @@
+
+package com.kxs.common.core.config;
+
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.AutoConfigureBefore;
+import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Primary;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.*;
+import org.springframework.data.redis.serializer.RedisSerializer;
+
+/**
+ * Redis 配置类
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/27
+ */
+@EnableCaching
+@AutoConfiguration
+@AutoConfigureBefore(RedisAutoConfiguration.class)
+public class RedisTemplateConfiguration {
+
+	@Bean
+	@Primary
+	public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
+		RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
+		redisTemplate.setKeySerializer(RedisSerializer.string());
+		redisTemplate.setHashKeySerializer(RedisSerializer.string());
+		redisTemplate.setValueSerializer(RedisSerializer.java());
+		redisTemplate.setHashValueSerializer(RedisSerializer.java());
+		redisTemplate.setConnectionFactory(factory);
+		return redisTemplate;
+	}
+
+	@Bean
+	public HashOperations<String, String, Object> hashOperations(RedisTemplate<String, Object> redisTemplate) {
+		return redisTemplate.opsForHash();
+	}
+
+	@Bean
+	public ValueOperations<String, String> valueOperations(RedisTemplate<String, String> redisTemplate) {
+		return redisTemplate.opsForValue();
+	}
+
+	@Bean
+	public ListOperations<String, Object> listOperations(RedisTemplate<String, Object> redisTemplate) {
+		return redisTemplate.opsForList();
+	}
+
+	@Bean
+	public SetOperations<String, Object> setOperations(RedisTemplate<String, Object> redisTemplate) {
+		return redisTemplate.opsForSet();
+	}
+
+	@Bean
+	public ZSetOperations<String, Object> zSetOperations(RedisTemplate<String, Object> redisTemplate) {
+		return redisTemplate.opsForZSet();
+	}
+
+}

+ 20 - 0
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/config/RestTemplateConfiguration.java

@@ -0,0 +1,20 @@
+
+package com.kxs.common.core.config;
+
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.web.client.RestTemplate;
+
+/**
+ * @author 没秃顶的码农
+ * @date 2023/2/1 RestTemplate
+ */
+@AutoConfiguration
+public class RestTemplateConfiguration {
+
+	@Bean
+	public RestTemplate restTemplate() {
+		return new RestTemplate();
+	}
+
+}

+ 55 - 0
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/config/TaskExecutorConfiguration.java

@@ -0,0 +1,55 @@
+package com.kxs.common.core.config;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.scheduling.annotation.AsyncConfigurer;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+
+import java.util.Optional;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ThreadPoolExecutor;
+
+/**
+ * @author 没秃顶的码农
+ * @date 2022/5/20
+ */
+@AutoConfiguration
+public class TaskExecutorConfiguration implements AsyncConfigurer {
+
+	/**
+	 * 获取当前机器的核数, 不一定准确 请根据实际场景 CPU密集 || IO 密集
+	 */
+	public static final int cpuNum = Runtime.getRuntime().availableProcessors();
+
+	@Value("${thread.pool.corePoolSize:}")
+	private Optional<Integer> corePoolSize;
+
+	@Value("${thread.pool.maxPoolSize:}")
+	private Optional<Integer> maxPoolSize;
+
+	@Value("${thread.pool.queueCapacity:}")
+	private Optional<Integer> queueCapacity;
+
+	@Value("${thread.pool.awaitTerminationSeconds:}")
+	private Optional<Integer> awaitTerminationSeconds;
+
+	@Override
+	@Bean
+	public Executor getAsyncExecutor() {
+		ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
+		// 核心线程大小 默认区 CPU 数量
+		taskExecutor.setCorePoolSize(corePoolSize.orElse(cpuNum));
+		// 最大线程大小 默认区 CPU * 2 数量
+		taskExecutor.setMaxPoolSize(maxPoolSize.orElse(cpuNum * 2));
+		// 队列最大容量
+		taskExecutor.setQueueCapacity(queueCapacity.orElse(500));
+		taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
+		taskExecutor.setWaitForTasksToCompleteOnShutdown(true);
+		taskExecutor.setAwaitTerminationSeconds(awaitTerminationSeconds.orElse(60));
+		taskExecutor.setThreadNamePrefix("KXS-Thread-");
+		taskExecutor.initialize();
+		return taskExecutor;
+	}
+
+}

+ 55 - 0
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/config/WebMvcConfiguration.java

@@ -0,0 +1,55 @@
+
+package com.kxs.common.core.config;
+
+import cn.hutool.core.date.DatePattern;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
+import org.springframework.context.MessageSource;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.support.ReloadableResourceBundleMessageSource;
+import org.springframework.format.FormatterRegistry;
+import org.springframework.format.datetime.standard.DateTimeFormatterRegistrar;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+import static org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type.SERVLET;
+
+/**
+ * @author 没秃顶的码农
+ * @date 2023-06-24
+ * <p>
+ * 注入自自定义SQL 过滤
+ */
+@AutoConfiguration
+@ConditionalOnWebApplication(type = SERVLET)
+public class WebMvcConfiguration implements WebMvcConfigurer {
+
+	/**
+	 * 增加GET请求参数中时间类型转换 {@link com.kxs.common.core.jackson.SkyJavaTimeModule}
+	 * <ul>
+	 * <li>HH:mm:ss -> LocalTime</li>
+	 * <li>yyyy-MM-dd -> LocalDate</li>
+	 * <li>yyyy-MM-dd HH:mm:ss -> LocalDateTime</li>
+	 * </ul>
+	 * @param registry
+	 */
+	@Override
+	public void addFormatters(FormatterRegistry registry) {
+		DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
+		registrar.setTimeFormatter(DatePattern.NORM_TIME_FORMATTER);
+		registrar.setDateFormatter(DatePattern.NORM_DATE_FORMATTER);
+		registrar.setDateTimeFormatter(DatePattern.NORM_DATETIME_FORMATTER);
+		registrar.registerFormatters(registry);
+	}
+
+	/**
+	 * 系统国际化文件配置
+	 * @return MessageSource
+	 */
+	@Bean
+	public MessageSource messageSource() {
+		ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
+		messageSource.setBasename("classpath:i18n/messages");
+		return messageSource;
+	}
+
+}

+ 65 - 0
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/constant/CacheConstants.java

@@ -0,0 +1,65 @@
+
+package com.kxs.common.core.constant;
+
+/**
+ * @author 没秃顶的码农
+ * @date 2021年01月01日
+ * <p>
+ * 缓存的key 常量
+ */
+public interface CacheConstants {
+
+	/**
+	 * oauth 缓存前缀
+	 */
+	String PROJECT_OAUTH_ACCESS = "llb_token::access_token";
+
+	/**
+	 * 验证码前缀
+	 */
+	String DEFAULT_CODE_KEY = "LLB_DEFAULT_CODE_KEY:";
+
+	/**
+	 * 菜单信息缓存
+	 */
+	String MENU_DETAILS = "llb_menu_details";
+
+	/**
+	 * 用户信息缓存
+	 */
+	String USER_DETAILS = "llb_user_details";
+
+	/**
+	 * 字典信息缓存
+	 */
+	String DICT_DETAILS = "llb_dict_details";
+
+	/**
+	 * 角色信息缓存
+	 */
+	String ROLE_DETAILS = "llb_role_details";
+
+	/**
+	 * oauth 客户端信息
+	 */
+	String CLIENT_DETAILS_KEY = "llb_client:details";
+
+	/**
+	 * 参数缓存
+	 */
+	String PARAMS_DETAILS = "llb_params_details";
+	String INTERFACE_DETAILS = "interface_details";
+
+
+	/**
+	 * 用户职级缓存前缀
+	 */
+	String USER_LEVEL = "user_level:";
+
+	String BANNER_LIST = "banner_list:";
+
+	/**
+	 * 上传文件缓存前缀
+	 */
+	String FILE_EXCEL = "file_excel";
+}

+ 113 - 0
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/constant/CommonConstants.java

@@ -0,0 +1,113 @@
+
+package com.kxs.common.core.constant;
+
+/**
+ * 公共常量
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/29
+ */
+public interface CommonConstants {
+
+	/**
+	 * 删除
+	 */
+	String STATUS_DEL = "1";
+
+	/**
+	 * 正常
+	 */
+	String STATUS_NORMAL = "0";
+
+	/**
+	 * 锁定
+	 */
+	String STATUS_LOCK = "9";
+
+	/**
+	 * 菜单树根节点
+	 */
+	Long MENU_TREE_ROOT_ID = -1L;
+
+	/**
+	 * 菜单
+	 */
+	String MENU = "0";
+
+	/**
+	 * 编码
+	 */
+	String UTF8 = "UTF-8";
+
+	/**
+	 * JSON 资源
+	 */
+	String CONTENT_TYPE = "application/json; charset=utf-8";
+
+	/**
+	 * 前端工程名
+	 */
+	String FRONT_END_PROJECT = "kxs-admin-ui";
+
+	/**
+	 * 后端工程名
+	 */
+	String BACK_END_PROJECT = "kxs";
+
+	/**
+	 * 包名
+	 */
+	String PACKAGE_NAME = "com.kxs.llb";
+
+	/**
+	 * 成功标记
+	 */
+	Integer SUCCESS = 1;
+
+	/**
+	 * 失败标记
+	 */
+	Integer FAIL = 0;
+
+	/**
+	 * 当前页
+	 */
+	String CURRENT = "pageNum";
+
+	/**
+	 * size
+	 */
+	String SIZE = "pageSize";
+
+	/**
+	 * 请求开始时间
+	 */
+	String REQUEST_START_TIME = "REQUEST-START-TIME";
+
+
+	/**
+	 * 验证码个数
+	 */
+	public static final Integer CODE_SIZE = 6;
+
+	/**
+	 * 验证码有效期,默认 60秒
+	 */
+	public static final long CODE_TIME = 60 * 5;
+
+	String ORDER_SN_PREFIX = "LHB";
+	String CASH_SN_PREFIX = "CA";
+
+	String REFUND_ORDER_PREFIX = "RT";
+
+	String PRODUCT_FLAG = "product_flag";
+
+	String CASH_FLAG = "cash_flag";
+
+	String DELETE_FLAG = "d";
+    String WECHAT_TOKEN_PREFIX = "wechat_token:";
+	String ORDER_PUSH_NAME = "OrderNotify";
+	String BIND_PUSH_NAME = "BindNotify";
+    String RESULT_SUC = "订购成功";
+
+}

+ 130 - 0
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/constant/SecurityConstants.java

@@ -0,0 +1,130 @@
+
+package com.kxs.common.core.constant;
+
+/**
+ * @author 没秃顶的码农
+ * @date 2023/2/1
+ */
+public interface SecurityConstants {
+
+	/**
+	 * 角色前缀
+	 */
+	String ROLE = "ROLE_";
+
+	/**
+	 * 前缀
+	 */
+	String PROJECT_PREFIX = "llb";
+
+	/**
+	 * BEARER
+	 */
+	String TOKEN_TYPE = "Bearer";
+
+	/**
+	 * 内部
+	 */
+	String FROM_IN = "Y";
+
+	/**
+	 * 标志
+	 */
+	String FROM = "from";
+
+	/**
+	 * 请求header
+	 */
+	String HEADER_FROM_IN = FROM + "=" + FROM_IN;
+
+	/**
+	 * 默认登录URL
+	 */
+	String OAUTH_TOKEN_URL = "/oauth2/token";
+
+	/**
+	 * grant_type
+	 */
+	String REFRESH_TOKEN = "refresh_token";
+
+	/**
+	 * 手机号登录
+	 */
+	String MOBILE = "mobile";
+
+	/**
+	 * 客户端
+	 */
+	String KXS = "llb";
+
+	/**
+	 * {bcrypt} 加密的特征码
+	 */
+	String BCRYPT = "{bcrypt}";
+
+	/**
+	 * {noop} 加密的特征码
+	 */
+	String NOOP = "{noop}";
+
+	/***
+	 * 资源服务器默认bean名称
+	 */
+	String RESOURCE_SERVER_CONFIGURER = "resourceServerConfigurerAdapter";
+
+	/**
+	 * 用户名
+	 */
+	String USERNAME = "username";
+
+	/**
+	 * 用户信息
+	 */
+	String DETAILS_USER = "user_info";
+
+	/**
+	 * 用户ID
+	 */
+	String DETAILS_USER_ID = "user_id";
+
+	/**
+	 * 协议字段
+	 */
+	String DETAILS_LICENSE = "license";
+
+	/**
+	 * 验证码有效期,默认 60秒
+	 */
+	long CODE_TIME = 60;
+
+	/**
+	 * 验证码长度
+	 */
+	String CODE_SIZE = "6";
+
+	/**
+	 * 客户端模式
+	 */
+	String CLIENT_CREDENTIALS = "client_credentials";
+
+	/**
+	 * 客户端ID
+	 */
+	String CLIENT_ID = "clientId";
+
+	/**
+	 * 短信登录 参数名称
+	 */
+	String SMS_PARAMETER_NAME = "mobile";
+
+	/**
+	 * 授权码模式confirm
+	 */
+	String CUSTOM_CONSENT_PAGE_URI = "/token/confirm_access";
+
+	/**
+	 * 项目的license
+	 */
+	String PROJECT_LICENSE = "https://apigateway.kexiaoshuang.com";
+
+}

+ 30 - 0
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/constant/ServiceNameConstants.java

@@ -0,0 +1,30 @@
+
+
+package com.kxs.common.core.constant;
+
+
+import java.time.Duration;
+
+/**
+ * 服务名称
+ *
+ * @author Lxq
+ * @date 2023-12-04
+ */
+public interface ServiceNameConstants {
+
+	String CLIENT_NAME = "http://";
+
+	/**
+	 * 请求超时时间
+	 * 单位:秒
+	 */
+	Duration REQ_TIMEOUT = Duration.ofSeconds(30);
+
+	/**
+	 * 系统模块
+	 */
+	String SYSTEM_SERVICE = "lhb-system-biz";
+	String MECH_SERVICE = "lhb-mech-biz";
+
+}

+ 37 - 0
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/constant/enums/DictTypeEnum.java

@@ -0,0 +1,37 @@
+package com.kxs.common.core.constant.enums;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * <p>
+ * 字典类型
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/27
+ */
+@Getter
+@RequiredArgsConstructor
+public enum DictTypeEnum {
+
+	/**
+	 * 字典类型-系统内置(不可修改)
+	 */
+	SYSTEM("1", "系统内置"),
+
+	/**
+	 * 字典类型-业务类型
+	 */
+	BIZ("0", "业务类");
+
+	/**
+	 * 类型
+	 */
+	private final String type;
+
+	/**
+	 * 描述
+	 */
+	private final String description;
+
+}

+ 50 - 0
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/constant/enums/ErrorTypeEnum.java

@@ -0,0 +1,50 @@
+package com.kxs.common.core.constant.enums;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * 错误类型枚举
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/29
+ */
+@Getter
+@RequiredArgsConstructor
+public enum ErrorTypeEnum {
+
+	DECRYPT_ERROR(-1, "解密异常"),
+	TOKEN_EXPIRED(-1, "令牌已过期"),
+	TOKEN_EMPTY(-1, "令牌不存在"),
+	CLIENT_NULL(-1, "客户端查询异常,请检查数据库链接"),
+	ACCESS_DENIED(-1, "不允许范访问"),
+	BAD_CREDENTIALS(-1, "用户名或密码错误"),
+	CREDENTIALS_EXPIRED(-1, "用户凭证已过期"),
+	USER_DISABLED(-1, "用户已失效"),
+	ACCOUNT_DISABLED(-1, "用户帐号已过期"),
+	ACCOUNT_LOCKED(-1, "用户帐号已被锁定"),
+	USER_NOT_FOUND(-1, "未找到用户"),
+	PHONE_NOT_FOUND(-1, "手机号不存在"),
+	VERIFICATION_CODE_IS_ILLEGAL(-1, "验证码不正确"),
+	VERIFICATION_CODE_IS_EMPTY(-1, "验证码不能为空"),
+	UNAUTHORIZED_CLIENT(-1, "客户端不允许登陆"),
+
+	SYS_PARAM_DELETE_SYSTEM(-1, "系统内置参数不能删除"),
+	SYS_DICT_DELETE_SYSTEM(-1, "系统内置字典不允许删除"),
+	SYS_MENU_DELETE_EXISTING(-1, "菜单存在下级节点 删除失败"),
+	SYS_USER_USERNAME_EXISTING(-1, "用户名已存在"),
+	REMOTE_DATA_ERROR(-1, "远程数据调用错误"),
+	SYS_USER_UPDATE_PASSWORD_ERROR(-1, "用户原密码错误"),
+	SYS_DICT_UPDATE_SYSTEM(-1, "系统内置字典不能修改"),
+	SYS_MORNING_IS_NULL(-1, "该晨会不存在"),
+	SYS_SCHOOL_IS_NULL(-1, "该学堂不存在"),
+	SYS_TYPE_ERROR(-1, "类型异常"),
+	PARAM_ERROR(0, "参数错误"),
+
+	;
+
+	private final int code;
+
+	private final String description;
+
+}

+ 37 - 0
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/constant/enums/LoginTypeEnum.java

@@ -0,0 +1,37 @@
+package com.kxs.common.core.constant.enums;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * <p>
+ * 社交登录类型
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/27
+ */
+@Getter
+@RequiredArgsConstructor
+public enum LoginTypeEnum {
+
+	/**
+	 * 账号密码登录
+	 */
+	PWD("PWD", "账号密码登录"),
+
+	/**
+	 * 验证码登录
+	 */
+	SMS("SMS", "验证码登录");
+
+	/**
+	 * 类型
+	 */
+	private final String type;
+
+	/**
+	 * 描述
+	 */
+	private final String description;
+
+}

+ 39 - 0
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/constant/enums/MachineTypeEnum.java

@@ -0,0 +1,39 @@
+package com.kxs.common.core.constant.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 机器类型enum
+ *
+ * @author Lxq
+ * @date 2023-10-25
+ */
+@Getter
+@AllArgsConstructor
+public enum MachineTypeEnum {
+
+    /**
+     * 机具状态
+     */
+    STATUS_BAD(-1, "坏机"),
+    STATUS_DIS(0, "停用"),
+    STATUS_IS(1, "正常"),
+
+    /**
+     * 机具激活
+     */
+    NO_ACT(0, "未激活"),
+    ACT(1, "已激活"),
+    /**
+     * 机具绑定
+     */
+    NO_BIND(0, "未绑定"),
+    BIND(1, "已绑定"),
+    SOLVE_BIND(2, "已解绑");
+
+    //状态码
+    private final Integer type;
+    //状态码码描述
+    private final String description;
+}

+ 42 - 0
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/constant/enums/MenuTypeEnum.java

@@ -0,0 +1,42 @@
+package com.kxs.common.core.constant.enums;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * <p>
+ * 菜单类型
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/27
+ */
+@Getter
+@RequiredArgsConstructor
+public enum MenuTypeEnum {
+
+	/**
+	 * 左侧菜单
+	 */
+	LEFT_MENU("0", "left"),
+
+	/**
+	 * 顶部菜单
+	 */
+	TOP_MENU("2", "top"),
+
+	/**
+	 * 按钮
+	 */
+	BUTTON("1", "button");
+
+	/**
+	 * 类型
+	 */
+	private final String type;
+
+	/**
+	 * 描述
+	 */
+	private final String description;
+
+}

+ 38 - 0
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/constant/enums/UserStatusEnum.java

@@ -0,0 +1,38 @@
+package com.kxs.common.core.constant.enums;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * <p>
+ * 社交登录类型
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/27
+ */
+@Getter
+@RequiredArgsConstructor
+public enum UserStatusEnum {
+
+	/**
+	 * 账号密码登录
+	 */
+	REMOVE(0, "注销"),
+	NORMAL(1, "正常"),
+
+	/**
+	 * 锁定不可登陆和提现
+	 */
+	LOCKED(1, "锁定");
+
+	/**
+	 * 类型
+	 */
+	private final Integer code;
+
+	/**
+	 * 描述
+	 */
+	private final String description;
+
+}

+ 21 - 0
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/exception/AesDecodeException.java

@@ -0,0 +1,21 @@
+
+package com.kxs.common.core.exception;
+
+import java.io.Serial;
+
+/**
+ * 解密异常
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/27
+ */
+public class AesDecodeException extends RuntimeException {
+
+	@Serial
+	private static final long serialVersionUID = -7285211528095468156L;
+
+	public AesDecodeException(String msg) {
+		super(msg);
+	}
+
+}

+ 36 - 0
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/exception/CheckedException.java

@@ -0,0 +1,36 @@
+
+package com.kxs.common.core.exception;
+
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+
+/**
+ * 检查异常
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/27
+ */
+@NoArgsConstructor
+public class CheckedException extends RuntimeException {
+
+	@Serial
+	private static final long serialVersionUID = 1L;
+
+	public CheckedException(String message) {
+		super(message);
+	}
+
+	public CheckedException(Throwable cause) {
+		super(cause);
+	}
+
+	public CheckedException(String message, Throwable cause) {
+		super(message, cause);
+	}
+
+	public CheckedException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
+		super(message, cause, enableSuppression, writableStackTrace);
+	}
+
+}

+ 176 - 0
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/exception/GlobalBizExceptionHandler.java

@@ -0,0 +1,176 @@
+package com.kxs.common.core.exception;
+
+import com.kxs.common.core.util.R;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
+import org.springframework.core.annotation.Order;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.converter.HttpMessageNotReadableException;
+import org.springframework.util.Assert;
+import org.springframework.validation.BindException;
+import org.springframework.validation.FieldError;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.ServletRequestBindingException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.security.access.AccessDeniedException;
+
+import java.util.List;
+
+/**
+ * <p>
+ * 全局异常处理器
+ * </p>
+ *
+ * @author 没秃顶的码农
+ * @date 2023/11/04
+ */
+@Slf4j
+@RestControllerAdvice
+@Order(10000)
+public class GlobalBizExceptionHandler {
+
+	/**
+	 * 全局异常.
+	 * @param e the e
+	 * @return R
+	 */
+	@ExceptionHandler(Exception.class)
+	@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
+	public R handleGlobalException(Exception e) {
+		log.error("全局异常信息 ex={}", e.getMessage(), e);
+
+		return R.failed(e.getLocalizedMessage());
+	}
+
+	/**
+	 * 运行时异常.
+	 * @param e the e
+	 * @return R
+	 */
+	@ExceptionHandler(RuntimeException.class)
+	@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
+	public R handleGlobalRuntimeException(RuntimeException e) {
+		log.error("运行时异常 ex={}", e.getMessage(), e);
+
+		return R.failed(e.getMessage());
+	}
+
+	/**
+	 * 处理业务校验过程中碰到的非法参数异常 该异常基本由{@link Assert}抛出
+	 * @see Assert#hasLength(String, String)
+	 * @see Assert#hasText(String, String)
+	 * @see Assert#isTrue(boolean, String)
+	 * @see Assert#isNull(Object, String)
+	 * @see Assert#notNull(Object, String)
+	 * @param exception 参数校验异常
+	 * @return API返回结果对象包装后的错误输出结果
+	 */
+	@ExceptionHandler(IllegalArgumentException.class)
+	@ResponseStatus(HttpStatus.OK)
+	public R handleIllegalArgumentException(IllegalArgumentException exception) {
+		log.error("非法参数,ex = {}", exception.getMessage(), exception);
+		return R.failed(exception.getMessage());
+	}
+
+
+	/**
+	 * AccessDeniedException
+	 * @param e the e
+	 * @return R
+	 */
+	@ExceptionHandler(AccessDeniedException.class)
+	@ResponseStatus(HttpStatus.FORBIDDEN)
+	public R handleAccessDeniedException(AccessDeniedException e) {
+		log.warn("无权限异常信息 ex={}", "Access denied");
+		return R.failed(e.getLocalizedMessage());
+	}
+
+	/**
+	 * validation Exception
+	 * @param exception e
+	 * @return R
+	 */
+	@ExceptionHandler({ MethodArgumentNotValidException.class })
+	@ResponseStatus(HttpStatus.OK)
+	public R handleBodyValidException(MethodArgumentNotValidException exception) {
+		List<FieldError> fieldErrors = exception.getBindingResult().getFieldErrors();
+		log.error("参数绑定异常,ex = {}", fieldErrors.get(0).getDefaultMessage());
+		return R.failed(String.format("%s %s", fieldErrors.get(0).getField(), fieldErrors.get(0).getDefaultMessage()));
+	}
+
+	/**
+	 * validation Exception (以form-data形式传参)
+	 * @param exception
+	 * @return R
+	 */
+	@ExceptionHandler({ BindException.class })
+	@ResponseStatus(HttpStatus.BAD_REQUEST)
+	public R bindExceptionHandler(BindException exception) {
+		List<FieldError> fieldErrors = exception.getBindingResult().getFieldErrors();
+		log.error("参数绑定异常,ex = {}", fieldErrors.get(0).getDefaultMessage());
+		return R.failed(fieldErrors.get(0).getDefaultMessage());
+	}
+
+	/**
+	 * validationCode Exception
+	 * @param exception 异常
+	 * @return R
+	 */
+	@ExceptionHandler({ ValidateCodeException.class })
+	@ResponseStatus(HttpStatus.OK)
+	public R bindValidateCodeHandler(ValidateCodeException exception) {
+		log.warn("验证码异常:{}", exception.getMessage());
+		return R.failed(exception.getMessage());
+	}
+
+	/**
+	 * aes 解密 Exception
+	 * @param exception 异常
+	 * @return R
+	 */
+	@ExceptionHandler({ AesDecodeException.class })
+	@ResponseStatus(HttpStatus.OK)
+	public R<Object> bindAesDecodeException(AesDecodeException exception) {
+		log.error("解密异常:{}", exception.getMessage());
+		return R.failed(exception.getMessage());
+	}
+
+
+	/**
+	 * 自定义异常
+	 * @param exception 错误类型
+	 * @return {@link R}
+	 */
+	@ExceptionHandler(GlobalCustomerException.class)
+	public R handlerCustomException(GlobalCustomerException exception){
+		log.error("自定义异常:{}", exception.getMessage());
+		return R.failed(exception.getMessage());
+	}
+
+	/**
+	 * 参数绑定异常
+	 * @param exception 错误类型
+	 * @return {@link R}
+	 */
+	@ExceptionHandler(HttpMessageNotReadableException.class)
+	public R handlerParamException(HttpMessageNotReadableException exception){
+
+		log.error("参数绑定异常:{}", exception.getMessage());
+		return R.failed(exception.getMessage());
+	}
+
+	/**
+	 * 远程调用报错
+	 * @param exception 错误类型
+	 * @return {@link R}
+	 */
+	@ExceptionHandler(ServletRequestBindingException.class)
+	public R handlerParamException(ServletRequestBindingException exception){
+
+		log.error("参数绑定异常:{}", exception.getMessage());
+		return R.failed(exception.getMessage());
+	}
+
+}

+ 20 - 0
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/exception/GlobalCustomerException.java

@@ -0,0 +1,20 @@
+package com.kxs.common.core.exception;
+
+
+import lombok.Getter;
+
+
+/**
+ * 自定义异常
+ * @author Lxq
+ * @date 2023-10-18
+ */
+@Getter
+public class GlobalCustomerException extends RuntimeException{
+
+    public GlobalCustomerException(String message) {
+        super(message);
+    }
+
+
+}

+ 21 - 0
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/exception/ValidateCodeException.java

@@ -0,0 +1,21 @@
+
+package com.kxs.common.core.exception;
+
+import java.io.Serial;
+
+/**
+ * 验证代码异常
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/27
+ */
+public class ValidateCodeException extends RuntimeException {
+
+	@Serial
+	private static final long serialVersionUID = -7285211528095468156L;
+
+	public ValidateCodeException(String msg) {
+		super(msg);
+	}
+
+}

+ 53 - 0
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/jackson/SkyJavaTimeModule.java

@@ -0,0 +1,53 @@
+package com.kxs.common.core.jackson;
+
+import cn.hutool.core.date.DatePattern;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import com.fasterxml.jackson.datatype.jsr310.PackageVersion;
+import com.fasterxml.jackson.datatype.jsr310.deser.InstantDeserializer;
+import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
+import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
+import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
+import com.fasterxml.jackson.datatype.jsr310.ser.InstantSerializer;
+import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
+import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
+import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
+
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
+
+/**
+ * 时间默认序列化
+ *
+ * @author 没秃顶的码农
+ * @date 2023/11/08
+ */
+public class SkyJavaTimeModule extends SimpleModule {
+
+	public SkyJavaTimeModule() {
+		super(PackageVersion.VERSION);
+
+		// ======================= 时间序列化规则 ===============================
+		// yyyy-MM-dd HH:mm:ss
+		this.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DatePattern.NORM_DATETIME_FORMATTER));
+		// yyyy-MM-dd
+		this.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ISO_LOCAL_DATE));
+		// HH:mm:ss
+		this.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ISO_LOCAL_TIME));
+		// Instant 类型序列化
+		this.addSerializer(Instant.class, InstantSerializer.INSTANCE);
+
+		// ======================= 时间反序列化规则 ==============================
+		// yyyy-MM-dd HH:mm:ss
+		this.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DatePattern.NORM_DATETIME_FORMATTER));
+		// yyyy-MM-dd
+		this.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ISO_LOCAL_DATE));
+		// HH:mm:ss
+		this.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ISO_LOCAL_TIME));
+		// Instant 反序列化
+		this.addDeserializer(Instant.class, InstantDeserializer.INSTANT);
+	}
+
+}

+ 86 - 0
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/util/AssertUtil.java

@@ -0,0 +1,86 @@
+package com.kxs.common.core.util;
+
+
+
+
+
+import com.kxs.common.core.exception.GlobalCustomerException;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * 断言参数异常 util
+ *
+ * @author 没秃顶的码农
+ * @date 2024-04-10
+ */
+public class AssertUtil {
+
+     //手机的正则表达式
+     private static final Pattern CHINA_PATTERN_PHONE = Pattern.compile("^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\\d{8}$");
+
+    private static final Pattern YYYY_MM = Pattern.compile("^\\d{4}-(0[1-9]|1[0-2])$");
+    private static final Pattern YYYYMM = Pattern.compile("^\\d{4}(0[1-9]|1[0-2])$");
+    private static final Pattern INTERVAL_VARIABLE_AMOUNT = Pattern.compile("^(-?\\d+(\\.\\d{1,2})?)-(-?\\d+(\\.\\d{1,2})?)$");
+    public static final String CHINA_PATTERN_PHONE_STR =  "^((13[0-9])|(14[014-9])|(15[0-35-9])|(16[2567])|(17[0-8])|(18[0-9])|(19[0-35-9]))\\d{8}$";
+
+     /**--------------------------------------------------------
+     手机号断言
+     --------------------------------------------------------**/
+    public static void isPhone(String phone){
+
+        //格式校验
+        Matcher m = CHINA_PATTERN_PHONE.matcher(phone);
+        if(!m.matches()){
+            throw new GlobalCustomerException("手机号格式错误");
+        }
+    }
+    /**--------------------------------------------------------
+     时间格式断言
+     --------------------------------------------------------**/
+    public static void isSimpleMonthPattern(String month){
+
+        //格式校验
+        Matcher m = YYYY_MM.matcher(month);
+        if(!m.matches()){
+            throw new GlobalCustomerException("时间格式错误yyyy-MM");
+        }
+    }
+    /**--------------------------------------------------------
+     时间格式断言
+     --------------------------------------------------------**/
+    public static void isyyyyMM(String month){
+
+        //格式校验
+        Matcher m = YYYYMM.matcher(month);
+        if(!m.matches()){
+            throw new GlobalCustomerException("时间格式错误yyyyMM");
+        }
+    }
+    /**--------------------------------------------------------
+     区间格式校验  金额
+     --------------------------------------------------------**/
+    public static void isIntervalVariableAmount(String month){
+
+        //格式校验
+        Matcher m = INTERVAL_VARIABLE_AMOUNT.matcher(month);
+        if(!m.matches()){
+            throw new GlobalCustomerException("金额区间格式错误:xxxxx-xxxxx");
+        }
+    }
+
+    public static void isFalse(boolean isFalse , String error){
+        if(isFalse){
+            throw new GlobalCustomerException(error);
+        }
+    }
+    public static void isTrue(boolean isTrue , String error){
+        if(!isTrue){
+            throw new GlobalCustomerException(error);
+        }
+    }
+
+
+
+}

+ 96 - 0
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/util/ClassUtils.java

@@ -0,0 +1,96 @@
+
+package com.kxs.common.core.util;
+
+import lombok.experimental.UtilityClass;
+import org.springframework.core.BridgeMethodResolver;
+import org.springframework.core.DefaultParameterNameDiscoverer;
+import org.springframework.core.MethodParameter;
+import org.springframework.core.ParameterNameDiscoverer;
+import org.springframework.core.annotation.AnnotatedElementUtils;
+import org.springframework.core.annotation.SynthesizingMethodParameter;
+import org.springframework.web.method.HandlerMethod;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+
+/**
+ * 类工具类
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/27
+ */
+@UtilityClass
+public class ClassUtils extends org.springframework.util.ClassUtils {
+
+	private final ParameterNameDiscoverer PARAMETERNAMEDISCOVERER = new DefaultParameterNameDiscoverer();
+
+	/**
+	 * 获取方法参数信息
+	 * @param constructor 构造器
+	 * @param parameterIndex 参数序号
+	 * @return {MethodParameter}
+	 */
+	public MethodParameter getMethodParameter(Constructor<?> constructor, int parameterIndex) {
+		MethodParameter methodParameter = new SynthesizingMethodParameter(constructor, parameterIndex);
+		methodParameter.initParameterNameDiscovery(PARAMETERNAMEDISCOVERER);
+		return methodParameter;
+	}
+
+	/**
+	 * 获取方法参数信息
+	 * @param method 方法
+	 * @param parameterIndex 参数序号
+	 * @return {MethodParameter}
+	 */
+	public MethodParameter getMethodParameter(Method method, int parameterIndex) {
+		MethodParameter methodParameter = new SynthesizingMethodParameter(method, parameterIndex);
+		methodParameter.initParameterNameDiscovery(PARAMETERNAMEDISCOVERER);
+		return methodParameter;
+	}
+
+	/**
+	 * 获取Annotation
+	 * @param method Method
+	 * @param annotationType 注解类
+	 * @param <A> 泛型标记
+	 * @return {Annotation}
+	 */
+	public <A extends Annotation> A getAnnotation(Method method, Class<A> annotationType) {
+		Class<?> targetClass = method.getDeclaringClass();
+		// The method may be on an interface, but we need attributes from the target
+		// class.
+		// If the target class is null, the method will be unchanged.
+		Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);
+		// If we are dealing with method with generic parameters, find the original
+		// method.
+		specificMethod = BridgeMethodResolver.findBridgedMethod(specificMethod);
+		// 先找方法,再找方法上的类
+		A annotation = AnnotatedElementUtils.findMergedAnnotation(specificMethod, annotationType);
+		;
+		if (null != annotation) {
+			return annotation;
+		}
+		// 获取类上面的Annotation,可能包含组合注解,故采用spring的工具类
+		return AnnotatedElementUtils.findMergedAnnotation(specificMethod.getDeclaringClass(), annotationType);
+	}
+
+	/**
+	 * 获取Annotation
+	 * @param handlerMethod HandlerMethod
+	 * @param annotationType 注解类
+	 * @param <A> 泛型标记
+	 * @return {Annotation}
+	 */
+	public <A extends Annotation> A getAnnotation(HandlerMethod handlerMethod, Class<A> annotationType) {
+		// 先找方法,再找方法上的类
+		A annotation = handlerMethod.getMethodAnnotation(annotationType);
+		if (null != annotation) {
+			return annotation;
+		}
+		// 获取类上面的Annotation,可能包含组合注解,故采用spring的工具类
+		Class<?> beanType = handlerMethod.getBeanType();
+		return AnnotatedElementUtils.findMergedAnnotation(beanType, annotationType);
+	}
+
+}

+ 79 - 0
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/util/R.java

@@ -0,0 +1,79 @@
+package com.kxs.common.core.util;
+
+import com.kxs.common.core.constant.CommonConstants;
+import com.kxs.common.core.constant.enums.ErrorTypeEnum;
+import lombok.*;
+import lombok.experimental.Accessors;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 响应信息主体
+ *
+ * @param <T>
+ * @author 没秃顶的码农
+ * @date 2023/10/29
+ */
+@ToString
+@NoArgsConstructor
+@AllArgsConstructor
+@Accessors(chain = true)
+public class R<T> implements Serializable {
+
+	@Serial
+	private static final long serialVersionUID = 1L;
+
+	@Getter
+	@Setter
+	private int status;
+
+	@Getter
+	@Setter
+	private String msg;
+
+	@Getter
+	@Setter
+	private T data;
+
+	public static <T> R<T> ok() {
+		return restResult(null, CommonConstants.SUCCESS, null);
+	}
+
+	public static <T> R<T> ok(T data) {
+		return restResult(data, CommonConstants.SUCCESS, null);
+	}
+
+	public static <T> R<T> ok(T data, String msg) {
+		return restResult(data, CommonConstants.SUCCESS, msg);
+	}
+
+	public static <T> R<T> failed() {
+		return restResult(null, CommonConstants.FAIL, null);
+	}
+
+	public static <T> R<T> failed(ErrorTypeEnum errorTypeEnum) {
+		return restResult(null, errorTypeEnum.getCode(), errorTypeEnum.getDescription());
+	}
+
+	public static <T> R<T> failed(String msg) {
+		return restResult(null, CommonConstants.FAIL, msg);
+	}
+
+	public static <T> R<T> failed(T data) {
+		return restResult(data, CommonConstants.FAIL, null);
+	}
+
+	public static <T> R<T> failed(T data, String msg) {
+		return restResult(data, CommonConstants.FAIL, msg);
+	}
+
+	public static <T> R<T> restResult(T data, int code, String msg) {
+		R<T> apiResult = new R<>();
+		apiResult.setStatus(code);
+		apiResult.setData(data);
+		apiResult.setMsg(msg);
+		return apiResult;
+	}
+
+}

+ 639 - 0
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/util/RedisUtils.java

@@ -0,0 +1,639 @@
+package com.kxs.common.core.util;
+
+import cn.hutool.core.convert.Convert;
+import lombok.experimental.UtilityClass;
+import org.springframework.data.redis.connection.RedisConnection;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.*;
+import org.springframework.data.redis.core.script.DefaultRedisScript;
+import org.springframework.data.redis.core.script.RedisScript;
+
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 缓存工具类
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/27
+ */
+@UtilityClass
+public class RedisUtils {
+
+	private static final Long SUCCESS = 1L;
+
+	/**
+	 * 指定缓存失效时间
+	 * @param key 键
+	 * @param time 时间(秒)
+	 */
+	public boolean expire(String key, long time) {
+		RedisTemplate<Object, Object> redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		Optional.ofNullable(redisTemplate)
+			.filter(template -> time > 0)
+			.ifPresent(template -> template.expire(key, time, TimeUnit.SECONDS));
+		return true;
+	}
+
+	/**
+	 * 根据 key 获取过期时间
+	 * @param key 键 不能为null
+	 * @return 时间(秒) 返回0代表为永久有效
+	 */
+	public long getExpire(Object key) {
+		RedisTemplate<Object, Object> redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		return Optional.ofNullable(redisTemplate)
+			.map(template -> template.getExpire(key, TimeUnit.SECONDS))
+			.orElse(-1L);
+	}
+
+	/**
+	 * 查找匹配key
+	 * @param pattern key
+	 * @return /
+	 */
+	public List<String> scan(String pattern) {
+		RedisTemplate<Object, Object> redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		ScanOptions options = ScanOptions.scanOptions().match(pattern).build();
+		return Optional.ofNullable(redisTemplate).map(template -> {
+			RedisConnectionFactory factory = template.getConnectionFactory();
+			RedisConnection rc = Objects.requireNonNull(factory).getConnection();
+			Cursor<byte[]> cursor = rc.scan(options);
+			List<String> result = new ArrayList<>();
+			while (cursor.hasNext()) {
+				result.add(new String(cursor.next()));
+			}
+			RedisConnectionUtils.releaseConnection(rc, factory);
+			return result;
+		}).orElse(Collections.emptyList());
+	}
+
+	/**
+	 * 分页查询 key
+	 * @param patternKey key
+	 * @param page 页码
+	 * @param size 每页数目
+	 * @return /
+	 */
+	public List<String> findKeysForPage(String patternKey, int page, int size) {
+		RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		ScanOptions options = ScanOptions.scanOptions().match(patternKey).build();
+		RedisConnectionFactory factory = redisTemplate.getConnectionFactory();
+		RedisConnection rc = Objects.requireNonNull(factory).getConnection();
+		Cursor<byte[]> cursor = rc.scan(options);
+		List<String> result = new ArrayList<>(size);
+		int tmpIndex = 0;
+		int fromIndex = page * size;
+		int toIndex = page * size + size;
+		while (cursor.hasNext()) {
+			if (tmpIndex >= fromIndex && tmpIndex < toIndex) {
+				result.add(new String(cursor.next()));
+				tmpIndex++;
+				continue;
+			}
+			// 获取到满足条件的数据后,就可以退出了
+			if (tmpIndex >= toIndex) {
+				break;
+			}
+			tmpIndex++;
+			cursor.next();
+		}
+		RedisConnectionUtils.releaseConnection(rc, factory);
+		return result;
+	}
+
+	/**
+	 * 判断key是否存在
+	 * @param key 键
+	 * @return true 存在 false不存在
+	 */
+	public boolean hasKey(String key) {
+		RedisTemplate<Object, Object> redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		return Optional.ofNullable(redisTemplate).map(template -> template.hasKey(key)).orElse(false);
+	}
+
+	/**
+	 * 删除缓存
+	 * @param keys 可以传一个值 或多个
+	 */
+	public void del(String... keys) {
+		RedisTemplate<Object, Object> redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		Optional.ofNullable(keys)
+			.map(Arrays::asList)
+			.filter(keysList -> !keysList.isEmpty())
+			.ifPresent(redisTemplate::delete);
+	}
+
+	/**
+	 * 获取锁
+	 * @param lockKey 锁key
+	 * @param value value
+	 * @param expireTime:单位-秒
+	 * @return boolean
+	 */
+	public boolean getLock(String lockKey, String value, int expireTime) {
+		RedisTemplate<Object, Object> redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		return Optional.ofNullable(redisTemplate)
+			.map(template -> template.opsForValue().setIfAbsent(lockKey, value, expireTime, TimeUnit.SECONDS))
+			.orElse(false);
+	}
+
+	/**
+	 * 释放锁
+	 * @param lockKey 锁key
+	 * @param value value
+	 * @return boolean
+	 */
+	public boolean releaseLock(String lockKey, String value) {
+		RedisTemplate<Object, Object> redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
+		RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
+		return Optional.ofNullable(redisTemplate.execute(redisScript, Collections.singletonList(lockKey), value))
+			.map(Convert::toLong)
+			.filter(SUCCESS::equals)
+			.isPresent();
+	}
+
+	// ============================String=============================
+
+	/**
+	 * 普通缓存获取
+	 * @param key 键
+	 * @return 值
+	 */
+	public <T> T get(String key) {
+		RedisTemplate<String, T> redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		return redisTemplate.opsForValue().get(key);
+	}
+
+	/**
+	 * 批量获取
+	 * @param keys
+	 * @return
+	 */
+	public <T> List<T> multiGet(List<String> keys) {
+		RedisTemplate<String, T> redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		return redisTemplate.opsForValue().multiGet(keys);
+	}
+
+	/**
+	 * 普通缓存放入
+	 * @param key 键
+	 * @param value 值
+	 * @return true成功 false失败
+	 */
+	public boolean set(String key, Object value) {
+		RedisTemplate<Object, Object> redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		Optional.ofNullable(redisTemplate).map(template -> {
+			template.opsForValue().set(key, value);
+			return true;
+		});
+		return true;
+	}
+
+	/**
+	 * 普通缓存放入并设置时间
+	 * @param key 键
+	 * @param value 值
+	 * @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
+	 * @return true成功 false 失败
+	 */
+	public boolean set(String key, Object value, long time) {
+		RedisTemplate<Object, Object> redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		return Optional.ofNullable(redisTemplate).map(template -> {
+			if (time > 0) {
+				template.opsForValue().set(key, value, time, TimeUnit.SECONDS);
+			}
+			else {
+				template.opsForValue().set(key, value);
+			}
+			return true;
+		}).orElse(false);
+	}
+
+	/**
+	 * 普通缓存放入并设置时间
+	 * @param key 键
+	 * @param value 值
+	 * @param time 时间
+	 * @param timeUnit 类型
+	 * @return true成功 false 失败
+	 */
+	public <T> boolean set(String key, T value, long time, TimeUnit timeUnit) {
+		RedisTemplate<String, T> redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		Optional.ofNullable(redisTemplate).map(template -> {
+			if (time > 0) {
+				template.opsForValue().set(key, value, time, timeUnit);
+			}
+			else {
+				template.opsForValue().set(key, value);
+			}
+			return true;
+		});
+		return true;
+	}
+
+	// ================================Map=================================
+
+	/**
+	 * HashGet
+	 * @param key 键 不能为null
+	 * @param hashKey 项 不能为null
+	 * @return 值
+	 */
+	public <HK, HV> HV hget(String key, HK hashKey) {
+		RedisTemplate<String, HV> redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		return redisTemplate.<HK, HV>opsForHash().get(key, hashKey);
+	}
+
+	/**
+	 * 获取hashKey对应的所有键值
+	 * @param key 键
+	 * @return 对应的多个键值
+	 */
+	public <HK, HV> Map<HK, HV> hmget(String key) {
+		RedisTemplate<String, HV> redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		return redisTemplate.<HK, HV>opsForHash().entries(key);
+	}
+
+	/**
+	 * HashSet
+	 * @param key 键
+	 * @param map 对应多个键值
+	 * @return true 成功 false 失败
+	 */
+	public boolean hmset(String key, Map<String, Object> map) {
+		RedisTemplate<Object, Object> redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		Optional.ofNullable(redisTemplate).map(template -> {
+			template.opsForHash().putAll(key, map);
+			return true;
+		});
+		return true;
+	}
+
+	/**
+	 * HashSet 并设置时间
+	 * @param key 键
+	 * @param map 对应多个键值
+	 * @param time 时间(秒)
+	 * @return true成功 false失败
+	 */
+	public boolean hmset(String key, Map<String, Object> map, long time) {
+		RedisTemplate<Object, Object> redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		Optional.ofNullable(redisTemplate).map(template -> {
+			template.opsForHash().putAll(key, map);
+			if (time > 0) {
+				template.expire(key, time, TimeUnit.SECONDS);
+			}
+			return true;
+		});
+		return true;
+	}
+
+	/**
+	 * 向一张hash表中放入数据,如果不存在将创建
+	 * @param key 键
+	 * @param item 项
+	 * @param value 值
+	 * @return true 成功 false失败
+	 */
+	public boolean hset(String key, String item, Object value) {
+		RedisTemplate<Object, Object> redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		return Optional.ofNullable(redisTemplate).map(template -> {
+			template.opsForHash().put(key, item, value);
+			return true;
+		}).orElse(false);
+	}
+
+	/**
+	 * 向一张hash表中放入数据,如果不存在将创建
+	 * @param key 键
+	 * @param item 项
+	 * @param value 值
+	 * @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
+	 * @return true 成功 false失败
+	 */
+	public boolean hset(String key, String item, Object value, long time) {
+		RedisTemplate<Object, Object> redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		return Optional.ofNullable(redisTemplate).map(template -> {
+			template.opsForHash().put(key, item, value);
+			if (time > 0) {
+				template.expire(key, time, TimeUnit.SECONDS);
+			}
+			return true;
+		}).orElse(false);
+	}
+
+	/**
+	 * 删除hash表中的值
+	 * @param key 键 不能为null
+	 * @param item 项 可以使多个 不能为null
+	 */
+	public void hdel(String key, Object... item) {
+		RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		redisTemplate.opsForHash().delete(key, item);
+	}
+
+	/**
+	 * 判断hash表中是否有该项的值
+	 * @param key 键 不能为null
+	 * @param item 项 不能为null
+	 * @return true 存在 false不存在
+	 */
+	public boolean hHasKey(String key, String item) {
+		RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		return redisTemplate.opsForHash().hasKey(key, item);
+	}
+
+	/**
+	 * hash递增 如果不存在,就会创建一个 并把新增后的值返回
+	 * @param key 键
+	 * @param item 项
+	 * @param by 要增加几(大于0)
+	 * @return
+	 */
+	public double hincr(String key, String item, double by) {
+		RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		return redisTemplate.opsForHash().increment(key, item, by);
+	}
+
+	/**
+	 * hash递减
+	 * @param key 键
+	 * @param item 项
+	 * @param by 要减少记(小于0)
+	 * @return
+	 */
+	public double hdecr(String key, String item, double by) {
+		RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		return redisTemplate.opsForHash().increment(key, item, -by);
+	}
+
+	// ============================set=============================
+
+	/**
+	 * 根据key获取Set中的所有值
+	 * @param key 键
+	 * @return
+	 */
+	public <T> Set<T> sGet(String key) {
+		RedisTemplate<String, T> redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		return redisTemplate.opsForSet().members(key);
+	}
+
+	/**
+	 * 根据value从一个set中查询,是否存在
+	 * @param key 键
+	 * @param value 值
+	 * @return true 存在 false不存在
+	 */
+	public boolean sHasKey(String key, Object value) {
+		RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		return redisTemplate.opsForSet().isMember(key, value);
+	}
+
+	/**
+	 * 将数据放入set缓存
+	 * @param key 键
+	 * @param values 值 可以是多个
+	 * @return 成功个数
+	 */
+	public long sSet(String key, Object... values) {
+		RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		return redisTemplate.opsForSet().add(key, values);
+	}
+
+	/**
+	 * 将set数据放入缓存
+	 * @param key 键
+	 * @param time 时间(秒)
+	 * @param values 值 可以是多个
+	 * @return 成功个数
+	 */
+	public long sSetAndTime(String key, long time, Object... values) {
+		RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		Long count = redisTemplate.opsForSet().add(key, values);
+		if (time > 0) {
+			expire(key, time);
+		}
+		return count;
+	}
+
+	/**
+	 * 获取set缓存的长度
+	 * @param key 键
+	 * @return
+	 */
+	public long sGetSetSize(String key) {
+		RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		return redisTemplate.opsForSet().size(key);
+	}
+
+	/**
+	 * 移除值为value的
+	 * @param key 键
+	 * @param values 值 可以是多个
+	 * @return 移除的个数
+	 */
+	public long setRemove(String key, Object... values) {
+		RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		Long count = redisTemplate.opsForSet().remove(key, values);
+		return count;
+	}
+
+	/**
+	 * 获集合key1和集合key2的差集元素
+	 * @param key 键
+	 * @return
+	 */
+	public <T> Set<T> sDifference(String key, String otherKey) {
+		RedisTemplate<String, T> redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		return redisTemplate.opsForSet().difference(key, otherKey);
+	}
+
+	// ===============================list=================================
+
+	/**
+	 * 获取list缓存的内容
+	 * @param key 键
+	 * @param start 开始
+	 * @param end 结束 0 到 -1代表所有值
+	 * @return
+	 */
+	public <T> List<T> lGet(String key, long start, long end) {
+		RedisTemplate<String, T> redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		return redisTemplate.opsForList().range(key, start, end);
+	}
+
+	/**
+	 * 获取list缓存的长度
+	 * @param key 键
+	 * @return
+	 */
+	public long lGetListSize(String key) {
+		RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		return redisTemplate.opsForList().size(key);
+	}
+
+	/**
+	 * 通过索引 获取list中的值
+	 * @param key 键
+	 * @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
+	 * @return
+	 */
+	public Object lGetIndex(String key, long index) {
+		RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		return redisTemplate.opsForList().index(key, index);
+	}
+
+	/**
+	 * 将list放入缓存
+	 * @param key 键
+	 * @param value 值
+	 * @return
+	 */
+	public boolean lSet(String key, Object value) {
+		RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		redisTemplate.opsForList().rightPush(key, value);
+		return true;
+	}
+
+	/**
+	 * 将list放入缓存
+	 * @param key 键
+	 * @param value 值
+	 * @param time 时间(秒)
+	 * @return
+	 */
+	public boolean lSet(String key, Object value, long time) {
+		RedisTemplate<Object, Object> redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		redisTemplate.opsForList().rightPush(key, value);
+		if (time > 0) {
+			Optional.ofNullable(redisTemplate).ifPresent(template -> template.expire(key, time, TimeUnit.SECONDS));
+		}
+		return true;
+	}
+
+	/**
+	 * 将list放入缓存
+	 * @param key 键
+	 * @param value 值
+	 * @return
+	 */
+	public boolean lSet(String key, List<Object> value) {
+		RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		redisTemplate.opsForList().rightPushAll(key, value);
+		return true;
+	}
+
+	/**
+	 * 将list放入缓存
+	 * @param key 键
+	 * @param value 值
+	 * @param time 时间(秒)
+	 * @return
+	 */
+	public boolean lSet(String key, List<Object> value, long time) {
+		RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		redisTemplate.opsForList().rightPushAll(key, value);
+		if (time > 0) {
+			expire(key, time);
+		}
+		return true;
+	}
+
+	/**
+	 * 根据索引修改list中的某条数据
+	 * @param key 键
+	 * @param index 索引
+	 * @param value 值
+	 * @return /
+	 */
+	public boolean lUpdateIndex(String key, long index, Object value) {
+		RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		redisTemplate.opsForList().set(key, index, value);
+		return true;
+	}
+
+	/**
+	 * 移除N个值为value
+	 * @param key 键
+	 * @param count 移除多少个
+	 * @param value 值
+	 * @return 移除的个数
+	 */
+	public long lRemove(String key, long count, Object value) {
+		RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		return redisTemplate.opsForList().remove(key, count, value);
+	}
+
+	/**
+	 * 将zSet数据放入缓存
+	 * @param key
+	 * @param time
+	 * @param tuples
+	 * @return
+	 */
+	public long zSetAndTime(String key, long time, Set<ZSetOperations.TypedTuple<Object>> tuples) {
+		RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		Long count = redisTemplate.opsForZSet().add(key, tuples);
+		if (time > 0) {
+			expire(key, time);
+		}
+		return count;
+
+	}
+
+	/**
+	 * Sorted set:有序集合获取
+	 * @param key
+	 * @param min
+	 * @param max
+	 * @return
+	 */
+	public Set<Object> zRangeByScore(String key, double min, double max) {
+		RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
+		return zset.rangeByScore(key, min, max);
+
+	}
+
+	/**
+	 * Sorted set:有序集合获取 正序
+	 * @param key
+	 * @param start
+	 * @param end
+	 * @return
+	 */
+	public Set<Object> zRange(String key, long start, long end) {
+		RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
+		return zset.range(key, start, end);
+
+	}
+
+	/**
+	 * Sorted set:有序集合获取 倒叙
+	 * @param key
+	 * @param start
+	 * @param end
+	 * @return
+	 */
+	public Set<Object> zReverseRange(String key, long start, long end) {
+		RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
+		return zset.reverseRange(key, start, end);
+
+	}
+
+	/**
+	 * 获取zSet缓存的长度
+	 * @param key 键
+	 * @return
+	 */
+	public long zGetSetSize(String key) {
+		RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		return redisTemplate.opsForZSet().size(key);
+	}
+
+}

+ 271 - 0
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/util/RetOps.java

@@ -0,0 +1,271 @@
+
+package com.kxs.common.core.util;
+
+import cn.hutool.core.util.ObjectUtil;
+import com.kxs.common.core.constant.CommonConstants;
+
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+/**
+ *
+ * 简化{@code R<T>} 的访问操作,例子 <pre>
+ * R<Integer> result = R.ok(0);
+ * // 使用场景1: 链式操作: 断言然后消费
+ * RetOps.of(result)
+ * 		.assertCode(-1,r -> new RuntimeException("error "+r.getCode()))
+ * 		.assertDataNotEmpty(r -> new IllegalStateException("oops!"))
+ * 		.useData(System.out::println);
+ *
+ * // 使用场景2: 读取原始值(data),这里返回的是Optional
+ * RetOps.of(result).getData().orElse(null);
+ *
+ * // 使用场景3: 类型转换
+ * R<String> s = RetOps.of(result)
+ *        .assertDataNotNull(r -> new IllegalStateException("nani??"))
+ *        .map(i -> Integer.toHexString(i))
+ *        .peek();
+ * </pre>
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/27
+ */
+public class RetOps<T> {
+
+	/** 状态码为成功 */
+	public static final Predicate<R<?>> CODE_SUCCESS = r -> CommonConstants.SUCCESS == r.getStatus();
+
+	/** 数据有值 */
+	public static final Predicate<R<?>> HAS_DATA = r -> ObjectUtil.isNotEmpty(r.getData());
+
+	/** 数据有值,并且包含元素 */
+	public static final Predicate<R<?>> HAS_ELEMENT = r -> ObjectUtil.isNotEmpty(r.getData());
+
+	/** 状态码为成功并且有值 */
+	public static final Predicate<R<?>> DATA_AVAILABLE = CODE_SUCCESS.and(HAS_DATA);
+
+	private final R<T> original;
+
+	// ~ 初始化
+	// ===================================================================================================
+
+	RetOps(R<T> original) {
+		this.original = original;
+	}
+
+	public static <T> RetOps<T> of(R<T> original) {
+		return new RetOps<>(Objects.requireNonNull(original));
+	}
+
+	// ~ 杂项方法
+	// ===================================================================================================
+
+	/**
+	 * 观察原始值
+	 * @return R
+	 */
+	public R<T> peek() {
+		return original;
+	}
+
+	/**
+	 * 读取{@code code}的值
+	 * @return 返回code的值
+	 */
+	public int getCode() {
+		return original.getStatus();
+	}
+
+	/**
+	 * 读取{@code data}的值
+	 * @return 返回 Optional 包装的data
+	 */
+	public Optional<T> getData() {
+		return Optional.ofNullable(original.getData());
+	}
+
+	/**
+	 * 有条件地读取{@code data}的值
+	 * @param predicate 断言函数
+	 * @return 返回 Optional 包装的data,如果断言失败返回empty
+	 */
+	public Optional<T> getDataIf(Predicate<? super R<?>> predicate) {
+		return predicate.test(original) ? getData() : Optional.empty();
+	}
+
+	/**
+	 * 读取{@code msg}的值
+	 * @return 返回Optional包装的 msg
+	 */
+	public Optional<String> getMsg() {
+		return Optional.of(original.getMsg());
+	}
+
+	/**
+	 * 对{@code code}的值进行相等性测试
+	 * @param value 基准值
+	 * @return 返回ture表示相等
+	 */
+	public boolean codeEquals(int value) {
+		return original.getStatus() == value;
+	}
+
+	/**
+	 * 对{@code code}的值进行相等性测试
+	 * @param value 基准值
+	 * @return 返回ture表示不相等
+	 */
+	public boolean codeNotEquals(int value) {
+		return !codeEquals(value);
+	}
+
+	/**
+	 * 是否成功
+	 * @return 返回ture表示成功
+	 * @see CommonConstants#SUCCESS
+	 */
+	public boolean isSuccess() {
+		return codeEquals(CommonConstants.SUCCESS);
+	}
+
+	/**
+	 * 是否失败
+	 * @return 返回ture表示失败
+	 */
+	public boolean notSuccess() {
+		return !isSuccess();
+	}
+
+	// ~ 链式操作
+	// ===================================================================================================
+
+	/**
+	 * 断言{@code code}的值
+	 * @param expect 预期的值
+	 * @param func 用户函数,负责创建异常对象
+	 * @param <Ex> 异常类型
+	 * @return 返回实例,以便于继续进行链式操作
+	 * @throws Ex 断言失败时抛出
+	 */
+	public <Ex extends Exception> RetOps<T> assertCode(int expect, Function<? super R<T>, ? extends Ex> func)
+			throws Ex {
+		if (codeNotEquals(expect)) {
+			throw func.apply(original);
+		}
+		return this;
+	}
+
+	/**
+	 * 断言成功
+	 * @param func 用户函数,负责创建异常对象
+	 * @param <Ex> 异常类型
+	 * @return 返回实例,以便于继续进行链式操作
+	 * @throws Ex 断言失败时抛出
+	 */
+	public <Ex extends Exception> RetOps<T> assertSuccess(Function<? super R<T>, ? extends Ex> func) throws Ex {
+		return assertCode(CommonConstants.SUCCESS, func);
+	}
+
+	/**
+	 * 断言业务数据有值
+	 * @param func 用户函数,负责创建异常对象
+	 * @param <Ex> 异常类型
+	 * @return 返回实例,以便于继续进行链式操作
+	 * @throws Ex 断言失败时抛出
+	 */
+	public <Ex extends Exception> RetOps<T> assertDataNotNull(Function<? super R<T>, ? extends Ex> func) throws Ex {
+		if (Objects.isNull(original.getData())) {
+			throw func.apply(original);
+		}
+		return this;
+	}
+
+	/**
+	 * 断言业务数据有值,并且包含元素
+	 * @param func 用户函数,负责创建异常对象
+	 * @param <Ex> 异常类型
+	 * @return 返回实例,以便于继续进行链式操作
+	 * @throws Ex 断言失败时抛出
+	 */
+	public <Ex extends Exception> RetOps<T> assertDataNotEmpty(Function<? super R<T>, ? extends Ex> func) throws Ex {
+		if (ObjectUtil.isNotEmpty(original.getData())) {
+			throw func.apply(original);
+		}
+		return this;
+	}
+
+	/**
+	 * 对业务数据(data)转换
+	 * @param mapper 业务数据转换函数
+	 * @param <U> 数据类型
+	 * @return 返回新实例,以便于继续进行链式操作
+	 */
+	public <U> RetOps<U> map(Function<? super T, ? extends U> mapper) {
+		R<U> result = R.restResult(mapper.apply(original.getData()), original.getStatus(), original.getMsg());
+		return of(result);
+	}
+
+	/**
+	 * 对业务数据(data)转换
+	 * @param predicate 断言函数
+	 * @param mapper 业务数据转换函数
+	 * @param <U> 数据类型
+	 * @return 返回新实例,以便于继续进行链式操作
+	 * @see RetOps#CODE_SUCCESS
+	 * @see RetOps#HAS_DATA
+	 * @see RetOps#HAS_ELEMENT
+	 * @see RetOps#DATA_AVAILABLE
+	 */
+	public <U> RetOps<U> mapIf(Predicate<? super R<T>> predicate, Function<? super T, ? extends U> mapper) {
+		R<U> result = R.restResult(mapper.apply(original.getData()), original.getStatus(), original.getMsg());
+		return of(result);
+	}
+
+	// ~ 数据消费
+	// ===================================================================================================
+
+	/**
+	 * 消费数据,注意此方法保证数据可用
+	 * @param consumer 消费函数
+	 */
+	public void useData(Consumer<? super T> consumer) {
+		consumer.accept(original.getData());
+	}
+
+	/**
+	 * 条件消费(错误代码匹配某个值)
+	 * @param consumer 消费函数
+	 * @param codes 错误代码集合,匹配任意一个则调用消费函数
+	 */
+	public void useDataOnCode(Consumer<? super T> consumer, int... codes) {
+		useDataIf(o -> Arrays.stream(codes).filter(c -> original.getStatus() == c).findFirst().isPresent(), consumer);
+	}
+
+	/**
+	 * 条件消费(错误代码表示成功)
+	 * @param consumer 消费函数
+	 */
+	public void useDataIfSuccess(Consumer<? super T> consumer) {
+		useDataIf(CODE_SUCCESS, consumer);
+	}
+
+	/**
+	 * 条件消费
+	 * @param predicate 断言函数
+	 * @param consumer 消费函数,断言函数返回{@code true}时被调用
+	 * @see RetOps#CODE_SUCCESS
+	 * @see RetOps#HAS_DATA
+	 * @see RetOps#HAS_ELEMENT
+	 * @see RetOps#DATA_AVAILABLE
+	 */
+	public void useDataIf(Predicate<? super R<T>> predicate, Consumer<? super T> consumer) {
+		if (predicate.test(original)) {
+			consumer.accept(original.getData());
+		}
+	}
+
+}

+ 95 - 0
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/util/SpringContextHolder.java

@@ -0,0 +1,95 @@
+
+package com.kxs.common.core.util;
+
+import lombok.Getter;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.context.ApplicationEvent;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Service;
+
+/**
+ * Spring 工具类
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/27
+ */
+@Slf4j
+@Service
+@Lazy(false)
+public class SpringContextHolder implements ApplicationContextAware, DisposableBean {
+
+	/**
+	 * -- GETTER --
+	 *  取得存储在静态变量中的ApplicationContext.
+	 */
+	@Getter
+	private static ApplicationContext applicationContext = null;
+
+	/**
+	 * 实现ApplicationContextAware接口, 注入Context到静态变量中.
+	 */
+	@Override
+	public void setApplicationContext(ApplicationContext applicationContext) {
+		SpringContextHolder.applicationContext = applicationContext;
+	}
+
+	/**
+	 * 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型.
+	 */
+	@SuppressWarnings("unchecked")
+	public static <T> T getBean(String name) {
+		return (T) applicationContext.getBean(name);
+	}
+
+	/**
+	 * 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型.
+	 */
+	public static <T> T getBean(Class<T> requiredType) {
+		return applicationContext.getBean(requiredType);
+	}
+
+	/**
+	 * 通过name,以及Clazz返回指定的Bean
+	 */
+	public static <T> T getBean(String name, Class<T> clazz){
+		if(!applicationContext.containsBean(name)){
+			return null;
+		}
+		return applicationContext.getBean(name, clazz);
+	}
+
+	/**
+	 * 清除SpringContextHolder中的ApplicationContext为Null.
+	 */
+	public static void clearHolder() {
+		if (log.isDebugEnabled()) {
+			log.debug("清除SpringContextHolder中的ApplicationContext:" + applicationContext);
+		}
+		applicationContext = null;
+	}
+
+	/**
+	 * 发布事件
+	 * @param event 事件
+	 */
+	public static void publishEvent(ApplicationEvent event) {
+		if (applicationContext == null) {
+			return;
+		}
+		applicationContext.publishEvent(event);
+	}
+
+	/**
+	 * 实现DisposableBean接口, 在Context关闭时清理静态变量.
+	 */
+	@Override
+	@SneakyThrows
+	public void destroy() {
+		SpringContextHolder.clearHolder();
+	}
+
+}

+ 53 - 0
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/util/SysUtils.java

@@ -0,0 +1,53 @@
+package com.kxs.common.core.util;
+
+import cn.hutool.core.date.DatePattern;
+import cn.hutool.core.date.LocalDateTimeUtil;
+
+import java.text.SimpleDateFormat;
+import java.time.LocalDate;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 系统模块 操作
+ *
+ * @author 没秃顶的码农
+ * @date 2024-01-11
+ */
+public class SysUtils {
+
+    /**
+     * 通过用户的pidPath查找最近所属的上级
+     *
+     * @param pidPaths PID 路径
+     * @param userIds  用户 ID
+     * @return {@link Boolean}
+     */
+    public static Integer checkUserIsChildren(String[] pidPaths, List<Integer> userIds){
+        // 倒序循环数组
+        for (int i = pidPaths.length - 1; i >= 0; i--) {
+            boolean contains = userIds.contains(Integer.valueOf(pidPaths[i]));
+            if(contains){
+                return Integer.valueOf(pidPaths[i]);
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 交易流水号 ID
+     *
+     * @param id ID
+     * @return {@link String}
+     */
+    public static String generatePaymentId(Long id) {
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmssSSS");
+        String timestamp = sdf.format(new Date());
+
+        // 生成一个6位随机数作为流水号的后缀
+        int randomNum = (int) (Math.random() * 1000000);
+        String paymentId = timestamp + String.format("%06d", randomNum);
+
+        return paymentId + id;
+    }
+}

+ 110 - 0
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/util/TotalDateUtil.java

@@ -0,0 +1,110 @@
+package com.kxs.common.core.util;
+
+import cn.hutool.core.date.DatePattern;
+import cn.hutool.core.date.LocalDateTimeUtil;
+import com.google.common.collect.Lists;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * 仓库模块 日期操作
+ *
+ * @author 没秃顶的码农
+ * @date 2024-01-11
+ */
+public class TotalDateUtil {
+
+    /**
+     * 获取日期 格式 20240110
+     *
+     * @return {@link String}
+     */
+    public static Integer getDateNumber(LocalDate date){
+
+        return Integer.valueOf(LocalDateTimeUtil.format(date, DatePattern.PURE_DATE_PATTERN));
+    }
+
+    /**
+     * 获取月份 格式 202401
+     *
+     * @return {@link String}
+     */
+    public static Integer getMonthNumber(LocalDate date){
+
+        return Integer.valueOf(LocalDateTimeUtil.format(date, DatePattern.SIMPLE_MONTH_PATTERN));
+    }
+
+    /**
+     * 获取过去某几个的月份列表
+     *
+     * @param date 日期
+     * @return {@link Integer}
+     */
+    public static List<Integer> getMonthList(LocalDate date, Integer num){
+
+        List<Integer> times = Lists.newArrayList();
+        for (int i = 0; i < num; i++) {
+            // 计算过去半年每月月份
+            LocalDate currentDay = date.minusMonths(i);
+
+            times.add(getMonthNumber(currentDay));
+        }
+        return times;
+    }
+
+    /**
+     * 获取过去某几天的日列表
+     *
+     * @param date 日期
+     * @return {@link List}<{@link Integer}>
+     */
+    public static List<Integer> getDayList(LocalDate date, Integer num){
+
+        List<Integer> times = Lists.newArrayList();
+
+        for (int i = 0; i < num; i++) {
+            // 计算每一天的日期
+            LocalDate currentDay = date.minusDays(i);
+            //获取当前日的月份值
+            times.add(getDateNumber(currentDay));
+        }
+        return times;
+    }
+
+    /**
+     * 获取月份的天列表
+     * 如果为当月则只取历史的天数
+     *
+     * @param month 月
+     * @return {@link List}<{@link Integer}>
+     */
+    public static List<Integer> getMonthDayList(String month){
+
+        List<Integer> times = Lists.newArrayList();
+
+        LocalDate parse = LocalDateTimeUtil.parseDate(month, DatePattern.SIMPLE_MONTH_PATTERN);
+
+        LocalDate localDate = LocalDate.now();
+        //本月
+        if(parse.getMonth() == localDate.getMonth()){
+            LocalDate firstDayOfMonth = localDate.withDayOfMonth(1);
+            for (LocalDate date = firstDayOfMonth; !date.isAfter(localDate); date = date.plusDays(1)) {
+                times.add(Integer.valueOf(date.format(DatePattern.PURE_DATE_FORMATTER)));
+            }
+        }else{
+            int daysInMonth = parse.lengthOfMonth();
+            for (int day = 1; day <= daysInMonth; day++) {
+                LocalDate date = parse.withDayOfMonth(day);
+                times.add(Integer.valueOf(date.format(DatePattern.PURE_DATE_FORMATTER)));
+            }
+        }
+        Collections.reverse(times);
+        return times;
+    }
+
+}

+ 155 - 0
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/util/WebUtils.java

@@ -0,0 +1,155 @@
+
+package com.kxs.common.core.util;
+
+import cn.hutool.core.codec.Base64;
+import com.kxs.common.core.exception.CheckedException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.constraints.NotNull;
+import lombok.SneakyThrows;
+import lombok.experimental.UtilityClass;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.server.reactive.ServerHttpRequest;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+import org.springframework.web.method.HandlerMethod;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * Miscellaneous utilities for web applications.
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/27
+ */
+@Slf4j
+@UtilityClass
+public class WebUtils extends org.springframework.web.util.WebUtils {
+
+	private final String BASIC_ = "Basic ";
+
+	/**
+	 * 判断是否ajax请求 spring ajax 返回含有 ResponseBody 或者 RestController注解
+	 * @param handlerMethod HandlerMethod
+	 * @return 是否ajax请求
+	 */
+	public boolean isBody(HandlerMethod handlerMethod) {
+		ResponseBody responseBody = ClassUtils.getAnnotation(handlerMethod, ResponseBody.class);
+		return responseBody != null;
+	}
+
+	/**
+	 * 读取cookie
+	 * @param name cookie name
+	 * @return cookie value
+	 */
+	public String getCookieVal(String name) {
+		if (WebUtils.getRequest().isPresent()) {
+			return getCookieVal(WebUtils.getRequest().get(), name);
+		}
+		return null;
+	}
+
+	/**
+	 * 读取cookie
+	 * @param request HttpServletRequest
+	 * @param name cookie name
+	 * @return cookie value
+	 */
+	public String getCookieVal(HttpServletRequest request, String name) {
+		Cookie cookie = getCookie(request, name);
+		return cookie != null ? cookie.getValue() : null;
+	}
+
+	/**
+	 * 清除 某个指定的cookie
+	 * @param response HttpServletResponse
+	 * @param key cookie key
+	 */
+	public void removeCookie(HttpServletResponse response, String key) {
+		setCookie(response, key, null, 0);
+	}
+
+	/**
+	 * 设置cookie
+	 * @param response HttpServletResponse
+	 * @param name cookie name
+	 * @param value cookie value
+	 * @param maxAgeInSeconds maxage
+	 */
+	public void setCookie(HttpServletResponse response, String name, String value, int maxAgeInSeconds) {
+		Cookie cookie = new Cookie(name, value);
+		cookie.setPath("/");
+		cookie.setMaxAge(maxAgeInSeconds);
+		cookie.setHttpOnly(true);
+		response.addCookie(cookie);
+	}
+
+	/**
+	 * 获取 HttpServletRequest
+	 * @return {HttpServletRequest}
+	 */
+	public Optional<HttpServletRequest> getRequest() {
+		return Optional
+			.of(((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes()))
+				.getRequest());
+	}
+
+	/**
+	 * 获取 HttpServletResponse
+	 * @return {HttpServletResponse}
+	 */
+	public HttpServletResponse getResponse() {
+		return ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes()))
+			.getResponse();
+	}
+
+	/**
+	 * 从request 获取CLIENT_ID
+	 * @return
+	 */
+	@SneakyThrows
+	public String getClientId(ServerHttpRequest request) {
+		String header = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
+		return splitClient(header)[0];
+	}
+
+	@SneakyThrows
+	public String getClientId() {
+		if (WebUtils.getRequest().isPresent()) {
+			String header = WebUtils.getRequest().get().getHeader(HttpHeaders.AUTHORIZATION);
+			return splitClient(header)[0];
+		}
+		return null;
+	}
+
+	@NotNull
+	private static String[] splitClient(String header) {
+		if (header == null || !header.startsWith(BASIC_)) {
+			throw new CheckedException("请求头中client信息为空");
+		}
+		byte[] base64Token = header.substring(6).getBytes(StandardCharsets.UTF_8);
+		byte[] decoded;
+		try {
+			decoded = Base64.decode(base64Token);
+		}
+		catch (IllegalArgumentException e) {
+			throw new CheckedException("Failed to decode basic authentication token");
+		}
+
+		String token = new String(decoded, StandardCharsets.UTF_8);
+
+		int delim = token.indexOf(":");
+
+		if (delim == -1) {
+			throw new CheckedException("Invalid basic authentication token");
+		}
+		return new String[] { token.substring(0, delim), token.substring(delim + 1) };
+	}
+
+}

+ 29 - 0
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/valid/NormMonth.java

@@ -0,0 +1,29 @@
+package com.kxs.common.core.valid;
+
+import com.kxs.common.core.valid.impl.NormMonthValidator;
+import com.kxs.common.core.valid.impl.PhoneValidator;
+import jakarta.validation.Constraint;
+import jakarta.validation.Payload;
+
+import java.lang.annotation.*;
+
+/**
+ * 时间格式校验标准月份yyyy-MM
+ * @author Pota1ovO
+ */
+@Target({ElementType.FIELD,ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Constraint(validatedBy = {NormMonthValidator.class})// 指定约束处理器,也就是时间格式验证是哪个类来做校验
+public @interface NormMonth {
+
+    String pattern() default "^\\d{4}(0[1-9]|1[0-2])$";
+
+    String message() default "时间格式错误yyyyMM";
+
+    Class<?>[] groups() default { }; // groups用来指定分组,可以让校验采取不同的机制,当前默认未指定任何分组机制,默认每次都要进行校验
+
+    Class<? extends Payload>[] payload() default { };
+
+
+}

+ 29 - 0
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/valid/Phone.java

@@ -0,0 +1,29 @@
+package com.kxs.common.core.valid;
+
+import com.kxs.common.core.valid.impl.PhoneValidator;
+import jakarta.validation.Constraint;
+import jakarta.validation.Payload;
+
+import java.lang.annotation.*;
+
+/**
+ * 手机号验证正则
+ * @author mac
+ */
+@Target({ElementType.FIELD,ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Constraint(validatedBy = {PhoneValidator.class})// 指定约束处理器,也就是手机号格式验证是哪个类来做校验
+public @interface Phone {
+
+    String pattern() default "^(?:(?:\\+|00)86)?1\\d{10}$";
+
+    String message() default "手机号格式非法";
+
+    Class<?>[] groups() default { }; // groups用来指定分组,可以让校验采取不同的机制,当前默认未指定任何分组机制,默认每次都要进行校验
+
+    Class<? extends Payload>[] payload() default { };
+
+
+}
+

+ 29 - 0
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/valid/Sn.java

@@ -0,0 +1,29 @@
+package com.kxs.common.core.valid;
+
+import com.kxs.common.core.valid.impl.SnValidator;
+import jakarta.validation.Constraint;
+import jakarta.validation.Payload;
+
+import java.lang.annotation.*;
+
+/**
+ * 手机号验证正则
+ * @author mac
+ */
+@Target({ElementType.FIELD,ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Constraint(validatedBy = {SnValidator.class})// 指定约束处理器
+public @interface Sn {
+
+    String pattern() default "^[a-zA-Z0-9]+$";
+
+    String message() default "SN格式非法";
+
+    Class<?>[] groups() default { }; // groups用来指定分组,可以让校验采取不同的机制,当前默认未指定任何分组机制,默认每次都要进行校验
+
+    Class<? extends Payload>[] payload() default { };
+
+
+}
+

+ 40 - 0
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/valid/impl/NormMonthValidator.java

@@ -0,0 +1,40 @@
+package com.kxs.common.core.valid.impl;
+
+import com.kxs.common.core.valid.NormMonth;
+import com.kxs.common.core.valid.Phone;
+import jakarta.validation.ConstraintValidator;
+import jakarta.validation.ConstraintValidatorContext;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * 校验处理器:做时间格式验证的核心类
+ *
+ * @author Pota1ovO
+ * @date 2024-04-26
+ */
+public class NormMonthValidator implements ConstraintValidator<NormMonth, String> {
+
+    // 注解对象
+    private NormMonth normMonth;
+
+    // 初始化【NormMonth】对象
+    @Override
+    public void initialize(NormMonth constraintAnnotation) {
+        normMonth = constraintAnnotation;
+    }
+
+    @Override
+    public boolean isValid(String value, ConstraintValidatorContext context) {
+        if (value == null || value.isEmpty()){
+            return false;
+        }
+        // 获取格式验证表达式
+        String pattern = normMonth.pattern();
+        Pattern compile = Pattern.compile(pattern);
+        Matcher matcher = compile.matcher(value);
+        return matcher.matches();
+    }
+}
+

+ 39 - 0
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/valid/impl/PhoneValidator.java

@@ -0,0 +1,39 @@
+package com.kxs.common.core.valid.impl;
+
+import com.kxs.common.core.valid.Phone;
+import jakarta.validation.ConstraintValidator;
+import jakarta.validation.ConstraintValidatorContext;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * 校验处理器:做手机号码格式验证的核心类
+ *
+ * @author 没秃顶的码农
+ * @date 2024-04-11
+ */
+public class PhoneValidator implements ConstraintValidator<Phone, String> {
+
+    // 注解对象
+    private Phone phone;
+
+    // 初始化【Phone】对象
+    @Override
+    public void initialize(Phone constraintAnnotation) {
+        phone = constraintAnnotation;
+    }
+
+    @Override
+    public boolean isValid(String value, ConstraintValidatorContext context) {
+        if (value == null || value.isEmpty()){
+            return false;
+        }
+        // 获取【Phone】对象的手机格式验证表达式
+        String pattern = phone.pattern();
+        Pattern compile = Pattern.compile(pattern);
+        Matcher matcher = compile.matcher(value);
+        return matcher.matches();
+    }
+}
+

+ 39 - 0
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/valid/impl/SnValidator.java

@@ -0,0 +1,39 @@
+package com.kxs.common.core.valid.impl;
+
+import com.kxs.common.core.valid.Sn;
+import jakarta.validation.ConstraintValidator;
+import jakarta.validation.ConstraintValidatorContext;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * sn格式校验器
+ *
+ * @author 没秃顶的码农
+ * @date 2024-04-11
+ */
+public class SnValidator implements ConstraintValidator<Sn, String> {
+
+    // 注解对象
+    private Sn sn;
+
+    // 初始化【sn】对象
+    @Override
+    public void initialize(Sn constraintAnnotation) {
+        sn = constraintAnnotation;
+    }
+
+    @Override
+    public boolean isValid(String value, ConstraintValidatorContext context) {
+        if (value == null || value.isEmpty()){
+            return false;
+        }
+        // 获取【sn】对象的手机格式验证表达式
+        String pattern = sn.pattern();
+        Pattern compile = Pattern.compile(pattern);
+        Matcher matcher = compile.matcher(value);
+        return matcher.matches();
+    }
+}
+

+ 7 - 0
kxs-common/kxs-common-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

@@ -0,0 +1,7 @@
+com.kxs.common.core.config.TaskExecutorConfiguration
+com.kxs.common.core.config.RedisTemplateConfiguration
+com.kxs.common.core.config.RestTemplateConfiguration
+com.kxs.common.core.util.SpringContextHolder
+com.kxs.common.core.config.WebMvcConfiguration
+com.kxs.common.core.config.JacksonConfiguration
+com.kxs.common.core.exception.GlobalBizExceptionHandler

+ 14 - 0
kxs-common/kxs-common-core/src/main/resources/banner.txt

@@ -0,0 +1,14 @@
+${AnsiColor.BRIGHT_YELLOW}
+
+ ██   ██ ██     ██  ████████
+░██  ██ ░░██   ██  ██░░░░░░
+░██ ██   ░░██ ██  ░██
+░████     ░░███   ░█████████
+░██░██     ██░██  ░░░░░░░░██
+░██░░██   ██ ░░██        ░██
+░██ ░░██ ██   ░░██ ████████
+░░   ░░ ░░     ░░ ░░░░░░░░
+
+
+
+

+ 143 - 0
kxs-common/kxs-common-core/src/main/resources/logback-spring.xml

@@ -0,0 +1,143 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright (c) 2021 sky4cloud Authors. All Rights Reserved.
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<configuration debug="false" scan="false">
+	<springProperty scop="context" name="spring.application.name" source="spring.application.name" defaultValue=""/>
+	<property name="log.path" value="logs/${spring.application.name}"/>
+	<!-- 彩色日志格式 -->
+	<property name="CONSOLE_LOG_PATTERN"
+			  value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
+	<!-- 彩色日志依赖的渲染类 -->
+	<conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter"/>
+	<conversionRule conversionWord="wex"
+					converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter"/>
+	<conversionRule conversionWord="wEx"
+					converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter"/>
+	<!-- Console log output -->
+	<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
+		<encoder>
+			<pattern>${CONSOLE_LOG_PATTERN}</pattern>
+		</encoder>
+	</appender>
+
+	<!-- Log file debug output -->
+	<appender name="debug" class="ch.qos.logback.core.rolling.RollingFileAppender">
+		<file>${log.path}/debug.log</file>
+		<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+			<fileNamePattern>${log.path}/%d{yyyy-MM, aux}/debug.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
+			<maxFileSize>50MB</maxFileSize>
+			<maxHistory>30</maxHistory>
+		</rollingPolicy>
+		<encoder>
+			<pattern>${CONSOLE_LOG_PATTERN}</pattern>
+		</encoder>
+	</appender>
+
+	<!-- Log file error output -->
+	<appender name="error" class="ch.qos.logback.core.rolling.RollingFileAppender">
+		<file>${log.path}/error.log</file>
+		<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+			<fileNamePattern>${log.path}/%d{yyyy-MM}/error.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
+			<maxFileSize>50MB</maxFileSize>
+			<maxHistory>30</maxHistory>
+		</rollingPolicy>
+		<encoder>
+			<pattern>${CONSOLE_LOG_PATTERN}</pattern>
+		</encoder>
+		<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
+			<level>ERROR</level>
+		</filter>
+	</appender>
+	<appender name="aliyun" class="com.aliyun.openservices.log.logback.LoghubAppender">
+		<!-- Configure account and network  -->
+		<endpoint>cn-chengdu.log.aliyuncs.com</endpoint>
+		<!-- 填写sls key密钥 -->
+		<accessKeyId>LTAI5tLsj8MPeZgqiR3omZtk</accessKeyId>
+		<accessKeySecret>Dj710GX7H4KHEfopzboT1g57a7RGTH</accessKeySecret>
+
+		<project>kxs-logs</project>
+		<!--logStore名称 -->
+		<logStore>lhb-store</logStore>
+
+		<!-- Optional parameters -->
+		<!-- 主题名称 -->
+		<topic>${spring.application.name}</topic>
+		<!--		#单个 producer 实例能缓存的日志大小上限,默认为 100MB。-->
+		<totalSizeInBytes>104857600</totalSizeInBytes>
+		<maxBlockMs>0</maxBlockMs>
+		<!--		执行日志发送任务的线程池大小,默认为可用处理器个数。-->
+		<ioThreadCount>8</ioThreadCount>
+		<batchSizeThresholdInBytes>524288</batchSizeThresholdInBytes>
+		<batchCountThreshold>4096</batchCountThreshold>
+		<lingerMs>2000</lingerMs>
+		<retries>10</retries>
+		<baseRetryBackoffMs>100</baseRetryBackoffMs>
+		<maxRetryBackoffMs>50000</maxRetryBackoffMs>
+
+		<!-- 可选项 通过配置 encoder 的 pattern 自定义 log 的格式 -->
+		<encoder>
+			<pattern>${CONSOLE_LOG_PATTERN}</pattern>
+		</encoder>
+
+		<!--  Optional parameters -->
+		<timeFormat>yyyy-MM-dd'T'HH:mmZ</timeFormat>
+		<!-- 可选项 设置时区 -->
+		<timeZone>Asia/Shanghai</timeZone>
+		<mdcFields>
+			TraceId,#####
+		</mdcFields>
+		<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
+			<level>ERROR</level>
+			<level>INFO</level>
+		</filter>
+
+		<!-- 屏蔽transfer模块 -->
+		<logger name="com.kxs.transfer.api" level="OFF"/>
+
+	</appender>
+	<!--为了防止进程退出时,内存中的数据丢失,请加上此选项-->
+	<shutdownHook class="ch.qos.logback.core.hook.DefaultShutdownHook"/>
+	<!-- 可用来获取StatusManager中的状态 -->
+	<statusListener class="ch.qos.logback.core.status.OnConsoleStatusListener"/>
+
+	<!-- 解决debug模式下循环发送的问题 -->
+	<logger name="org.apache.http.impl.conn.Wire" level="WARN" />
+
+	<!-- 引入sls日志 -->
+	<springProfile name="dev">
+		<root level="INFO">
+			<appender-ref ref="console"/>
+		</root>
+	</springProfile>
+
+	<!-- Level: FATAL 0  ERROR 3  WARN 4  INFO 6  DEBUG 7 -->
+
+	<springProfile name="test">
+		<root level="INFO">
+			<appender-ref ref="console"/>
+			<appender-ref ref="debug"/>
+			<appender-ref ref="error"/>
+		</root>
+	</springProfile>
+	<springProfile name="prod">
+		<root level="INFO">
+			<appender-ref ref="console"/>
+			<appender-ref ref="aliyun" />
+		</root>
+	</springProfile>
+
+</configuration>

+ 104 - 0
kxs-common/kxs-common-excel/pom.xml

@@ -0,0 +1,104 @@
+<?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">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>com.kxs</groupId>
+        <artifactId>kxs-common</artifactId>
+        <version>1.1.0</version>
+    </parent>
+    <artifactId>kxs-common-excel</artifactId>
+    <description>kxs 导入导出功能</description>
+
+    <developers>
+        <developer>
+            <name>没秃顶的码农</name>
+            <email>609827073@qq.com</email>
+        </developer>
+    </developers>
+    <properties>
+        <maven.compiler.source>17</maven.compiler.source>
+        <maven.compiler.target>17</maven.compiler.target>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <easyexcel.version>3.3.2</easyexcel.version>
+        <mica.version>3.0.0</mica.version>
+        <spring.checkstyle.plugin>0.0.39</spring.checkstyle.plugin>
+        <commons-compress>1.21</commons-compress>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-validation</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>jakarta.servlet</groupId>
+            <artifactId>jakarta.servlet-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.aliyun.oss</groupId>
+            <artifactId>aliyun-sdk-oss</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.kxs</groupId>
+            <artifactId>kxs-common-core</artifactId>
+        </dependency>
+        <!-- LB 扩展 -->
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
+        </dependency>
+        <!--caffeine 替换LB 默认缓存实现-->
+        <dependency>
+            <groupId>com.github.ben-manes.caffeine</groupId>
+            <artifactId>caffeine</artifactId>
+        </dependency>
+        <!--引入AOP依赖-->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-aop</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>easyexcel</artifactId>
+            <version>${easyexcel.version}</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.apache.commons</groupId>
+                    <artifactId>commons-compress</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-compress</artifactId>
+            <version>${commons-compress}</version>
+        </dependency>
+        <!-- lombok -->
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <!-- mica-auto -->
+        <dependency>
+            <groupId>net.dreamlu</groupId>
+            <artifactId>mica-auto</artifactId>
+            <version>${mica.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <!-- test -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+</project>

+ 100 - 0
kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/ExcelHandlerConfiguration.java

@@ -0,0 +1,100 @@
+package com.kxs.common.excel;
+
+import com.alibaba.excel.converters.Converter;
+import com.aliyun.oss.OSS;
+import com.aliyun.oss.OSSClientBuilder;
+import com.kxs.common.excel.aop.ResponseExcelReturnValueHandler;
+import com.kxs.common.excel.config.ExcelConfigProperties;
+import com.kxs.common.excel.config.FileProperties;
+import com.kxs.common.excel.enhance.DefaultWriterBuilderEnhancer;
+import com.kxs.common.excel.enhance.WriterBuilderEnhancer;
+import com.kxs.common.excel.handler.ManySheetWriteHandler;
+import com.kxs.common.excel.handler.SheetWriteHandler;
+import com.kxs.common.excel.handler.SingleSheetWriteHandler;
+import com.kxs.common.excel.head.I18nHeaderCellWriteHandler;
+import com.kxs.common.excel.http.RemoteSysService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.context.MessageSource;
+import org.springframework.context.annotation.Bean;
+import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
+
+import java.util.List;
+
+/**
+ * @author 没秃顶的码农
+ * @version 1.0
+ */
+@RequiredArgsConstructor
+public class ExcelHandlerConfiguration implements InitializingBean {
+
+	private final ExcelConfigProperties configProperties;
+
+	private final FileProperties ossProperties;
+	private OSS ossClient;
+
+	private final ObjectProvider<List<Converter<?>>> converterProvider;
+
+	private final RemoteSysService remoteSysService;
+
+	/**
+	 * ExcelBuild增强
+	 * @return DefaultWriterBuilderEnhancer 默认什么也不做的增强器
+	 */
+	@Bean("ExcelWriterBuilderEnhancer")
+	@ConditionalOnMissingBean
+	public WriterBuilderEnhancer writerBuilderEnhancer() {
+		return new DefaultWriterBuilderEnhancer();
+	}
+
+	/**
+	 * 单sheet 写入处理器
+	 */
+	@Bean("ExcelSingleSheetWriteHandler")
+	@ConditionalOnMissingBean
+	public SingleSheetWriteHandler singleSheetWriteHandler() {
+		return new SingleSheetWriteHandler(configProperties, ossClient, ossProperties, converterProvider, writerBuilderEnhancer(), remoteSysService);
+	}
+
+	/**
+	 * 多sheet 写入处理器
+	 */
+	@Bean("ExcelManySheetWriteHandler")
+	@ConditionalOnMissingBean
+	public ManySheetWriteHandler manySheetWriteHandler() {
+		return new ManySheetWriteHandler(configProperties, ossClient, ossProperties, converterProvider, writerBuilderEnhancer(), remoteSysService);
+	}
+
+	/**
+	 * 返回Excel文件的 response 处理器
+	 * @param sheetWriteHandlerList 页签写入处理器集合
+	 * @return ResponseExcelReturnValueHandler
+	 */
+	@Bean("ExcelResponseExcelReturnValueHandler")
+	@ConditionalOnMissingBean
+	public ResponseExcelReturnValueHandler responseExcelReturnValueHandler(
+			List<SheetWriteHandler> sheetWriteHandlerList) {
+		return new ResponseExcelReturnValueHandler(sheetWriteHandlerList, new MappingJackson2HttpMessageConverter());
+	}
+
+	/**
+	 * excel 头的国际化处理器
+	 * @param messageSource 国际化源
+	 */
+	@Bean("ExcelI18nHeaderCellWriteHandler")
+	@ConditionalOnBean(MessageSource.class)
+	@ConditionalOnMissingBean
+	public I18nHeaderCellWriteHandler i18nHeaderCellWriteHandler(MessageSource messageSource) {
+		return new I18nHeaderCellWriteHandler(messageSource);
+	}
+
+	@Override
+	public void afterPropertiesSet() {
+		// 创建OSSClient实例。
+		ossClient = new OSSClientBuilder().
+				build(ossProperties.getEndpoint(), ossProperties.getAccessKey(), ossProperties.getSecretKey());
+	}
+}

+ 95 - 0
kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/ResponseExcelAutoConfiguration.java

@@ -0,0 +1,95 @@
+package com.kxs.common.excel;
+
+import com.kxs.common.excel.aop.DynamicNameAspect;
+import com.kxs.common.excel.aop.RequestExcelArgumentResolver;
+import com.kxs.common.excel.aop.ResponseExcelReturnValueHandler;
+import com.kxs.common.excel.config.ExcelConfigProperties;
+import com.kxs.common.excel.config.FileProperties;
+import com.kxs.common.excel.config.ExcelSystemFeignClientConfiguration;
+import com.kxs.common.excel.processor.NameProcessor;
+import com.kxs.common.excel.processor.NameSpelExpressionProcessor;
+import jakarta.annotation.PostConstruct;
+import lombok.RequiredArgsConstructor;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Import;
+import org.springframework.web.method.support.HandlerMethodArgumentResolver;
+import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
+import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author 没秃顶的码农
+ * @date 2024/07/24
+ * <p>
+ * 配置初始化
+ */
+@AutoConfiguration
+@RequiredArgsConstructor
+@Import({ExcelHandlerConfiguration.class, ExcelSystemFeignClientConfiguration.class})
+@EnableConfigurationProperties({ExcelConfigProperties.class, FileProperties.class})
+public class ResponseExcelAutoConfiguration {
+
+	private final RequestMappingHandlerAdapter requestMappingHandlerAdapter;
+
+	private final ResponseExcelReturnValueHandler responseExcelReturnValueHandler;
+
+
+
+
+	/**
+	 * SPEL 解析处理器
+	 * @return NameProcessor excel名称解析器
+	 */
+	@Bean("ExcelNameProcessor")
+	@ConditionalOnMissingBean
+	public NameProcessor nameProcessor() {
+		return new NameSpelExpressionProcessor();
+	}
+
+	/**
+	 * Excel名称解析处理切面
+	 * @param nameProcessor SPEL 解析处理器
+	 * @return DynamicNameAspect
+	 */
+	@Bean("ExcelDynamicNameAspect")
+	@ConditionalOnMissingBean
+	public DynamicNameAspect dynamicNameAspect(NameProcessor nameProcessor) {
+		return new DynamicNameAspect(nameProcessor);
+	}
+
+	/**
+	 * 追加 Excel返回值处理器 到 springmvc 中
+	 */
+	@PostConstruct
+	public void setReturnValueHandlers() {
+		List<HandlerMethodReturnValueHandler> returnValueHandlers = requestMappingHandlerAdapter
+				.getReturnValueHandlers();
+
+		List<HandlerMethodReturnValueHandler> newHandlers = new ArrayList<>();
+		newHandlers.add(responseExcelReturnValueHandler);
+		assert returnValueHandlers != null;
+		newHandlers.addAll(returnValueHandlers);
+		requestMappingHandlerAdapter.setReturnValueHandlers(newHandlers);
+	}
+
+	/**
+	 * 追加 Excel 请求处理器 到 springmvc 中
+	 */
+	@PostConstruct
+	public void setRequestExcelArgumentResolver() {
+		List<HandlerMethodArgumentResolver> argumentResolvers = requestMappingHandlerAdapter.getArgumentResolvers();
+		List<HandlerMethodArgumentResolver> resolverList = new ArrayList<>();
+		resolverList.add(new RequestExcelArgumentResolver());
+		resolverList.addAll(argumentResolvers);
+		requestMappingHandlerAdapter.setArgumentResolvers(resolverList);
+	}
+
+
+
+
+}

+ 35 - 0
kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/annotation/EnumFiledConvert.java

@@ -0,0 +1,35 @@
+package com.kxs.common.excel.annotation;
+ 
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+ 
+/**
+ * 对excel导入时,处理枚举转化
+ * @author LXQ
+ * @date 2024/3/13
+ */
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface EnumFiledConvert {
+ 
+ 
+    /**
+     * 枚举映射map  key-value,key-value,key-value,key-value
+     * @return
+     */
+    String enumMap() default "";
+ 
+    /**
+     * 枚举类导入、导出在excel中的分隔符号
+     * @return
+     */
+    String spiteChar() default ",";
+ 
+    /**
+     * 单选 or 多选
+     * @return
+     */
+    boolean single() default true;
+}

+ 10 - 0
kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/annotation/ExcelLine.java

@@ -0,0 +1,10 @@
+package com.kxs.common.excel.annotation;
+
+import java.lang.annotation.*;
+
+@Documented
+@Target({ ElementType.FIELD })
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ExcelLine {
+
+}

+ 42 - 0
kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/annotation/RequestExcel.java

@@ -0,0 +1,42 @@
+package com.kxs.common.excel.annotation;
+
+import com.kxs.common.excel.handler.DefaultAnalysisEventListener;
+import com.kxs.common.excel.handler.ListAnalysisEventListener;
+
+import java.lang.annotation.*;
+
+/**
+ * 导入excel
+ *
+ * @author 没秃顶的码农
+ * @date 2021/4/16
+ */
+@Documented
+@Target({ ElementType.PARAMETER })
+@Retention(RetentionPolicy.RUNTIME)
+public @interface RequestExcel {
+
+	/**
+	 * 前端上传字段名称 file
+	 */
+	String fileName() default "file";
+
+	/**
+	 * 读取的监听器类
+	 * @return readListener
+	 */
+	Class<? extends ListAnalysisEventListener<?>> readListener() default DefaultAnalysisEventListener.class;
+
+	/**
+	 * 是否跳过空行
+	 * @return 默认跳过
+	 */
+	boolean ignoreEmptyRow() default false;
+
+	/**
+	 * 读取的标题行数
+	 * @return
+	 */
+	int headRowNumber() default 1;
+
+}

+ 105 - 0
kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/annotation/ResponseExcel.java

@@ -0,0 +1,105 @@
+package com.kxs.common.excel.annotation;
+
+import com.alibaba.excel.converters.Converter;
+import com.alibaba.excel.support.ExcelTypeEnum;
+import com.alibaba.excel.write.handler.WriteHandler;
+import com.kxs.common.excel.head.HeadGenerator;
+
+import java.lang.annotation.*;
+
+/**
+ * `@ResponseExcel 注解`
+ *
+ * @author 没秃顶的码农
+ */
+@Documented
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ResponseExcel {
+
+	/**
+	 * 文件名称
+	 * @return string
+	 */
+	String name() default "";
+
+	/**
+	 * 文件类型 (xlsx xls)
+	 * @return string
+	 */
+	ExcelTypeEnum suffix() default ExcelTypeEnum.XLSX;
+
+	/**
+	 * 文件密码
+	 * @return password
+	 */
+	String password() default "";
+
+	/**
+	 * sheet 名称,支持多个
+	 * @return String[]
+	 */
+	Sheet[] sheets() default @Sheet(sheetName = "sheet1");
+
+	/**
+	 * 内存操作
+	 * @return
+	 */
+	boolean inMemory() default false;
+
+	/**
+	 * excel 模板
+	 * @return String
+	 */
+	String template() default "";
+
+	/**
+	 * + 包含字段
+	 * @return String[]
+	 */
+	String[] include() default {};
+
+	/**
+	 * 排除字段
+	 * @return String[]
+	 */
+	String[] exclude() default {};
+
+	/**
+	 * 拦截器,自定义样式等处理器
+	 * @return WriteHandler[]
+	 */
+	Class<? extends WriteHandler>[] writeHandler() default {};
+
+	/**
+	 * 转换器
+	 * @return Converter[]
+	 */
+	Class<? extends Converter>[] converter() default {};
+
+	/**
+	 * 自定义Excel头生成器
+	 * @return HeadGenerator
+	 */
+	Class<? extends HeadGenerator> headGenerator() default HeadGenerator.class;
+
+	/**
+	 * excel 头信息国际化
+	 * @return boolean
+	 */
+	boolean i18nHeader() default false;
+
+	/**
+	 * 填充模式
+	 * @return
+	 */
+	boolean fill() default false;
+
+	/**
+	 * 是否上传到oss
+	 *
+	 * @return boolean
+	 */
+	boolean oss() default false;
+
+}

+ 41 - 0
kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/annotation/Sheet.java

@@ -0,0 +1,41 @@
+package com.kxs.common.excel.annotation;
+
+import com.kxs.common.excel.head.HeadGenerator;
+
+import java.lang.annotation.*;
+
+/**
+ * @author LXQ
+ * @Topic Sheet
+ * @Description
+ * @date 2021/4/29 15:03
+ * @Version 1.0
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface Sheet {
+
+	int sheetNo() default -1;
+
+	/**
+	 * sheet name
+	 */
+	String sheetName();
+
+	/**
+	 * 包含字段
+	 */
+	String[] includes() default {};
+
+	/**
+	 * 排除字段
+	 */
+	String[] excludes() default {};
+
+	/**
+	 * 头生成器
+	 */
+	Class<? extends HeadGenerator> headGenerateClass() default HeadGenerator.class;
+
+}

+ 46 - 0
kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/aop/DynamicNameAspect.java

@@ -0,0 +1,46 @@
+package com.kxs.common.excel.aop;
+
+import com.kxs.common.excel.annotation.ResponseExcel;
+import com.kxs.common.excel.processor.NameProcessor;
+import lombok.RequiredArgsConstructor;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Before;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.util.StringUtils;
+import org.springframework.web.context.request.RequestAttributes;
+import org.springframework.web.context.request.RequestContextHolder;
+
+import java.time.LocalDateTime;
+import java.util.Objects;
+
+/**
+ * @author 没秃顶的码农
+ * @date 2024/07/24
+ */
+@Aspect
+@RequiredArgsConstructor
+public class DynamicNameAspect {
+
+	public static final String EXCEL_NAME_KEY = "__EXCEL_NAME_KEY__";
+
+	private final NameProcessor processor;
+
+	@Before("@annotation(excel)")
+	public void around(JoinPoint point, ResponseExcel excel) {
+		MethodSignature ms = (MethodSignature) point.getSignature();
+
+		String name = excel.name();
+		// 当配置的 excel 名称为空时,取当前时间
+		if (!StringUtils.hasText(name)) {
+			name = LocalDateTime.now().toString();
+		}
+		else {
+			name = processor.doDetermineName(point.getArgs(), ms.getMethod(), excel.name());
+		}
+
+		RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
+		Objects.requireNonNull(requestAttributes).setAttribute(EXCEL_NAME_KEY, name, RequestAttributes.SCOPE_REQUEST);
+	}
+
+}

+ 85 - 0
kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/aop/RequestExcelArgumentResolver.java

@@ -0,0 +1,85 @@
+package com.kxs.common.excel.aop;
+
+import com.alibaba.excel.EasyExcel;
+import com.kxs.common.excel.annotation.RequestExcel;
+import com.kxs.common.excel.converters.LocalDateStringConverter;
+import com.kxs.common.excel.converters.LocalDateTimeStringConverter;
+import com.kxs.common.excel.handler.ListAnalysisEventListener;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.BeanUtils;
+import org.springframework.core.MethodParameter;
+import org.springframework.core.ResolvableType;
+import org.springframework.ui.ModelMap;
+import org.springframework.validation.BindingResult;
+import org.springframework.web.bind.WebDataBinder;
+import org.springframework.web.bind.support.WebDataBinderFactory;
+import org.springframework.web.context.request.NativeWebRequest;
+import org.springframework.web.method.support.HandlerMethodArgumentResolver;
+import org.springframework.web.method.support.ModelAndViewContainer;
+import org.springframework.web.multipart.MultipartFile;
+import org.springframework.web.multipart.MultipartRequest;
+
+import java.io.InputStream;
+import java.util.List;
+
+/**
+ * 上传excel 解析注解
+ *
+ * @author 没秃顶的码农
+ * @date 2021/4/16
+ */
+@Slf4j
+public class RequestExcelArgumentResolver implements HandlerMethodArgumentResolver {
+
+	@Override
+	public boolean supportsParameter(MethodParameter parameter) {
+		return parameter.hasParameterAnnotation(RequestExcel.class);
+	}
+
+	@Override
+	@SneakyThrows(Exception.class)
+	public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer modelAndViewContainer,
+			NativeWebRequest webRequest, WebDataBinderFactory webDataBinderFactory) {
+		Class<?> parameterType = parameter.getParameterType();
+		if (!parameterType.isAssignableFrom(List.class)) {
+			throw new IllegalArgumentException(
+					"Excel upload request resolver error, @RequestExcel parameter is not List " + parameterType);
+		}
+
+		// 处理自定义 readListener
+		RequestExcel requestExcel = parameter.getParameterAnnotation(RequestExcel.class);
+		assert requestExcel != null;
+		Class<? extends ListAnalysisEventListener<?>> readListenerClass = requestExcel.readListener();
+		ListAnalysisEventListener<?> readListener = BeanUtils.instantiateClass(readListenerClass);
+		// 获取请求文件流
+		HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
+		assert request != null;
+		InputStream inputStream;
+		if (request instanceof MultipartRequest) {
+			MultipartFile file = ((MultipartRequest) request).getFile(requestExcel.fileName());
+			assert file != null;
+			inputStream = file.getInputStream();
+		}
+		else {
+			inputStream = request.getInputStream();
+		}
+
+		// 获取目标类型
+		Class<?> excelModelClass = ResolvableType.forMethodParameter(parameter).getGeneric(0).resolve();
+
+		// 这里需要指定读用哪个 class 去读,然后读取第一个 sheet 文件流会自动关闭
+		EasyExcel.read(inputStream, excelModelClass, readListener).registerConverter(LocalDateStringConverter.INSTANCE)
+				.registerConverter(LocalDateTimeStringConverter.INSTANCE).ignoreEmptyRow(requestExcel.ignoreEmptyRow())
+				.sheet().headRowNumber(requestExcel.headRowNumber()).doRead();
+
+		// 校验失败的数据处理 交给 BindResult
+		WebDataBinder dataBinder = webDataBinderFactory.createBinder(webRequest, readListener.getErrors(), "excel");
+		ModelMap model = modelAndViewContainer.getModel();
+		model.put(BindingResult.MODEL_KEY_PREFIX + "excel", dataBinder.getBindingResult());
+
+		return readListener.getList();
+	}
+
+}

+ 84 - 0
kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/aop/ResponseExcelReturnValueHandler.java

@@ -0,0 +1,84 @@
+package com.kxs.common.excel.aop;
+
+import com.kxs.common.core.util.R;
+import com.kxs.common.excel.annotation.ResponseExcel;
+import com.kxs.common.excel.handler.SheetWriteHandler;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.core.MethodParameter;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
+import org.springframework.http.server.ServletServerHttpResponse;
+import org.springframework.util.Assert;
+import org.springframework.web.context.request.NativeWebRequest;
+import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
+import org.springframework.web.method.support.ModelAndViewContainer;
+
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * 处理@ResponseExcel 返回值
+ *
+ * @author 没秃顶的码农
+ */
+@Slf4j
+@RequiredArgsConstructor
+public class ResponseExcelReturnValueHandler implements HandlerMethodReturnValueHandler {
+
+	private final List<SheetWriteHandler> sheetWriteHandlerList;
+
+	private final MappingJackson2HttpMessageConverter errorHttpResponseConverter;
+
+
+	/**
+	 * 只处理@ResponseExcel 声明的方法
+	 * @param parameter 方法签名
+	 * @return 是否处理
+	 */
+	@Override
+	public boolean supportsReturnType(MethodParameter parameter) {
+		return parameter.getMethodAnnotation(ResponseExcel.class) != null;
+	}
+
+	/**
+	 * 处理逻辑
+	 * @param o 返回参数
+	 * @param parameter 方法签名
+	 * @param mavContainer 上下文容器
+	 * @param nativeWebRequest 上下文
+	 */
+	@Override
+	public void handleReturnValue(Object o, MethodParameter parameter, ModelAndViewContainer mavContainer,
+			NativeWebRequest nativeWebRequest) {
+		/* check */
+		HttpServletResponse response = nativeWebRequest.getNativeResponse(HttpServletResponse.class);
+		Assert.state(response != null, "No HttpServletResponse");
+		ResponseExcel responseExcel = parameter.getMethodAnnotation(ResponseExcel.class);
+		Assert.state(responseExcel != null, "No @ResponseExcel");
+		mavContainer.setRequestHandled(true);
+
+		if(responseExcel.oss()){
+			sheetWriteHandlerList.stream().filter(handler -> handler.support(o)).findFirst()
+					.ifPresent(handler -> handler.exportOss(o, response, responseExcel));
+			sendResponse(response);
+		}else{
+			sheetWriteHandlerList.stream().filter(handler -> handler.support(o)).findFirst()
+					.ifPresent(handler -> handler.export(o, response, responseExcel));
+		}
+	}
+
+	private void sendResponse(HttpServletResponse response) {
+		ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
+		httpResponse.setStatusCode(HttpStatus.OK);
+
+        try {
+            errorHttpResponseConverter.write(R.ok(), MediaType.APPLICATION_JSON, httpResponse);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+}

+ 21 - 0
kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/config/ExcelConfigProperties.java

@@ -0,0 +1,21 @@
+package com.kxs.common.excel.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * @author 没秃顶的码农
+ * @date 2024/07/24
+ */
+@Data
+@ConfigurationProperties(prefix = ExcelConfigProperties.PREFIX)
+public class ExcelConfigProperties {
+
+	static final String PREFIX = "excel";
+
+	/**
+	 * 模板路径
+	 */
+	private String templatePath = "excel";
+
+}

+ 70 - 0
kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/config/ExcelSystemFeignClientConfiguration.java

@@ -0,0 +1,70 @@
+package com.kxs.common.excel.config;
+
+import cn.hutool.core.text.CharSequenceUtil;
+import com.kxs.common.core.constant.ServiceNameConstants;
+import com.kxs.common.excel.http.RemoteSysService;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.cloud.client.loadbalancer.reactive.ReactorLoadBalancerExchangeFilterFunction;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpHeaders;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+import org.springframework.web.reactive.function.client.ClientRequest;
+import org.springframework.web.reactive.function.client.WebClient;
+import org.springframework.web.reactive.function.client.support.WebClientAdapter;
+import org.springframework.web.service.invoker.HttpServiceProxyFactory;
+
+
+/**
+ * 远程system 客户端配置
+ *
+ * @author 没秃顶的码农
+ * @date 2024-07-26
+ */
+@Configuration
+@RequiredArgsConstructor
+public class ExcelSystemFeignClientConfiguration {
+
+	/**
+	 * 负载均衡器
+	 */
+	private final ReactorLoadBalancerExchangeFilterFunction reactorLoadBalancerExchangeFilterFunction;
+
+	private static final String SERVICE_NAME = ServiceNameConstants.CLIENT_NAME + ServiceNameConstants.SYSTEM_SERVICE;
+
+	@Bean("ExcelSystemFeignClient")
+	public WebClient oauthRequestInterceptor() {
+
+		return WebClient.builder()
+			// 给请求添加过滤器,添加自定义的认证头
+			.filter((request, next) -> {
+				ClientRequest.Builder filtered = ClientRequest.from(request);
+
+				ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder
+					.getRequestAttributes();
+				// 不是web请求不传递token
+				if (requestAttributes != null) {
+					HttpServletRequest httpServletRequest = requestAttributes.getRequest();
+					String token = httpServletRequest.getHeader(HttpHeaders.AUTHORIZATION);
+					if (!CharSequenceUtil.isBlank(token)) {
+						// 传递token
+						filtered.header(HttpHeaders.AUTHORIZATION, token);
+					}
+				}
+				return next.exchange(filtered.build());
+			}).filter(reactorLoadBalancerExchangeFilterFunction)
+			.baseUrl(SERVICE_NAME)
+			.build();
+	}
+
+
+	@Bean("ExcelRemoteSysService")
+	RemoteSysService remoteSysService(@Qualifier("ExcelSystemFeignClient") WebClient client) {
+		HttpServiceProxyFactory factory = HttpServiceProxyFactory.builder(WebClientAdapter.forClient(client)).blockTimeout(ServiceNameConstants.REQ_TIMEOUT).build();
+		return factory.createClient(RemoteSysService.class);
+	}
+
+}

+ 42 - 0
kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/config/FileProperties.java

@@ -0,0 +1,42 @@
+
+package com.kxs.common.excel.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * 文件 配置信息
+ *
+ * @author 没秃顶的码农
+ * @date 2024-04-18
+ */
+@Data
+@ConfigurationProperties(prefix = "file.oss")
+public class FileProperties {
+
+	/**
+	 * 默认的存储桶名称
+	 */
+	private String bucketName;
+
+	/**
+	 * 文件前缀
+	 */
+	private String filePrefix;
+
+	/**
+	 * 对象存储服务的URL
+	 */
+	private String endpoint = "https://oss-cn-hangzhou.aliyuncs.com";
+
+	/**
+	 * Access key
+	 */
+	private String accessKey = "test";
+
+	/**
+	 * Secret key
+	 */
+	private String secretKey = "test";
+
+}

+ 130 - 0
kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/converters/EasyExcelConvert.java

@@ -0,0 +1,130 @@
+package com.kxs.common.excel.converters;
+
+import com.alibaba.excel.converters.Converter;
+import com.alibaba.excel.enums.CellDataTypeEnum;
+import com.alibaba.excel.metadata.GlobalConfiguration;
+import com.alibaba.excel.metadata.data.ReadCellData;
+import com.alibaba.excel.metadata.data.WriteCellData;
+import com.alibaba.excel.metadata.property.ExcelContentProperty;
+import com.kxs.common.excel.annotation.EnumFiledConvert;
+import org.springframework.util.StringUtils;
+
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * excel导入导出针对枚举类型的转换器
+ *
+ * @author 没秃顶的码农
+ * @date 2024-07-03
+ */
+public class EasyExcelConvert implements Converter<Object> {
+    /**
+     * 枚举列表
+     */
+    private final Map<String, String> enumMap = new HashMap<>();
+ 
+ 
+    /**
+     * excel转化后的类型
+     *
+     */
+    @Override
+    public Class<?> supportJavaTypeKey() {
+        return Object.class;
+    }
+ 
+    /**
+     * excel中的数据类型,统一设置字符串
+     *
+     */
+    @Override
+    public CellDataTypeEnum supportExcelTypeKey() {
+        return CellDataTypeEnum.STRING;
+    }
+ 
+    /**
+     * 导入转换
+     * @param cellData            当前单元格对象
+     * @param contentProperty     当前单元格属性
+     * @param globalConfiguration --
+     */
+    @Override
+    public Object convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
+        String cellMsg = cellData.getStringValue();
+        Field field = contentProperty.getField();
+        EnumFiledConvert enumFiledConvert = field.getAnnotation(EnumFiledConvert.class);
+        if (enumFiledConvert == null) {
+            return null;
+        }
+        String enumStr = enumFiledConvert.enumMap();
+        // 解析枚举映射关系
+        getEnumMap(enumStr, true);
+        // 是否为单选
+        boolean single = enumFiledConvert.single();
+        // 如果是单选,默认Java属性为integer
+        if (single) {
+            String res = enumMap.get(cellMsg);
+            return StringUtils.hasText(res) ? Integer.valueOf(res) : null;
+        } else {
+            // 多选分隔符
+            String spiteChar = enumFiledConvert.spiteChar();
+            // 多选枚举,默认Java属性为字符串,格式为 key1,key2,key3
+            List<String> strStr = Arrays.asList(cellMsg.split(spiteChar)).stream().map(s -> String.valueOf(enumMap.get(s))).collect(Collectors.toList());
+            return String.join(spiteChar, strStr);
+        }
+    }
+ 
+    /**
+     * 导出转化
+     * @param value               当前值
+     * @param contentProperty     当前单元格属性
+     * @param globalConfiguration -
+     */
+    @Override
+    public WriteCellData<?> convertToExcelData(Object value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {
+        Field field = contentProperty.getField();
+        EnumFiledConvert enumFiledConvert = field.getAnnotation(EnumFiledConvert.class);
+        if (enumFiledConvert == null) {
+            return new WriteCellData<>();
+        }
+        // 解析枚举字符串
+        String enumStr = enumFiledConvert.enumMap();
+        getEnumMap(enumStr, false);
+        // 是否为单选
+        boolean single = enumFiledConvert.single();
+        // 如果是单选,默认Java属性为integer
+        if (single) {
+            return new WriteCellData<>(enumMap.getOrDefault(String.valueOf(value), ""));
+        } else {
+            // 多选分隔符
+            String spiteChar = enumFiledConvert.spiteChar();
+            // 多选枚举,默认Java属性为字符串,格式为 key1,key2,key3
+            List<String> strStr = Arrays.stream(String.valueOf(value).split(spiteChar)).map(s -> String.valueOf(enumMap.get(s))).collect(Collectors.toList());
+            String str = String.join(spiteChar, strStr);
+            return new WriteCellData<>(str);
+        }
+    }
+ 
+    /**
+     * 根据注解配置的枚举映射字符串进行解析到map中
+     * @param readOrWrite 读excel 、 写excel
+     */
+    private void getEnumMap(String mapStr, boolean readOrWrite) {
+        String[] enumS = mapStr.split(",");
+        for (String anEnum : enumS) {
+            String[] data = anEnum.split("-");
+            if (readOrWrite) {
+                // 读excel excel中的数据都是value,转换成key
+                enumMap.put(data[1], data[0]);
+            } else {
+                // 写excel  Java中的数据都是key,转换成value
+                enumMap.put(data[0], data[1]);
+            }
+        }
+    }
+}

+ 75 - 0
kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/converters/ImportAsyncInfo.java

@@ -0,0 +1,75 @@
+package com.kxs.common.excel.converters;
+
+import com.kxs.common.excel.vo.ErrorMessage;
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+/**
+ * 用于导入的进度信息
+ *
+ * @author 没秃顶的码农
+ * @date 2024-07-26
+ */
+@Data
+public class ImportAsyncInfo {
+
+
+    //提示信息或 异常信息
+    List<ErrorMessage> errorMessageList = new ArrayList<>();
+
+    //数据总数
+    private Integer totality = 0;
+
+    //已处理的数据条数
+    private Integer doneSum = 0;
+
+    //失败的数据条数
+    private Integer errorSum = 0;
+
+    //成功的数据条数
+    private Integer sucSum = 0;
+
+    //导入是否结束
+    public  Boolean isEnd= false;
+
+
+    /**
+     * uuid对应的进度 已处理的数据条数+1
+     */
+    public void doneAdd(int num){
+
+        doneSum = doneSum + num;
+    }
+
+    /**
+     * uuid对应的进度 失败的数据条数+1
+     */
+    public void errAdd(int num){
+
+        errorSum = errorSum + num;
+    }
+
+    /**
+     * uuid对应的进度 成功的数据条数+1
+     */
+    public void sucAdd(int num){
+
+        sucSum = sucSum + num;
+    }
+
+    /**
+     * 错误列表添加
+     *
+     * @param errorMessage 错误信息
+     */
+    public void errListAdd(ErrorMessage errorMessage){
+
+      errorMessageList.add(errorMessage);
+    }
+
+
+
+}

+ 61 - 0
kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/converters/LocalDateStringConverter.java

@@ -0,0 +1,61 @@
+package com.kxs.common.excel.converters;
+
+import com.alibaba.excel.converters.Converter;
+import com.alibaba.excel.enums.CellDataTypeEnum;
+import com.alibaba.excel.metadata.GlobalConfiguration;
+import com.alibaba.excel.metadata.data.ReadCellData;
+import com.alibaba.excel.metadata.data.WriteCellData;
+import com.alibaba.excel.metadata.property.ExcelContentProperty;
+
+import java.text.ParseException;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+
+/**
+ * LocalDate and string converter
+ *
+ */
+public enum LocalDateStringConverter implements Converter<LocalDate> {
+
+	/**
+	 * 实例
+	 */
+	INSTANCE;
+
+	@Override
+	public Class supportJavaTypeKey() {
+		return LocalDate.class;
+	}
+
+	@Override
+	public CellDataTypeEnum supportExcelTypeKey() {
+		return CellDataTypeEnum.STRING;
+	}
+
+	@Override
+	public LocalDate convertToJavaData(ReadCellData cellData, ExcelContentProperty contentProperty,
+			GlobalConfiguration globalConfiguration) throws ParseException {
+		if (contentProperty == null || contentProperty.getDateTimeFormatProperty() == null) {
+			return LocalDate.parse(cellData.getStringValue());
+		}
+		else {
+			DateTimeFormatter formatter = DateTimeFormatter
+					.ofPattern(contentProperty.getDateTimeFormatProperty().getFormat());
+			return LocalDate.parse(cellData.getStringValue(), formatter);
+		}
+	}
+
+	@Override
+	public WriteCellData<String> convertToExcelData(LocalDate value, ExcelContentProperty contentProperty,
+			GlobalConfiguration globalConfiguration) {
+		DateTimeFormatter formatter;
+		if (contentProperty == null || contentProperty.getDateTimeFormatProperty() == null) {
+			formatter = DateTimeFormatter.ISO_LOCAL_DATE;
+		}
+		else {
+			formatter = DateTimeFormatter.ofPattern(contentProperty.getDateTimeFormatProperty().getFormat());
+		}
+		return new WriteCellData<>(value.format(formatter));
+	}
+
+}

+ 93 - 0
kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/converters/LocalDateTimeStringConverter.java

@@ -0,0 +1,93 @@
+package com.kxs.common.excel.converters;
+
+import com.alibaba.excel.converters.Converter;
+import com.alibaba.excel.enums.CellDataTypeEnum;
+import com.alibaba.excel.metadata.GlobalConfiguration;
+import com.alibaba.excel.metadata.data.ReadCellData;
+import com.alibaba.excel.metadata.data.WriteCellData;
+import com.alibaba.excel.metadata.property.ExcelContentProperty;
+import com.alibaba.excel.util.DateUtils;
+
+import java.text.ParseException;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+
+/**
+ * LocalDateTime and string converter
+ *
+ */
+public enum LocalDateTimeStringConverter implements Converter<LocalDateTime> {
+
+	/**
+	 * 实例
+	 */
+	INSTANCE;
+
+	private static final String MINUS = "-";
+
+	@Override
+	public Class supportJavaTypeKey() {
+		return LocalDateTime.class;
+	}
+
+	@Override
+	public CellDataTypeEnum supportExcelTypeKey() {
+		return CellDataTypeEnum.STRING;
+	}
+
+	@Override
+	public LocalDateTime convertToJavaData(ReadCellData cellData, ExcelContentProperty contentProperty,
+			GlobalConfiguration globalConfiguration) throws ParseException {
+		String stringValue = cellData.getStringValue();
+		String pattern;
+		if (contentProperty == null || contentProperty.getDateTimeFormatProperty() == null) {
+			pattern = switchDateFormat(stringValue);
+		}
+		else {
+			pattern = contentProperty.getDateTimeFormatProperty().getFormat();
+		}
+		DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);
+		return LocalDateTime.parse(cellData.getStringValue(), formatter);
+	}
+
+	@Override
+	public WriteCellData<String> convertToExcelData(LocalDateTime value, ExcelContentProperty contentProperty,
+			GlobalConfiguration globalConfiguration) {
+		String pattern;
+		if (contentProperty == null || contentProperty.getDateTimeFormatProperty() == null) {
+			pattern = DateUtils.DATE_FORMAT_19;
+		}
+		else {
+			pattern = contentProperty.getDateTimeFormatProperty().getFormat();
+		}
+		DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);
+		return new WriteCellData<>(value.format(formatter));
+	}
+
+	/**
+	 * switch date format
+	 * @param dateString dateString
+	 * @return pattern
+	 */
+	private static String switchDateFormat(String dateString) {
+		int length = dateString.length();
+		switch (length) {
+		case 19:
+			if (dateString.contains(MINUS)) {
+				return DateUtils.DATE_FORMAT_19;
+			}
+			else {
+				return DateUtils.DATE_FORMAT_19_FORWARD_SLASH;
+			}
+		case 17:
+			return DateUtils.DATE_FORMAT_17;
+		case 14:
+			return DateUtils.DATE_FORMAT_14;
+		case 10:
+			return DateUtils.DATE_FORMAT_10;
+		default:
+			throw new IllegalArgumentException("can not find date format for:" + dateString);
+		}
+	}
+
+}

+ 47 - 0
kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/enhance/DefaultWriterBuilderEnhancer.java

@@ -0,0 +1,47 @@
+package com.kxs.common.excel.enhance;
+
+import com.alibaba.excel.write.builder.ExcelWriterBuilder;
+import com.alibaba.excel.write.builder.ExcelWriterSheetBuilder;
+import com.kxs.common.excel.annotation.ResponseExcel;
+import com.kxs.common.excel.head.HeadGenerator;
+import jakarta.servlet.http.HttpServletResponse;
+
+/**
+ * @author 没秃顶的码农 2020/12/18
+ * @version 1.0
+ */
+public class DefaultWriterBuilderEnhancer implements WriterBuilderEnhancer {
+
+	/**
+	 * ExcelWriterBuilder 增强
+	 * @param writerBuilder ExcelWriterBuilder
+	 * @param response HttpServletResponse
+	 * @param responseExcel ResponseExcel
+	 * @param templatePath 模板地址
+	 * @return ExcelWriterBuilder
+	 */
+	@Override
+	public ExcelWriterBuilder enhanceExcel(ExcelWriterBuilder writerBuilder, HttpServletResponse response,
+			ResponseExcel responseExcel, String templatePath) {
+		// doNothing
+		return writerBuilder;
+	}
+
+	/**
+	 * ExcelWriterSheetBuilder 增强
+	 * @param writerSheetBuilder ExcelWriterSheetBuilder
+	 * @param sheetNo sheet角标
+	 * @param sheetName sheet名,有模板时为空
+	 * @param dataClass 当前写入的数据所属类
+	 * @param template 模板文件
+	 * @param headEnhancerClass 当前指定的自定义头处理器
+	 * @return ExcelWriterSheetBuilder
+	 */
+	@Override
+	public ExcelWriterSheetBuilder enhanceSheet(ExcelWriterSheetBuilder writerSheetBuilder, Integer sheetNo,
+			String sheetName, Class<?> dataClass, String template, Class<? extends HeadGenerator> headEnhancerClass) {
+		// doNothing
+		return writerSheetBuilder;
+	}
+
+}

+ 41 - 0
kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/enhance/WriterBuilderEnhancer.java

@@ -0,0 +1,41 @@
+package com.kxs.common.excel.enhance;
+
+import com.alibaba.excel.write.builder.ExcelWriterBuilder;
+import com.alibaba.excel.write.builder.ExcelWriterSheetBuilder;
+import com.kxs.common.excel.annotation.ResponseExcel;
+import com.kxs.common.excel.head.HeadGenerator;
+import jakarta.servlet.http.HttpServletResponse;
+
+/**
+ * ExcelWriterBuilder 增强
+ *
+ * @author 没秃顶的码农 2020/12/18
+ * @version 1.0
+ */
+public interface WriterBuilderEnhancer {
+
+	/**
+	 * ExcelWriterBuilder 增强
+	 * @param writerBuilder ExcelWriterBuilder
+	 * @param response HttpServletResponse
+	 * @param responseExcel ResponseExcel
+	 * @param templatePath 模板地址
+	 * @return ExcelWriterBuilder
+	 */
+	ExcelWriterBuilder enhanceExcel(ExcelWriterBuilder writerBuilder, HttpServletResponse response,
+			ResponseExcel responseExcel, String templatePath);
+
+	/**
+	 * ExcelWriterSheetBuilder 增强
+	 * @param writerSheetBuilder ExcelWriterSheetBuilder
+	 * @param sheetNo sheet角标
+	 * @param sheetName sheet名,有模板时为空
+	 * @param dataClass 当前写入的数据所属类
+	 * @param template 模板文件
+	 * @param headEnhancerClass 当前指定的自定义头处理器
+	 * @return ExcelWriterSheetBuilder
+	 */
+	ExcelWriterSheetBuilder enhanceSheet(ExcelWriterSheetBuilder writerSheetBuilder, Integer sheetNo, String sheetName,
+			Class<?> dataClass, String template, Class<? extends HeadGenerator> headEnhancerClass);
+
+}

+ 315 - 0
kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/handler/AbstractSheetWriteHandler.java

@@ -0,0 +1,315 @@
+package com.kxs.common.excel.handler;
+
+import cn.hutool.core.util.StrUtil;
+import com.alibaba.excel.EasyExcel;
+import com.alibaba.excel.ExcelWriter;
+import com.alibaba.excel.converters.Converter;
+import com.alibaba.excel.write.builder.ExcelWriterBuilder;
+import com.alibaba.excel.write.builder.ExcelWriterSheetBuilder;
+import com.alibaba.excel.write.handler.WriteHandler;
+import com.alibaba.excel.write.metadata.WriteSheet;
+import com.aliyun.oss.OSS;
+import com.aliyun.oss.model.PutObjectResult;
+import com.kxs.common.core.constant.SecurityConstants;
+import com.kxs.common.excel.annotation.ResponseExcel;
+import com.kxs.common.excel.annotation.Sheet;
+import com.kxs.common.excel.aop.DynamicNameAspect;
+import com.kxs.common.excel.config.ExcelConfigProperties;
+import com.kxs.common.excel.config.FileProperties;
+import com.kxs.common.excel.converters.LocalDateStringConverter;
+import com.kxs.common.excel.converters.LocalDateTimeStringConverter;
+import com.kxs.common.excel.enhance.WriterBuilderEnhancer;
+import com.kxs.common.excel.head.HeadGenerator;
+import com.kxs.common.excel.head.HeadMeta;
+import com.kxs.common.excel.head.I18nHeaderCellWriteHandler;
+import com.kxs.common.excel.http.RemoteSysService;
+import com.kxs.common.excel.kit.ExcelException;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.Setter;
+import lombok.SneakyThrows;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.MediaTypeFactory;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+import org.springframework.web.context.request.RequestAttributes;
+import org.springframework.web.context.request.RequestContextHolder;
+
+import java.io.*;
+import java.lang.reflect.Modifier;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.text.SimpleDateFormat;
+import java.util.*;
+
+/**
+ * @author 没秃顶的码农
+ * @date 2020/3/31
+ */
+@RequiredArgsConstructor
+public abstract class AbstractSheetWriteHandler implements SheetWriteHandler, ApplicationContextAware {
+
+	private final ExcelConfigProperties configProperties;
+
+	private final OSS ossClient;
+
+	private final FileProperties fileProperties;
+
+	private final ObjectProvider<List<Converter<?>>> converterProvider;
+
+	private final WriterBuilderEnhancer excelWriterBuilderEnhance;
+
+	private final RemoteSysService remoteSysService;
+
+	private ApplicationContext applicationContext;
+
+
+
+	@Getter
+	@Setter
+	@Autowired(required = false)
+	private I18nHeaderCellWriteHandler i18nHeaderCellWriteHandler;
+
+	@Override
+	public void check(ResponseExcel responseExcel) {
+		if (responseExcel.sheets().length == 0) {
+			throw new ExcelException("@ResponseExcel sheet 配置不合法");
+		}
+	}
+
+	@Override
+	@SneakyThrows(Exception.class)
+	public void export(Object o, HttpServletResponse response, ResponseExcel responseExcel) {
+		check(responseExcel);
+		RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
+		String name = (String) Objects.requireNonNull(requestAttributes).getAttribute(DynamicNameAspect.EXCEL_NAME_KEY,
+				RequestAttributes.SCOPE_REQUEST);
+		if (name == null) {
+			name = UUID.randomUUID().toString();
+		}
+		String fileName = String.format("%s%s", URLEncoder.encode(name, StandardCharsets.UTF_8), responseExcel.suffix().getValue());
+
+		// 根据实际的文件类型找到对应的 contentType
+		String contentType = MediaTypeFactory.getMediaType(fileName).map(MediaType::toString)
+				.orElse("application/vnd.ms-excel");
+		response.setContentType(contentType);
+		response.setCharacterEncoding("utf-8");
+		response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename*=utf-8''" + fileName);
+		write(o, response, responseExcel);
+
+	}
+
+	@Async
+	@Override
+	@SneakyThrows(Exception.class)
+	public void exportOss(Object o, HttpServletResponse response, ResponseExcel responseExcel){
+		check(responseExcel);
+		String name = responseExcel.name();
+		if (StrUtil.isBlank(name)) {
+			name = UUID.randomUUID().toString();
+		}
+		SimpleDateFormat formatter = new SimpleDateFormat("yyMMddHHmm");
+		String times = formatter.format(new Date());
+		String fileName = fileProperties.getFilePrefix() + "/excel/" + name + times + responseExcel.suffix().getValue();
+
+		FileInputStream fileInputStream = writeToOss(o, response, responseExcel);
+		ossClient.putObject(fileProperties.getBucketName(), fileName, fileInputStream);
+		//save to sysFile
+		HashMap<String, Object> param = new HashMap<>();
+		param.put("title", name + times);
+		param.put("fileName", fileName);
+		param.put("bucketName", fileProperties.getBucketName());
+		param.put("type", responseExcel.suffix().getValue().replace(StrUtil.DOT, ""));
+		remoteSysService.saveFile(param, SecurityConstants.FROM_IN);
+		fileInputStream.close();
+	}
+
+	/**
+	 * 通用的获取ExcelWriter方法
+	 * @param response HttpServletResponse
+	 * @param responseExcel ResponseExcel注解
+	 * @return ExcelWriter
+	 */
+	@SneakyThrows(IOException.class)
+	public ExcelWriter getExcelWriter(HttpServletResponse response, ResponseExcel responseExcel) {
+		ExcelWriterBuilder writerBuilder = EasyExcel.write(response.getOutputStream())
+				.registerConverter(LocalDateStringConverter.INSTANCE)
+				.registerConverter(LocalDateTimeStringConverter.INSTANCE).autoCloseStream(true)
+				.excelType(responseExcel.suffix()).inMemory(responseExcel.inMemory());
+
+		if (StringUtils.hasText(responseExcel.password())) {
+			writerBuilder.password(responseExcel.password());
+		}
+
+		if (responseExcel.include().length != 0) {
+			writerBuilder.includeColumnFieldNames(Arrays.asList(responseExcel.include()));
+		}
+
+		if (responseExcel.exclude().length != 0) {
+			writerBuilder.excludeColumnFieldNames(Arrays.asList(responseExcel.exclude()));
+		}
+
+		for (Class<? extends WriteHandler> clazz : responseExcel.writeHandler()) {
+			writerBuilder.registerWriteHandler(BeanUtils.instantiateClass(clazz));
+		}
+
+		// 开启国际化头信息处理
+		if (responseExcel.i18nHeader() && i18nHeaderCellWriteHandler != null) {
+			writerBuilder.registerWriteHandler(i18nHeaderCellWriteHandler);
+		}
+
+		// 自定义注入的转换器
+		registerCustomConverter(writerBuilder);
+
+		for (Class<? extends Converter> clazz : responseExcel.converter()) {
+			writerBuilder.registerConverter(BeanUtils.instantiateClass(clazz));
+		}
+
+		String templatePath = configProperties.getTemplatePath();
+		if (StringUtils.hasText(responseExcel.template())) {
+			ClassPathResource classPathResource = new ClassPathResource(
+					templatePath + File.separator + responseExcel.template());
+			InputStream inputStream = classPathResource.getInputStream();
+			writerBuilder.withTemplate(inputStream);
+		}
+
+		writerBuilder = excelWriterBuilderEnhance.enhanceExcel(writerBuilder, response, responseExcel, templatePath);
+
+		return writerBuilder.build();
+	}
+	@SneakyThrows(IOException.class)
+	public ExcelWriter getExcelWriterFile(FileOutputStream fileOutputStream, ResponseExcel responseExcel) {
+		ExcelWriterBuilder writerBuilder = EasyExcel.write(fileOutputStream)
+				.registerConverter(LocalDateStringConverter.INSTANCE)
+				.registerConverter(LocalDateTimeStringConverter.INSTANCE).autoCloseStream(true)
+				.excelType(responseExcel.suffix()).inMemory(responseExcel.inMemory());
+
+		if (StringUtils.hasText(responseExcel.password())) {
+			writerBuilder.password(responseExcel.password());
+		}
+
+		if (responseExcel.include().length != 0) {
+			writerBuilder.includeColumnFieldNames(Arrays.asList(responseExcel.include()));
+		}
+
+		if (responseExcel.exclude().length != 0) {
+			writerBuilder.excludeColumnFieldNames(Arrays.asList(responseExcel.exclude()));
+		}
+
+		for (Class<? extends WriteHandler> clazz : responseExcel.writeHandler()) {
+			writerBuilder.registerWriteHandler(BeanUtils.instantiateClass(clazz));
+		}
+
+		// 开启国际化头信息处理
+		if (responseExcel.i18nHeader() && i18nHeaderCellWriteHandler != null) {
+			writerBuilder.registerWriteHandler(i18nHeaderCellWriteHandler);
+		}
+
+		// 自定义注入的转换器
+		registerCustomConverter(writerBuilder);
+
+		for (Class<? extends Converter> clazz : responseExcel.converter()) {
+			writerBuilder.registerConverter(BeanUtils.instantiateClass(clazz));
+		}
+
+		String templatePath = configProperties.getTemplatePath();
+		if (StringUtils.hasText(responseExcel.template())) {
+			ClassPathResource classPathResource = new ClassPathResource(
+					templatePath + File.separator + responseExcel.template());
+			InputStream inputStream = classPathResource.getInputStream();
+			writerBuilder.withTemplate(inputStream);
+		}
+
+		return writerBuilder.build();
+	}
+
+	/**
+	 * 自定义注入转换器 如果有需要,子类自己重写
+	 * @param builder ExcelWriterBuilder
+	 */
+	public void registerCustomConverter(ExcelWriterBuilder builder) {
+		converterProvider.ifAvailable(converters -> converters.forEach(builder::registerConverter));
+	}
+
+	/**
+	 * 获取 WriteSheet 对象
+	 * @param sheet sheet annotation info
+	 * @param dataClass 数据类型
+	 * @param template 模板
+	 * @param bookHeadEnhancerClass 自定义头处理器
+	 * @return WriteSheet
+	 */
+	public WriteSheet sheet(Sheet sheet, Class<?> dataClass, String template,
+			Class<? extends HeadGenerator> bookHeadEnhancerClass) {
+
+		// Sheet 编号和名称
+		Integer sheetNo = sheet.sheetNo() >= 0 ? sheet.sheetNo() : null;
+		String sheetName = sheet.sheetName();
+
+		// 是否模板写入
+		ExcelWriterSheetBuilder writerSheetBuilder = StringUtils.hasText(template) ? EasyExcel.writerSheet(sheetNo)
+				: EasyExcel.writerSheet(sheetNo, sheetName);
+
+		// 头信息增强 1. 优先使用 sheet 指定的头信息增强 2. 其次使用 @ResponseExcel 中定义的全局头信息增强
+		Class<? extends HeadGenerator> headGenerateClass = null;
+		if (isNotInterface(sheet.headGenerateClass())) {
+			headGenerateClass = sheet.headGenerateClass();
+		}
+		else if (isNotInterface(bookHeadEnhancerClass)) {
+			headGenerateClass = bookHeadEnhancerClass;
+		}
+		// 定义头信息增强则使用其生成头信息,否则使用 dataClass 来自动获取
+		if (headGenerateClass != null) {
+			fillCustomHeadInfo(dataClass, bookHeadEnhancerClass, writerSheetBuilder);
+		}
+		else if (dataClass != null) {
+			writerSheetBuilder.head(dataClass);
+			if (sheet.excludes().length > 0) {
+				writerSheetBuilder.excludeColumnFieldNames(Arrays.asList(sheet.excludes()));
+			}
+			if (sheet.includes().length > 0) {
+				writerSheetBuilder.includeColumnFieldNames(Arrays.asList(sheet.includes()));
+			}
+		}
+
+		// sheetBuilder 增强
+		writerSheetBuilder = excelWriterBuilderEnhance.enhanceSheet(writerSheetBuilder, sheetNo, sheetName, dataClass,
+				template, headGenerateClass);
+
+		return writerSheetBuilder.build();
+	}
+
+	private void fillCustomHeadInfo(Class<?> dataClass, Class<? extends HeadGenerator> headEnhancerClass,
+			ExcelWriterSheetBuilder writerSheetBuilder) {
+		HeadGenerator headGenerator = this.applicationContext.getBean(headEnhancerClass);
+		Assert.notNull(headGenerator, "The header generated bean does not exist.");
+		HeadMeta head = headGenerator.head(dataClass);
+		writerSheetBuilder.head(head.getHead());
+		writerSheetBuilder.excludeColumnFieldNames(head.getIgnoreHeadFields());
+	}
+
+	/**
+	 * 是否为Null Head Generator
+	 * @param headGeneratorClass 头生成器类型
+	 * @return true 已指定 false 未指定(默认值)
+	 */
+	private boolean isNotInterface(Class<? extends HeadGenerator> headGeneratorClass) {
+		return !Modifier.isInterface(headGeneratorClass.getModifiers());
+	}
+
+	@Override
+	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
+		this.applicationContext = applicationContext;
+	}
+
+}

+ 70 - 0
kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/handler/CustomWidthStyleStrategyHandler.java

@@ -0,0 +1,70 @@
+package com.kxs.common.excel.handler;
+
+import com.alibaba.excel.enums.CellDataTypeEnum;
+import com.alibaba.excel.metadata.Head;
+import com.alibaba.excel.metadata.data.CellData;
+import com.alibaba.excel.metadata.data.WriteCellData;
+import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
+import com.alibaba.excel.write.style.column.AbstractColumnWidthStyleStrategy;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.poi.ss.usermodel.Cell;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 表头宽度根据数据内容自适应
+ *
+ * @author 没秃顶的码农
+ * @date 2024-09-20
+ */
+public class CustomWidthStyleStrategyHandler extends AbstractColumnWidthStyleStrategy {
+
+    private final Map<Integer, Map<Integer, Integer>> CACHE = new HashMap<>();
+
+    /**
+     * 设置列宽
+     */
+    @Override
+    protected void setColumnWidth(WriteSheetHolder writeSheetHolder, List<WriteCellData<?>> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
+        boolean needSetWidth = isHead || !CollectionUtils.isEmpty(cellDataList);
+        if (needSetWidth) {
+            Map<Integer, Integer> maxColumnWidthMap = CACHE.computeIfAbsent(writeSheetHolder.getSheetNo(), k -> new HashMap<>());
+
+            Integer columnWidth = this.dataLength(cellDataList, cell, isHead);
+            if (columnWidth >= 0) {
+                if (columnWidth > 255) {
+                    columnWidth = 255;
+                }
+                Integer maxColumnWidth = maxColumnWidthMap.get(cell.getColumnIndex());
+                if (maxColumnWidth == null || columnWidth > maxColumnWidth) {
+                    maxColumnWidthMap.put(cell.getColumnIndex(), columnWidth);
+                    writeSheetHolder.getSheet().setColumnWidth(cell.getColumnIndex(), columnWidth * 256);
+                }
+            }
+        }
+    }
+
+    /**
+     * 数据长度
+     */
+    private Integer dataLength(List<WriteCellData<?>> cellDataList, Cell cell, Boolean isHead) {
+        if (isHead) {
+            return cell.getStringCellValue().getBytes().length;
+        } else {
+            CellData<?> cellData = cellDataList.get(0);
+            CellDataTypeEnum type = cellData.getType();
+            if (type == null) {
+                return -1;
+            } else {
+                return switch (type) {
+                    case STRING -> cellData.getStringValue().getBytes().length + 1;
+                    case BOOLEAN -> cellData.getBooleanValue().toString().getBytes().length;
+                    case NUMBER -> cellData.getNumberValue().toString().getBytes().length * 2;
+                    default -> -1;
+                };
+            }
+        }
+    }
+}

+ 73 - 0
kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/handler/DefaultAnalysisEventListener.java

@@ -0,0 +1,73 @@
+package com.kxs.common.excel.handler;
+
+import com.alibaba.excel.context.AnalysisContext;
+import com.kxs.common.excel.annotation.ExcelLine;
+import com.kxs.common.excel.kit.Validators;
+import com.kxs.common.excel.vo.ErrorMessage;
+import jakarta.validation.ConstraintViolation;
+import lombok.extern.slf4j.Slf4j;
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * 默认的 AnalysisEventListener
+ *
+ * @author 没秃顶的码农
+ * @date 2021/4/16
+ */
+@Slf4j
+public class DefaultAnalysisEventListener extends ListAnalysisEventListener<Object> {
+
+	private final List<Object> list = new ArrayList<>();
+
+	private final List<ErrorMessage> errorMessageList = new ArrayList<>();
+
+	private Long lineNum = 1L;
+
+	@Override
+	public void invoke(Object o, AnalysisContext analysisContext) {
+		lineNum++;
+
+		Set<ConstraintViolation<Object>> violations = Validators.validate(o);
+		if (!violations.isEmpty()) {
+			Set<String> messageSet = violations.stream().map(ConstraintViolation::getMessage)
+					.collect(Collectors.toSet());
+			errorMessageList.add(new ErrorMessage(lineNum, messageSet));
+		}
+		else {
+			Field[] fields = o.getClass().getDeclaredFields();
+			for (Field field : fields) {
+				if (field.isAnnotationPresent(ExcelLine.class) && field.getType() == Long.class) {
+					try {
+						field.setAccessible(true);
+						field.set(o, lineNum);
+					}
+					catch (IllegalAccessException e) {
+						e.printStackTrace();
+					}
+				}
+			}
+			list.add(o);
+		}
+	}
+
+	@Override
+	public void doAfterAllAnalysed(AnalysisContext analysisContext) {
+		log.debug("Excel read analysed");
+	}
+
+	@Override
+	public List<Object> getList() {
+		return list;
+	}
+
+	@Override
+	public List<ErrorMessage> getErrors() {
+		return errorMessageList;
+	}
+
+}

+ 26 - 0
kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/handler/ListAnalysisEventListener.java

@@ -0,0 +1,26 @@
+package com.kxs.common.excel.handler;
+
+import com.alibaba.excel.event.AnalysisEventListener;
+import com.kxs.common.excel.vo.ErrorMessage;
+
+import java.util.List;
+
+/**
+ * list analysis EventListener
+ *
+ */
+public abstract class ListAnalysisEventListener<T> extends AnalysisEventListener<T> {
+
+	/**
+	 * 获取 excel 解析的对象列表
+	 * @return 集合
+	 */
+	public abstract List<T> getList();
+
+	/**
+	 * 获取异常校验结果
+	 * @return 集合
+	 */
+	public abstract List<ErrorMessage> getErrors();
+
+}

+ 129 - 0
kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/handler/ManySheetWriteHandler.java

@@ -0,0 +1,129 @@
+package com.kxs.common.excel.handler;
+
+import com.alibaba.excel.EasyExcel;
+import com.alibaba.excel.ExcelWriter;
+import com.alibaba.excel.converters.Converter;
+import com.alibaba.excel.write.metadata.WriteSheet;
+import com.aliyun.oss.OSS;
+import com.kxs.common.excel.annotation.ResponseExcel;
+import com.kxs.common.excel.annotation.Sheet;
+import com.kxs.common.excel.config.ExcelConfigProperties;
+import com.kxs.common.excel.config.FileProperties;
+import com.kxs.common.excel.enhance.WriterBuilderEnhancer;
+import com.kxs.common.excel.http.RemoteSysService;
+import com.kxs.common.excel.kit.ExcelException;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.util.CollectionUtils;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.List;
+
+import static com.alibaba.excel.EasyExcelFactory.writerSheet;
+
+/**
+ * @author 没秃顶的码农
+ * @date 2024/07/24
+ */
+public class ManySheetWriteHandler extends AbstractSheetWriteHandler {
+
+	public ManySheetWriteHandler(ExcelConfigProperties configProperties,
+								 OSS ossClient, FileProperties ossProperties,
+								 ObjectProvider<List<Converter<?>>> converterProvider,
+								 WriterBuilderEnhancer excelWriterBuilderEnhance, RemoteSysService remoteSysService) {
+		super(configProperties, ossClient, ossProperties, converterProvider, excelWriterBuilderEnhance, remoteSysService);
+	}
+
+	/**
+	 * 当且仅当List不为空且List中的元素也是List 才返回true
+	 * @param obj 返回对象
+	 * @return boolean
+	 */
+	@Override
+	public boolean support(Object obj) {
+		if (obj instanceof List) {
+			List<?> objList = (List<?>) obj;
+			return !objList.isEmpty() && objList.get(0) instanceof List;
+		}
+		else {
+			throw new ExcelException("@ResponseExcel 返回值必须为List类型");
+		}
+	}
+
+	@Override
+	public void write(Object obj, HttpServletResponse response, ResponseExcel responseExcel) {
+		List<?> objList = (List<?>) obj;
+		ExcelWriter excelWriter = getExcelWriter(response, responseExcel);
+
+		Sheet[] sheets = responseExcel.sheets();
+		WriteSheet sheet;
+		for (int i = 0; i < sheets.length; i++) {
+			List<?> eleList = (List<?>) objList.get(i);
+
+			if (CollectionUtils.isEmpty(eleList)) {
+				sheet = EasyExcel.writerSheet(responseExcel.sheets()[i].sheetName()).build();
+			}
+			else {
+				// 有模板则不指定sheet名
+				Class<?> dataClass = eleList.get(0).getClass();
+				sheet = this.sheet(responseExcel.sheets()[i], dataClass, responseExcel.template(),
+						responseExcel.headGenerator());
+			}
+
+			// 填充 sheet
+			if (responseExcel.fill()) {
+				excelWriter.fill(eleList, sheet);
+			}
+			else {
+				// 写入sheet
+				excelWriter.write(eleList, sheet);
+			}
+		}
+		excelWriter.finish();
+	}
+
+	@Override
+	public FileInputStream writeToOss(Object obj, HttpServletResponse response, ResponseExcel responseExcel) {
+
+		try {
+			List<?> eleList = (List<?>) obj;
+
+			// 临时目录 临时文件名
+			String tmp = System.getProperty("java.io.tmpdir");
+			SimpleDateFormat formatter = new SimpleDateFormat("yyMMddHHmm");
+			String times = formatter.format(new Date());
+			String fileName = responseExcel.name() + times;
+			File file = new File(tmp + fileName + responseExcel.suffix().getValue());
+			FileOutputStream fileOutputStream = new FileOutputStream(file);
+			ExcelWriter excelWriter = getExcelWriterFile(fileOutputStream, responseExcel);
+
+			WriteSheet sheet;
+			if (CollectionUtils.isEmpty(eleList)) {
+				sheet = writerSheet(responseExcel.sheets()[0].sheetName()).build();
+			} else {
+				// 有模板则不指定sheet名
+				Class<?> dataClass = eleList.get(0).getClass();
+				sheet = this.sheet(responseExcel.sheets()[0], dataClass, responseExcel.template(),
+						responseExcel.headGenerator());
+			}
+
+
+			// 填充 sheet
+			if (responseExcel.fill()) {
+				excelWriter.fill(eleList, sheet);
+			} else {
+				// 写入sheet
+				excelWriter.write(eleList, sheet);
+			}
+			excelWriter.finish();
+			return new FileInputStream(file);
+		} catch (Exception e) {
+			throw new RuntimeException("导出异常");
+		}
+	}
+
+}

+ 68 - 0
kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/handler/SheetWriteHandler.java

@@ -0,0 +1,68 @@
+package com.kxs.common.excel.handler;
+
+import com.kxs.common.excel.annotation.ResponseExcel;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.SneakyThrows;
+import org.springframework.scheduling.annotation.Async;
+
+import java.io.FileInputStream;
+
+/**
+ * @author 没秃顶的码农
+ * @date 2024/07/24
+ * <p>
+ * sheet 写出处理器
+ */
+public interface SheetWriteHandler {
+
+	/**
+	 * 是否支持
+	 * @param obj
+	 * @return
+	 */
+	boolean support(Object obj);
+
+	/**
+	 * 校验
+	 * @param responseExcel 注解
+	 */
+	void check(ResponseExcel responseExcel);
+
+	/**
+	 * 返回的对象
+	 * @param o obj
+	 * @param response 输出对象
+	 * @param responseExcel 注解
+	 */
+	void export(Object o, HttpServletResponse response, ResponseExcel responseExcel);
+
+	/**
+	 * 导出到 OSS
+	 *
+	 * @param o             o
+	 * @param response      响应
+	 * @param responseExcel 响应 excel
+	 */
+	void exportOss(Object o, HttpServletResponse response, ResponseExcel responseExcel);
+
+
+	/**
+	 * 写成对象
+	 * @param o obj
+	 * @param response 输出对象
+	 * @param responseExcel 注解
+	 */
+	void write(Object o, HttpServletResponse response, ResponseExcel responseExcel);
+
+	/**
+	 * 写入 OSS
+	 *
+	 * @param o             obj
+	 * @param response      输出对象
+	 * @param responseExcel 注解
+	 * @return
+	 */
+	FileInputStream writeToOss(Object o, HttpServletResponse response, ResponseExcel responseExcel);
+
+
+}

+ 121 - 0
kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/handler/SingleSheetWriteHandler.java

@@ -0,0 +1,121 @@
+package com.kxs.common.excel.handler;
+
+import com.alibaba.excel.ExcelWriter;
+import com.alibaba.excel.converters.Converter;
+import com.alibaba.excel.write.metadata.WriteSheet;
+import com.aliyun.oss.OSS;
+import com.kxs.common.excel.annotation.ResponseExcel;
+import com.kxs.common.excel.config.ExcelConfigProperties;
+import com.kxs.common.excel.config.FileProperties;
+import com.kxs.common.excel.enhance.WriterBuilderEnhancer;
+import com.kxs.common.excel.http.RemoteSysService;
+import com.kxs.common.excel.kit.ExcelException;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.util.CollectionUtils;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.List;
+
+import static com.alibaba.excel.EasyExcelFactory.writerSheet;
+
+/**
+ * @author 没秃顶的码农
+ * @date 2024/07/24
+ * <p>
+ * 处理单sheet 页面
+ */
+public class SingleSheetWriteHandler extends AbstractSheetWriteHandler {
+
+    public SingleSheetWriteHandler(ExcelConfigProperties configProperties,
+                                   OSS ossClient, FileProperties ossProperties,
+                                   ObjectProvider<List<Converter<?>>> converterProvider,
+                                   WriterBuilderEnhancer excelWriterBuilderEnhance, RemoteSysService remoteSysService) {
+        super(configProperties, ossClient, ossProperties, converterProvider, excelWriterBuilderEnhance, remoteSysService);
+    }
+
+    /**
+     * obj 是List 且list不为空同时list中的元素不是是List 才返回true
+     *
+     * @param obj 返回对象
+     * @return boolean
+     */
+    @Override
+    public boolean support(Object obj) {
+        if (obj instanceof List) {
+            List<?> objList = (List<?>) obj;
+            return !objList.isEmpty() && !(objList.get(0) instanceof List);
+        } else {
+            throw new ExcelException("@ResponseExcel 返回值必须为List类型");
+        }
+    }
+
+    @Override
+    public void write(Object obj, HttpServletResponse response, ResponseExcel responseExcel) {
+        List<?> eleList = (List<?>) obj;
+        ExcelWriter excelWriter = getExcelWriter(response, responseExcel);
+
+        WriteSheet sheet;
+        if (CollectionUtils.isEmpty(eleList)) {
+            sheet = writerSheet(responseExcel.sheets()[0].sheetName()).build();
+        } else {
+            // 有模板则不指定sheet名
+            Class<?> dataClass = eleList.get(0).getClass();
+            sheet = this.sheet(responseExcel.sheets()[0], dataClass, responseExcel.template(),
+                    responseExcel.headGenerator());
+        }
+
+        // 填充 sheet
+        if (responseExcel.fill()) {
+            excelWriter.fill(eleList, sheet);
+        } else {
+            // 写入sheet
+            excelWriter.write(eleList, sheet);
+        }
+        excelWriter.finish();
+    }
+
+    @Override
+    public FileInputStream writeToOss(Object obj, HttpServletResponse response, ResponseExcel responseExcel) {
+        try {
+            List<?> eleList = (List<?>) obj;
+
+            // 临时目录 临时文件名
+            String tmp = System.getProperty("java.io.tmpdir");
+            SimpleDateFormat formatter = new SimpleDateFormat("yyMMddHHmm");
+            String times = formatter.format(new Date());
+            String fileName = responseExcel.name() + times;
+			File file = new File(tmp + fileName + responseExcel.suffix().getValue());
+            FileOutputStream fileOutputStream = new FileOutputStream(file);
+            ExcelWriter excelWriter = getExcelWriterFile(fileOutputStream, responseExcel);
+
+            WriteSheet sheet;
+            if (CollectionUtils.isEmpty(eleList)) {
+                sheet = writerSheet(responseExcel.sheets()[0].sheetName()).build();
+            } else {
+                // 有模板则不指定sheet名
+                Class<?> dataClass = eleList.get(0).getClass();
+                sheet = this.sheet(responseExcel.sheets()[0], dataClass, responseExcel.template(),
+                        responseExcel.headGenerator());
+            }
+
+
+            // 填充 sheet
+            if (responseExcel.fill()) {
+                excelWriter.fill(eleList, sheet);
+            } else {
+                // 写入sheet
+                excelWriter.write(eleList, sheet);
+            }
+            excelWriter.finish();
+			return new FileInputStream(file);
+		} catch (Exception e) {
+            throw new RuntimeException(e.getMessage());
+        }
+    }
+
+}

+ 22 - 0
kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/head/HeadGenerator.java

@@ -0,0 +1,22 @@
+package com.kxs.common.excel.head;
+
+/**
+ * Excel头生成器,用于自定义生成头部信息
+ *
+ * @author 没秃顶的码农 2020/10/27
+ * @version 1.0
+ */
+public interface HeadGenerator {
+
+	/**
+	 * <p>
+	 * 自定义头部信息
+	 * </p>
+	 * 实现类根据数据的class信息,定制Excel头<br/>
+	 * 具体方法使用参考:https://www.yuque.com/easyexcel/doc/write#b4b9de00
+	 * @param clazz 当前sheet的数据类型
+	 * @return List<List<String>> Head头信息
+	 */
+	HeadMeta head(Class<?> clazz);
+
+}

+ 29 - 0
kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/head/HeadMeta.java

@@ -0,0 +1,29 @@
+package com.kxs.common.excel.head;
+
+import lombok.Data;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ * @author Yakir
+ * @date 2021/4/26 10:58
+ */
+@Data
+public class HeadMeta {
+
+	/**
+	 * <p>
+	 * 自定义头部信息
+	 * </p>
+	 * 实现类根据数据的class信息,定制Excel头<br/>
+	 * 具体方法使用参考:https://www.yuque.com/easyexcel/doc/write#b4b9de00
+	 */
+	private List<List<String>> head;
+
+	/**
+	 * 忽略头对应字段名称
+	 */
+	private Set<String> ignoreHeadFields;
+
+}

+ 61 - 0
kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/head/I18nHeaderCellWriteHandler.java

@@ -0,0 +1,61 @@
+package com.kxs.common.excel.head;
+
+import com.alibaba.excel.metadata.Head;
+import com.alibaba.excel.write.handler.CellWriteHandler;
+import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
+import com.alibaba.excel.write.metadata.holder.WriteTableHolder;
+import lombok.RequiredArgsConstructor;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.poi.ss.usermodel.Row;
+import org.springframework.context.MessageSource;
+import org.springframework.context.i18n.LocaleContextHolder;
+import org.springframework.util.PropertyPlaceholderHelper;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 对表头进行国际化处理
+ *
+ * @author 没秃顶的码农
+ */
+@RequiredArgsConstructor
+public class I18nHeaderCellWriteHandler implements CellWriteHandler {
+
+	/**
+	 * 国际化消息源
+	 */
+	private final MessageSource messageSource;
+
+	/**
+	 * 国际化翻译
+	 */
+	private final PropertyPlaceholderHelper.PlaceholderResolver placeholderResolver;
+
+	public I18nHeaderCellWriteHandler(MessageSource messageSource) {
+		this.messageSource = messageSource;
+		this.placeholderResolver = placeholderName -> this.messageSource.getMessage(placeholderName, null,
+				LocaleContextHolder.getLocale());
+	}
+
+	/**
+	 * 占位符处理
+	 */
+	private final PropertyPlaceholderHelper propertyPlaceholderHelper = new PropertyPlaceholderHelper("{", "}");
+
+	@Override
+	public void beforeCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row,
+			Head head, Integer columnIndex, Integer relativeRowIndex, Boolean isHead) {
+		if (isHead != null && isHead) {
+			List<String> originHeadNameList = head.getHeadNameList();
+			if (CollectionUtils.isNotEmpty(originHeadNameList)) {
+				// 国际化处理
+				List<String> i18nHeadNames = originHeadNameList.stream()
+						.map(headName -> propertyPlaceholderHelper.replacePlaceholders(headName, placeholderResolver))
+						.collect(Collectors.toList());
+				head.setHeadNameList(i18nHeadNames);
+			}
+		}
+	}
+
+}

+ 25 - 0
kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/http/RemoteSysService.java

@@ -0,0 +1,25 @@
+package com.kxs.common.excel.http;
+
+import com.kxs.common.core.constant.SecurityConstants;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.service.annotation.PostExchange;
+
+import java.util.Map;
+
+/**
+ * 调用系统模块保存导出文件
+ *
+ * @author 没秃顶的码农
+ * @date 2023/11/12
+ */
+public interface RemoteSysService {
+
+	/**
+	 * 保存文件
+	 * @param sysFile 文件
+	 */
+	@PostExchange("/sysFile/save")
+	void saveFile(@RequestBody Map<String, Object> sysFile, @RequestHeader(SecurityConstants.FROM) String from);
+
+}

+ 15 - 0
kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/kit/ExcelException.java

@@ -0,0 +1,15 @@
+package com.kxs.common.excel.kit;
+
+/**
+ * @author 没秃顶的码农
+ * @date 2020/3/31
+ */
+public class ExcelException extends RuntimeException {
+
+	private static final long serialVersionUID = 1L;
+
+	public ExcelException(String message) {
+		super(message);
+	}
+
+}

+ 40 - 0
kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/kit/Validators.java

@@ -0,0 +1,40 @@
+package com.kxs.common.excel.kit;
+
+import jakarta.validation.ConstraintViolation;
+import jakarta.validation.Validation;
+import jakarta.validation.Validator;
+import jakarta.validation.ValidatorFactory;
+
+import java.util.Set;
+
+/**
+ * 校验工具
+ *
+ */
+public final class Validators {
+
+	private Validators() {
+	}
+
+	private static final Validator VALIDATOR;
+
+	static {
+		ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
+		VALIDATOR = factory.getValidator();
+	}
+
+	/**
+	 * Validates all constraints on {@code object}.
+	 * @param object object to validate
+	 * @param <T> the type of the object to validate
+	 * @return constraint violations or an empty set if none
+	 * @throws IllegalArgumentException if object is {@code null} or if {@code null} is
+	 * passed to the varargs groups
+	 * @throws ValidationException if a non recoverable error happens during the
+	 * validation process
+	 */
+	public static <T> Set<ConstraintViolation<T>> validate(T object) {
+		return VALIDATOR.validate(object);
+	}
+
+}

+ 20 - 0
kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/processor/NameProcessor.java

@@ -0,0 +1,20 @@
+package com.kxs.common.excel.processor;
+
+import java.lang.reflect.Method;
+
+/**
+ * @author 没秃顶的码农
+ * @date 2024/07/24
+ */
+public interface NameProcessor {
+
+	/**
+	 * 解析名称
+	 * @param args 拦截器对象
+	 * @param method
+	 * @param key 表达式
+	 * @return
+	 */
+	String doDetermineName(Object[] args, Method method, String key);
+
+}

+ 40 - 0
kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/processor/NameSpelExpressionProcessor.java

@@ -0,0 +1,40 @@
+package com.kxs.common.excel.processor;
+
+import org.springframework.context.expression.MethodBasedEvaluationContext;
+import org.springframework.core.DefaultParameterNameDiscoverer;
+import org.springframework.core.ParameterNameDiscoverer;
+import org.springframework.expression.EvaluationContext;
+import org.springframework.expression.ExpressionParser;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+
+import java.lang.reflect.Method;
+
+/**
+ * @author 没秃顶的码农
+ * @date 2024/07/24
+ */
+public class NameSpelExpressionProcessor implements NameProcessor {
+
+	/**
+	 * 参数发现器
+	 */
+	private static final ParameterNameDiscoverer NAME_DISCOVERER = new DefaultParameterNameDiscoverer();
+
+	/**
+	 * Express语法解析器
+	 */
+	private static final ExpressionParser PARSER = new SpelExpressionParser();
+
+	@Override
+	public String doDetermineName(Object[] args, Method method, String key) {
+
+		if (!key.contains("#")) {
+			return key;
+		}
+
+		EvaluationContext context = new MethodBasedEvaluationContext(null, method, args, NAME_DISCOVERER);
+		final Object value = PARSER.parseExpression(key).getValue(context);
+		return value == null ? null : value.toString();
+	}
+
+}

+ 41 - 0
kxs-common/kxs-common-excel/src/main/java/com/kxs/common/excel/vo/ErrorMessage.java

@@ -0,0 +1,41 @@
+package com.kxs.common.excel.vo;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * 校验错误信息
+ *
+ * @author 没秃顶的码农
+ * @date 2021/8/4
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class ErrorMessage {
+
+	/**
+	 * 行号
+	 */
+	private Long lineNum;
+
+	/**
+	 * 错误信息
+	 */
+	private Set<String> errors = new HashSet<>();
+
+	public ErrorMessage(Set<String> errors) {
+		this.errors = errors;
+	}
+
+	public ErrorMessage(String error) {
+		HashSet<String> objects = new HashSet<>();
+		objects.add(error);
+		this.errors = objects;
+	}
+
+}

+ 1 - 0
kxs-common/kxs-common-excel/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

@@ -0,0 +1 @@
+com.kxs.common.excel.ResponseExcelAutoConfiguration

+ 56 - 0
kxs-common/kxs-common-idempotent/pom.xml

@@ -0,0 +1,56 @@
+<?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">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>com.kxs</groupId>
+        <artifactId>kxs-common</artifactId>
+        <version>1.1.0</version>
+    </parent>
+
+    <artifactId>kxs-common-idempotent</artifactId>
+
+    <properties>
+        <maven.compiler.source>17</maven.compiler.source>
+        <maven.compiler.target>17</maven.compiler.target>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <mica.version>3.0.0</mica.version>
+<!--        <redisson.version>3.24.3</redisson.version>-->
+        <redisson.version>3.16.2</redisson.version>
+    </properties>
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.kxs</groupId>
+            <artifactId>kxs-common-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <!--aop-->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-aop</artifactId>
+        </dependency>
+        <!--redisson-->
+        <dependency>
+            <groupId>org.redisson</groupId>
+            <artifactId>redisson-spring-boot-starter</artifactId>
+            <version>${redisson.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>net.dreamlu</groupId>
+            <artifactId>mica-auto</artifactId>
+            <version>${mica.version}</version>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+</project>

+ 69 - 0
kxs-common/kxs-common-idempotent/src/main/java/com/kxs/common/idempotent/IdempotentAutoConfiguration.java

@@ -0,0 +1,69 @@
+package com.kxs.common.idempotent;
+
+import cn.hutool.core.util.StrUtil;
+import com.kxs.common.idempotent.aspect.IdempotentAspect;
+import com.kxs.common.idempotent.config.RedisProperties;
+import com.kxs.common.idempotent.expression.ExpressionResolver;
+import com.kxs.common.idempotent.expression.KeyResolver;
+import lombok.AllArgsConstructor;
+import org.redisson.Redisson;
+import org.redisson.api.RedissonClient;
+import org.redisson.config.Config;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.AutoConfigureAfter;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * @author 没秃顶的码农
+ * @date 2020/9/25
+ * <p>
+ * 幂等插件初始化
+ */
+@AutoConfiguration
+@Configuration(proxyBeanMethods = false)
+@AutoConfigureAfter(RedisAutoConfiguration.class)
+@EnableConfigurationProperties({ RedisProperties.class })
+@AllArgsConstructor
+public class IdempotentAutoConfiguration {
+
+	private final RedisProperties redisProperties;
+
+	/**
+	 * 切面 拦截处理所有 @Idempotent
+	 * @return Aspect
+	 */
+	@Bean
+	public IdempotentAspect idempotentAspect() {
+		return new IdempotentAspect();
+	}
+
+	/**
+	 * key 解析器
+	 * @return KeyResolver
+	 */
+	@Bean
+	@ConditionalOnMissingBean(KeyResolver.class)
+	public KeyResolver keyResolver() {
+		return new ExpressionResolver();
+	}
+
+	@Bean
+	@ConditionalOnMissingBean
+	public RedissonClient redissonClient() {
+		Config config = new Config();
+		config.useSingleServer().setAddress(redisProperties.getPrefix() + redisProperties.getHost() + ":" + redisProperties.getPort());
+		if(StrUtil.isNotBlank(redisProperties.getUsername())){
+			config.useSingleServer().setUsername(redisProperties.getUsername());
+		}
+		if(StrUtil.isNotBlank(redisProperties.getPassword())){
+			config.useSingleServer().setPassword(redisProperties.getPassword());
+		}
+		return Redisson.create(config);
+	}
+
+}

+ 51 - 0
kxs-common/kxs-common-idempotent/src/main/java/com/kxs/common/idempotent/annotation/Idempotent.java

@@ -0,0 +1,51 @@
+package com.kxs.common.idempotent.annotation;
+
+import java.lang.annotation.*;
+import java.util.concurrent.TimeUnit;
+
+
+/**
+ * 接口幂等
+ *
+ * @author 没秃顶的码农
+ * @date 2024-04-26
+ */
+@Inherited
+@Target(ElementType.METHOD)
+@Retention(value = RetentionPolicy.RUNTIME)
+public @interface Idempotent {
+
+	/**
+	 * <p>
+	 * 如果是实体类的话,默认拦截不会生效. objects.toString()会返回不同地址.
+	 * </p>
+	 * 幂等操作的唯一标识,使用spring el表达式 用#来引用方法参数
+	 * @return Spring-EL expression
+	 */
+	String key() default "";
+
+	/**
+	 * 有效期 默认:1 有效期要大于程序执行时间,否则请求还是可能会进来
+	 * @return expireTime
+	 */
+	int expireTime() default 2;
+
+	/**
+	 * 时间单位 默认:s
+	 * @return TimeUnit
+	 */
+	TimeUnit timeUnit() default TimeUnit.SECONDS;
+
+	/**
+	 * 提示信息,可自定义
+	 * @return String
+	 */
+	String info() default "操作频繁,请稍后重试";
+
+	/**
+	 * 是否在业务完成后删除key true:删除 false:不删除
+	 * @return boolean
+	 */
+	boolean delKey() default false;
+
+}

+ 142 - 0
kxs-common/kxs-common-idempotent/src/main/java/com/kxs/common/idempotent/aspect/IdempotentAspect.java

@@ -0,0 +1,142 @@
+package com.kxs.common.idempotent.aspect;
+
+import com.kxs.common.core.exception.GlobalCustomerException;
+import com.kxs.common.idempotent.annotation.Idempotent;
+import com.kxs.common.idempotent.exception.IdempotentException;
+import com.kxs.common.idempotent.expression.KeyResolver;
+import jakarta.servlet.http.HttpServletRequest;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.annotation.After;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Before;
+import org.aspectj.lang.annotation.Pointcut;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.redisson.api.RMapCache;
+import org.redisson.api.RedissonClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import java.lang.reflect.Method;
+import java.time.LocalDateTime;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+
+/**
+ * The Idempotent Aspect
+ *
+ * @author 没秃顶的码农
+ * @date 2024-04-26
+ */
+@Aspect
+public class IdempotentAspect {
+
+	private static final Logger LOGGER = LoggerFactory.getLogger(IdempotentAspect.class);
+
+	private static final ThreadLocal<Map<String, Object>> THREAD_CACHE = ThreadLocal.withInitial(HashMap::new);
+
+	private static final String RMAPCACHE_KEY = "idempotent";
+
+	private static final String KEY = "key";
+
+	private static final String DELKEY = "delKey";
+
+	@Autowired
+	private RedissonClient redissonClient;
+
+	@Autowired
+	private KeyResolver keyResolver;
+
+	@Pointcut("@annotation(com.kxs.common.idempotent.annotation.Idempotent)")
+	public void pointCut() {
+	}
+
+	@Before("pointCut()")
+	public void beforePointCut(JoinPoint joinPoint) {
+		ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder
+				.getRequestAttributes();
+		HttpServletRequest request = requestAttributes.getRequest();
+
+		MethodSignature signature = (MethodSignature) joinPoint.getSignature();
+		Method method = signature.getMethod();
+		if (!method.isAnnotationPresent(Idempotent.class)) {
+			return;
+		}
+		Idempotent idempotent = method.getAnnotation(Idempotent.class);
+
+		String key;
+
+		// 若没有配置 幂等 标识编号,则使用 url + 参数列表作为区分
+		if (!StringUtils.hasLength(idempotent.key())) {
+			String url = request.getRequestURL().toString();
+			String argString = Arrays.asList(joinPoint.getArgs()).toString();
+			key = url + argString;
+		}
+		else {
+			// 使用jstl 规则区分
+			key = keyResolver.resolver(idempotent, joinPoint);
+		}
+		// 当配置了el表达式但是所选字段为空时,会抛出异常,兜底使用url做标识
+		if (key == null) {
+			key = request.getRequestURL().toString();
+		}
+
+		long expireTime = idempotent.expireTime();
+		String info = idempotent.info();
+		TimeUnit timeUnit = idempotent.timeUnit();
+		boolean delKey = idempotent.delKey();
+
+		// do not need check null
+		RMapCache<String, Object> rMapCache = redissonClient.getMapCache(RMAPCACHE_KEY);
+		String value = LocalDateTime.now().toString().replace("T", " ");
+		Object v1;
+		if (null != rMapCache.get(key)) {
+			// 自定义异常
+			throw new GlobalCustomerException(info);
+		}
+		synchronized (this) {
+			v1 = rMapCache.putIfAbsent(key, value, expireTime, timeUnit);
+			if (null != v1) {
+				throw new IdempotentException(info);
+			}
+			else {
+				LOGGER.info("[idempotent]:has stored key={},value={},expireTime={}{},now={}", key, value, expireTime,
+						timeUnit, LocalDateTime.now().toString());
+			}
+		}
+
+		Map<String, Object> map = THREAD_CACHE.get();
+		map.put(KEY, key);
+		map.put(DELKEY, delKey);
+	}
+
+	@After("pointCut()")
+	public void afterPointCut(JoinPoint joinPoint) {
+		Map<String, Object> map = THREAD_CACHE.get();
+		if (CollectionUtils.isEmpty(map)) {
+			return;
+		}
+
+		RMapCache<Object, Object> mapCache = redissonClient.getMapCache(RMAPCACHE_KEY);
+		if (mapCache.size() == 0) {
+			return;
+		}
+
+		String key = map.get(KEY).toString();
+		boolean delKey = (boolean) map.get(DELKEY);
+
+		if (delKey) {
+			mapCache.fastRemove(key);
+			LOGGER.info("[idempotent]:has removed key={}", key);
+		}
+		THREAD_CACHE.remove();
+	}
+
+}

+ 38 - 0
kxs-common/kxs-common-idempotent/src/main/java/com/kxs/common/idempotent/config/RedisProperties.java

@@ -0,0 +1,38 @@
+package com.kxs.common.idempotent.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+/**
+ * redis配置属性
+ *
+ * @author 没秃顶的码农
+ * @date 2024-04-24
+ */
+@Data
+@ConfigurationProperties(prefix = "spring.data.redis")
+public class RedisProperties {
+
+    private String prefix = "redis://";
+
+    /**
+     * host
+     */
+    private String host;
+
+    /**
+     * username
+     */
+    private String username;
+
+    /**
+     * pwd
+     */
+    private String password;
+
+    /**
+     * port
+     */
+    private String port = "6379";
+}

+ 33 - 0
kxs-common/kxs-common-idempotent/src/main/java/com/kxs/common/idempotent/exception/IdempotentException.java

@@ -0,0 +1,33 @@
+package com.kxs.common.idempotent.exception;
+
+
+/**
+ * 幂等异常
+ *
+ * @author 没秃顶的码农
+ * @date 2024-04-26
+ */
+public class IdempotentException extends RuntimeException {
+
+	public IdempotentException() {
+		super();
+	}
+
+	public IdempotentException(String message) {
+		super(message);
+	}
+
+	public IdempotentException(String message, Throwable cause) {
+		super(message, cause);
+	}
+
+	public IdempotentException(Throwable cause) {
+		super(cause);
+	}
+
+	protected IdempotentException(String message, Throwable cause, boolean enableSuppression,
+			boolean writableStackTrace) {
+		super(message, cause, enableSuppression, writableStackTrace);
+	}
+
+}

+ 66 - 0
kxs-common/kxs-common-idempotent/src/main/java/com/kxs/common/idempotent/expression/ExpressionResolver.java

@@ -0,0 +1,66 @@
+package com.kxs.common.idempotent.expression;
+
+
+
+import com.kxs.common.idempotent.annotation.Idempotent;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.core.DefaultParameterNameDiscoverer;
+import org.springframework.core.ParameterNameDiscoverer;
+import org.springframework.expression.Expression;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.expression.spel.support.StandardEvaluationContext;
+
+import java.lang.reflect.Method;
+
+
+/**
+ * <p>
+ * 默认key 抽取, 优先根据 spel 处理
+ *
+ * @author 没秃顶的码农
+ * @date 2024-04-26
+ */
+public class ExpressionResolver implements KeyResolver {
+
+	private static final SpelExpressionParser PARSER = new SpelExpressionParser();
+
+	private static final ParameterNameDiscoverer DISCOVERER = new DefaultParameterNameDiscoverer();
+
+	@Override
+	public String resolver(Idempotent idempotent, JoinPoint point) {
+		Object[] arguments = point.getArgs();
+		String[] params = DISCOVERER.getParameterNames(getMethod(point));
+		StandardEvaluationContext context = new StandardEvaluationContext();
+
+		if (params != null && params.length > 0) {
+			for (int len = 0; len < params.length; len++) {
+				context.setVariable(params[len], arguments[len]);
+			}
+		}
+
+		Expression expression = PARSER.parseExpression(idempotent.key());
+		return expression.getValue(context, String.class);
+	}
+
+	/**
+	 * 根据切点解析方法信息
+	 * @param joinPoint 切点信息
+	 * @return Method 原信息
+	 */
+	private Method getMethod(JoinPoint joinPoint) {
+		MethodSignature signature = (MethodSignature) joinPoint.getSignature();
+		Method method = signature.getMethod();
+		if (method.getDeclaringClass().isInterface()) {
+			try {
+				method = joinPoint.getTarget().getClass().getDeclaredMethod(joinPoint.getSignature().getName(),
+						method.getParameterTypes());
+			}
+			catch (SecurityException | NoSuchMethodException e) {
+				throw new RuntimeException(e);
+			}
+		}
+		return method;
+	}
+
+}

+ 23 - 0
kxs-common/kxs-common-idempotent/src/main/java/com/kxs/common/idempotent/expression/KeyResolver.java

@@ -0,0 +1,23 @@
+package com.kxs.common.idempotent.expression;
+
+import com.kxs.common.idempotent.annotation.Idempotent;
+import org.aspectj.lang.JoinPoint;
+
+
+/**
+ * 唯一标志处理器
+ *
+ * @author 没秃顶的码农
+ * @date 2024-04-26
+ */
+public interface KeyResolver {
+
+	/**
+	 * 解析处理 key
+	 * @param idempotent 接口注解标识
+	 * @param point 接口切点信息
+	 * @return 处理结果
+	 */
+	String resolver(Idempotent idempotent, JoinPoint point);
+
+}

+ 1 - 0
kxs-common/kxs-common-idempotent/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

@@ -0,0 +1 @@
+com.kxs.common.idempotent.IdempotentAutoConfiguration

+ 54 - 0
kxs-common/kxs-common-log/pom.xml

@@ -0,0 +1,54 @@
+<?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">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>com.kxs</groupId>
+        <artifactId>kxs-common</artifactId>
+        <version>1.1.0</version>
+    </parent>
+
+    <artifactId>kxs-common-log</artifactId>
+    <packaging>jar</packaging>
+
+    <description>kxs 日志服务</description>
+
+    <properties>
+        <maven.compiler.source>17</maven.compiler.source>
+        <maven.compiler.target>17</maven.compiler.target>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+
+    <dependencies>
+        <!--工具类核心包-->
+        <dependency>
+            <groupId>com.kxs</groupId>
+            <artifactId>kxs-common-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-extra</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-http</artifactId>
+        </dependency>
+        <!--系統模块-->
+        <dependency>
+            <groupId>com.kxs</groupId>
+            <artifactId>lhb-system-api</artifactId>
+        </dependency>
+        <!--安全依赖获取上下文信息-->
+        <dependency>
+            <groupId>org.springframework.security</groupId>
+            <artifactId>spring-security-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.security</groupId>
+            <artifactId>spring-security-oauth2-core</artifactId>
+        </dependency>
+
+    </dependencies>
+
+</project>

+ 35 - 0
kxs-common/kxs-common-log/src/main/java/com/kxs/common/log/LogAutoConfiguration.java

@@ -0,0 +1,35 @@
+package com.kxs.common.log;
+
+import com.kxs.common.log.aspect.SysLogAspect;
+import com.kxs.common.log.config.SkyLogProperties;
+import com.kxs.common.log.event.SysLogListener;
+import com.kxs.lhb.system.api.feign.RemoteLogService;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.annotation.EnableAsync;
+
+/**
+ * 日志自动配置
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/28
+ */
+@EnableAsync
+@Configuration(proxyBeanMethods = false)
+@EnableConfigurationProperties(SkyLogProperties.class)
+@ConditionalOnProperty(value = "security.log.enabled", matchIfMissing = true)
+public class LogAutoConfiguration {
+
+	@Bean
+	public SysLogListener sysLogListener(SkyLogProperties logProperties, RemoteLogService remoteLogService) {
+		return new SysLogListener(remoteLogService, logProperties);
+	}
+
+	@Bean
+	public SysLogAspect sysLogAspect() {
+		return new SysLogAspect();
+	}
+
+}

+ 28 - 0
kxs-common/kxs-common-log/src/main/java/com/kxs/common/log/annotation/SysLog.java

@@ -0,0 +1,28 @@
+package com.kxs.common.log.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 操作日志注解
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/28
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface SysLog {
+
+	/**
+	 * 描述
+	 * @return {String}
+	 */
+	String value() default "";
+
+	/**
+	 * spel 表达式
+	 * @return 日志描述
+	 */
+	String expression() default "";
+
+}

+ 81 - 0
kxs-common/kxs-common-log/src/main/java/com/kxs/common/log/aspect/SysLogAspect.java

@@ -0,0 +1,81 @@
+
+package com.kxs.common.log.aspect;
+
+import cn.hutool.core.text.CharSequenceUtil;
+import cn.hutool.core.util.StrUtil;
+import com.kxs.common.core.util.SpringContextHolder;
+import com.kxs.common.log.event.SysLogEvent;
+import com.kxs.common.log.event.SysLogEventSource;
+import com.kxs.common.log.util.LogTypeEnum;
+import com.kxs.common.log.util.SysLogUtils;
+import lombok.RequiredArgsConstructor;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.expression.EvaluationContext;
+
+/**
+ * 操作日志使用spring event异步入库
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/28
+ */
+@Aspect
+@Slf4j
+@RequiredArgsConstructor
+public class SysLogAspect {
+
+	@Around("@annotation(sysLog)")
+	@SneakyThrows
+	public Object around(ProceedingJoinPoint point, com.kxs.common.log.annotation.SysLog sysLog) {
+		String strClassName = point.getTarget().getClass().getName();
+		String strMethodName = point.getSignature().getName();
+		log.debug("[类名]:{},[方法]:{}", strClassName, strMethodName);
+
+		String value = sysLog.value();
+		String expression = sysLog.expression();
+		// 当前表达式存在 SPEL,会覆盖 value 的值
+		if (CharSequenceUtil.isNotBlank(expression)) {
+			// 解析SPEL
+			MethodSignature signature = (MethodSignature) point.getSignature();
+			EvaluationContext context = SysLogUtils.getContext(point.getArgs(), signature.getMethod());
+			try {
+				value = SysLogUtils.getValue(context, expression, String.class);
+			}
+			catch (Exception e) {
+				// SPEL 表达式异常,获取 value 的值
+				log.error("@SysLog 解析SPEL {} 异常", expression);
+			}
+		}
+
+		SysLogEventSource logVo = SysLogUtils.getSysLog();
+		logVo.setTitle(value);
+		// 获取请求body参数
+		if (CharSequenceUtil.isBlank(logVo.getParams())) {
+			logVo.setBody(point.getArgs());
+		}
+		// 发送异步日志事件
+		Long startTime = System.currentTimeMillis();
+		Object obj;
+
+		try {
+			obj = point.proceed();
+		}
+		catch (Exception e) {
+			logVo.setLogType(LogTypeEnum.ERROR.getType());
+			logVo.setException(e.getMessage());
+			throw e;
+		}
+		finally {
+			Long endTime = System.currentTimeMillis();
+			logVo.setTime(endTime - startTime);
+			SpringContextHolder.publishEvent(new SysLogEvent(logVo));
+		}
+
+		return obj;
+	}
+
+}

+ 39 - 0
kxs-common/kxs-common-log/src/main/java/com/kxs/common/log/config/SkyLogProperties.java

@@ -0,0 +1,39 @@
+package com.kxs.common.log.config;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+import java.util.List;
+
+/**
+ * 日志配置类
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/28
+ */
+@Getter
+@Setter
+@ConfigurationProperties(SkyLogProperties.PREFIX)
+public class SkyLogProperties {
+
+	public static final String PREFIX = "security.log";
+
+	/**
+	 * 开启日志记录
+	 */
+	private boolean enabled = true;
+
+	/**
+	 * 放行字段,password,mobile,idcard,phone
+	 */
+	@Value("${security.log.exclude-fields:password,mobile,idcard,phone}")
+	private List<String> excludeFields;
+
+	/**
+	 * 请求报文最大存储长度
+	 */
+	private Integer maxLength = 2000;
+
+}

+ 18 - 0
kxs-common/kxs-common-log/src/main/java/com/kxs/common/log/event/SysLogEvent.java

@@ -0,0 +1,18 @@
+package com.kxs.common.log.event;
+
+import com.kxs.lhb.system.api.model.SysLog;
+import org.springframework.context.ApplicationEvent;
+
+/**
+ * 系统日志事件
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/28
+ */
+public class SysLogEvent extends ApplicationEvent {
+
+	public SysLogEvent(SysLog source) {
+		super(source);
+	}
+
+}

+ 22 - 0
kxs-common/kxs-common-log/src/main/java/com/kxs/common/log/event/SysLogEventSource.java

@@ -0,0 +1,22 @@
+package com.kxs.common.log.event;
+
+import com.kxs.lhb.system.api.model.SysLog;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * sys日志事件源
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/28
+ */
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class SysLogEventSource extends SysLog {
+
+	/**
+	 * 参数重写成object
+	 */
+	private Object body;
+
+}

+ 76 - 0
kxs-common/kxs-common-log/src/main/java/com/kxs/common/log/event/SysLogListener.java

@@ -0,0 +1,76 @@
+package com.kxs.common.log.event;
+
+import cn.hutool.core.text.CharSequenceUtil;
+import com.fasterxml.jackson.annotation.JsonFilter;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ser.FilterProvider;
+import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
+import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
+import com.kxs.common.core.constant.SecurityConstants;
+import com.kxs.common.core.jackson.SkyJavaTimeModule;
+import com.kxs.common.log.config.SkyLogProperties;
+import com.kxs.lhb.system.api.feign.RemoteLogService;
+import com.kxs.lhb.system.api.model.SysLog;
+import lombok.RequiredArgsConstructor;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.context.event.EventListener;
+import org.springframework.core.annotation.Order;
+import org.springframework.scheduling.annotation.Async;
+
+import java.util.Objects;
+
+/**
+ * 异步监听日志事件
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/28
+ */
+@Slf4j
+@RequiredArgsConstructor
+public class SysLogListener implements InitializingBean {
+
+	// new 一个 避免日志脱敏策略影响全局ObjectMapper
+	private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+	private final RemoteLogService remoteLogService;
+
+	private final SkyLogProperties logProperties;
+
+	@SneakyThrows
+	@Async
+	@Order
+	@EventListener(SysLogEvent.class)
+	public void saveSysLog(SysLogEvent event) {
+		SysLogEventSource source = (SysLogEventSource) event.getSource();
+		SysLog sysLog = new SysLog();
+		BeanUtils.copyProperties(source, sysLog);
+
+		// json 格式刷参数放在异步中处理,提升性能
+		if (Objects.nonNull(source.getBody())) {
+			String params = OBJECT_MAPPER.writeValueAsString(source.getBody());
+			sysLog.setParams(CharSequenceUtil.subPre(params, logProperties.getMaxLength()));
+		}
+		log.info("保存日志:{}", sysLog);
+		remoteLogService.saveLog(sysLog, SecurityConstants.FROM_IN);
+	}
+
+	@Override
+	public void afterPropertiesSet() {
+		OBJECT_MAPPER.addMixIn(Object.class, PropertyFilterMixIn.class);
+		String[] ignorableFieldNames = logProperties.getExcludeFields().toArray(new String[0]);
+
+		FilterProvider filters = new SimpleFilterProvider().addFilter("filter properties by name",
+				SimpleBeanPropertyFilter.serializeAllExcept(ignorableFieldNames));
+		OBJECT_MAPPER.setFilterProvider(filters);
+		OBJECT_MAPPER.registerModule(new SkyJavaTimeModule());
+	}
+
+	@JsonFilter("filter properties by name")
+	static class PropertyFilterMixIn {
+
+	}
+
+}

+ 34 - 0
kxs-common/kxs-common-log/src/main/java/com/kxs/common/log/init/ApplicationLoggerInitializer.java

@@ -0,0 +1,34 @@
+package com.kxs.common.log.init;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.env.EnvironmentPostProcessor;
+import org.springframework.core.Ordered;
+import org.springframework.core.env.ConfigurableEnvironment;
+
+/**
+ * <p>
+ * 通过环境变量的形式注入 logging.file 自动维护 Spring Boot Admin Logger Viewer
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/28
+ */
+public class ApplicationLoggerInitializer implements EnvironmentPostProcessor, Ordered {
+
+	@Override
+	public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
+		String appName = environment.getProperty("spring.application.name");
+		String logBase = environment.getProperty("LOGGING_PATH", "logs");
+
+		// spring boot admin 直接加载日志
+		System.setProperty("logging.file.name", String.format("%s/%s/debug.log", logBase, appName));
+
+		// 避免 sentinel 1.8.4+ 心跳日志过大
+		System.setProperty("csp.sentinel.log.level", "OFF");
+	}
+
+	@Override
+	public int getOrder() {
+		return Ordered.LOWEST_PRECEDENCE;
+	}
+
+}

+ 37 - 0
kxs-common/kxs-common-log/src/main/java/com/kxs/common/log/util/LogTypeEnum.java

@@ -0,0 +1,37 @@
+package com.kxs.common.log.util;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * <p>
+ * 日志类型
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/28
+ */
+@Getter
+@RequiredArgsConstructor
+public enum LogTypeEnum {
+
+	/**
+	 * 正常日志类型
+	 */
+	NORMAL("0", "正常日志"),
+
+	/**
+	 * 错误日志类型
+	 */
+	ERROR("9", "错误日志");
+
+	/**
+	 * 类型
+	 */
+	private final String type;
+
+	/**
+	 * 描述
+	 */
+	private final String description;
+
+}

+ 123 - 0
kxs-common/kxs-common-log/src/main/java/com/kxs/common/log/util/SysLogUtils.java

@@ -0,0 +1,123 @@
+package com.kxs.common.log.util;
+
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.core.util.URLUtil;
+import cn.hutool.extra.servlet.JakartaServletUtil;
+import cn.hutool.http.HttpUtil;
+import com.kxs.common.core.constant.SecurityConstants;
+import com.kxs.common.core.util.SpringContextHolder;
+import com.kxs.common.log.config.SkyLogProperties;
+import com.kxs.common.log.event.SysLogEventSource;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.experimental.UtilityClass;
+import org.springframework.core.StandardReflectionParameterNameDiscoverer;
+import org.springframework.expression.EvaluationContext;
+import org.springframework.expression.Expression;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.expression.spel.support.StandardEvaluationContext;
+import org.springframework.http.HttpHeaders;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * 系统日志工具类
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/28
+ */
+@UtilityClass
+public class SysLogUtils {
+
+	public SysLogEventSource getSysLog() {
+		HttpServletRequest request = ((ServletRequestAttributes) Objects
+			.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
+		SysLogEventSource sysLog = new SysLogEventSource();
+		sysLog.setLogType(LogTypeEnum.NORMAL.getType());
+		sysLog.setRequestUri(URLUtil.getPath(request.getRequestURI()));
+		sysLog.setMethod(request.getMethod());
+		sysLog.setRemoteAddr(JakartaServletUtil.getClientIP(request));
+		sysLog.setUserAgent(request.getHeader(HttpHeaders.USER_AGENT));
+		sysLog.setCreateBy(getUsername());
+		sysLog.setServiceId(getClientId());
+
+		// get 参数脱敏
+		SkyLogProperties logProperties = SpringContextHolder.getBean(SkyLogProperties.class);
+		HashMap<String, String[]> requestMap = new HashMap<>(request.getParameterMap());
+		Map<String, String[]> paramsMap = MapUtil.removeAny(requestMap,
+				ArrayUtil.toArray(logProperties.getExcludeFields(), String.class));
+		sysLog.setParams(HttpUtil.toParams(paramsMap));
+		return sysLog;
+	}
+
+	/**
+	 * 获取用户名称
+	 * @return username
+	 */
+	private String getUsername() {
+		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+		if (authentication == null) {
+			return null;
+		}
+		return authentication.getName();
+	}
+
+	/**
+	 * 获取spel 定义的参数值
+	 * @param context 参数容器
+	 * @param key key
+	 * @param clazz 需要返回的类型
+	 * @param <T> 返回泛型
+	 * @return 参数值
+	 */
+	public <T> T getValue(EvaluationContext context, String key, Class<T> clazz) {
+		SpelExpressionParser spelExpressionParser = new SpelExpressionParser();
+		Expression expression = spelExpressionParser.parseExpression(key);
+		return expression.getValue(context, clazz);
+	}
+
+	/**
+	 * 获取参数容器
+	 * @param arguments 方法的参数列表
+	 * @param signatureMethod 被执行的方法体
+	 * @return 装载参数的容器
+	 */
+	public EvaluationContext getContext(Object[] arguments, Method signatureMethod) {
+		String[] parameterNames = new StandardReflectionParameterNameDiscoverer().getParameterNames(signatureMethod);
+		EvaluationContext context = new StandardEvaluationContext();
+		if (parameterNames == null) {
+			return context;
+		}
+		for (int i = 0; i < arguments.length; i++) {
+			context.setVariable(parameterNames[i], arguments[i]);
+		}
+		return context;
+	}
+
+	/**
+	 * 获取客户端
+	 * @return clientId
+	 */
+	private String getClientId() {
+		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+		if (authentication == null) {
+			return null;
+		}
+
+		Object principal = authentication.getPrincipal();
+		if (principal instanceof OAuth2AuthenticatedPrincipal) {
+			OAuth2AuthenticatedPrincipal auth2Authentication = (OAuth2AuthenticatedPrincipal) principal;
+			return MapUtil.getStr(auth2Authentication.getAttributes(), SecurityConstants.CLIENT_ID);
+		}
+		return null;
+	}
+
+}

+ 30 - 0
kxs-common/kxs-common-log/src/main/resources/META-INF/spring-configuration-metadata.json

@@ -0,0 +1,30 @@
+{
+  "groups": [
+    {
+      "name": "security.log",
+      "type": "com.kxs.common.log.config.SkyLogProperties",
+      "sourceType": "com.kxs.common.log.config.SkyLogProperties"
+    }
+  ],
+  "properties": [
+    {
+      "name": "security.log.enabled",
+      "type": "java.lang.Boolean",
+      "description": "开启日志记录",
+      "sourceType": "com.kxs.common.log.config.SkyLogProperties"
+    },
+    {
+      "name": "security.log.exclude-fields",
+      "type": "java.util.List<java.lang.String>",
+      "description": "放行字段,password,mobile,idcard,phone",
+      "sourceType": "com.kxs.common.log.config.SkyLogProperties"
+    },
+    {
+      "name": "security.log.max-length",
+      "type": "java.lang.Integer",
+      "description": "请求报文最大存储长度",
+      "sourceType": "com.kxs.common.log.config.SkyLogProperties"
+    }
+  ],
+  "hints": []
+}

+ 1 - 0
kxs-common/kxs-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

@@ -0,0 +1 @@
+com.kxs.common.log.LogAutoConfiguration

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است