Browse Source

:tada: 2.3.2.RELEASE

smallchill 5 years ago
parent
commit
1d6f118774
32 changed files with 1738 additions and 90 deletions
  1. 1 1
      blade-core-boot/pom.xml
  2. 1 1
      blade-core-boot/src/main/resources/bootstrap.yml
  3. 1 1
      blade-core-cloud/pom.xml
  4. 1 1
      blade-core-develop/pom.xml
  5. 1 1
      blade-core-launch/pom.xml
  6. 5 2
      blade-core-launch/src/main/java/org/springblade/core/launch/BladeApplication.java
  7. 5 1
      blade-core-launch/src/main/java/org/springblade/core/launch/constant/AppConstant.java
  8. 23 1
      blade-core-launch/src/main/java/org/springblade/core/launch/service/LauncherService.java
  9. 1 1
      blade-core-log/pom.xml
  10. 1 1
      blade-core-mybatis/pom.xml
  11. 32 0
      blade-core-oss/pom.xml
  12. 248 0
      blade-core-oss/src/main/java/org/springblade/core/oss/QiniuTemplate.java
  13. 83 0
      blade-core-oss/src/main/java/org/springblade/core/oss/config/OssConfiguration.java
  14. 35 0
      blade-core-oss/src/main/java/org/springblade/core/oss/model/BladeFile.java
  15. 53 0
      blade-core-oss/src/main/java/org/springblade/core/oss/model/OssFile.java
  16. 65 0
      blade-core-oss/src/main/java/org/springblade/core/oss/props/OssProperties.java
  17. 42 0
      blade-core-oss/src/main/java/org/springblade/core/oss/rule/BladeOssRule.java
  18. 41 0
      blade-core-oss/src/main/java/org/springblade/core/oss/rule/OssRule.java
  19. 1 1
      blade-core-secure/pom.xml
  20. 1 1
      blade-core-swagger/pom.xml
  21. 1 1
      blade-core-swagger/src/main/java/org/springblade/core/swagger/SwaggerProperties.java
  22. 31 0
      blade-core-test/pom.xml
  23. 30 0
      blade-core-test/src/main/java/org/springblade/core/test/BladeBaseTest.java
  24. 57 0
      blade-core-test/src/main/java/org/springblade/core/test/BladeBootTest.java
  25. 29 0
      blade-core-test/src/main/java/org/springblade/core/test/BladeBootTestException.java
  26. 80 0
      blade-core-test/src/main/java/org/springblade/core/test/BladeSpringRunner.java
  27. 12 12
      blade-core-tool/pom.xml
  28. 36 0
      blade-core-tool/src/main/java/org/springblade/core/tool/utils/CharPool.java
  29. 429 59
      blade-core-tool/src/main/java/org/springblade/core/tool/utils/DateUtil.java
  30. 385 0
      blade-core-tool/src/main/java/org/springblade/core/tool/utils/FileUtil.java
  31. 1 1
      blade-core-tool/src/main/java/org/springblade/core/tool/utils/StringPool.java
  32. 6 4
      pom.xml

+ 1 - 1
blade-core-boot/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <groupId>org.springblade</groupId>
         <artifactId>blade-tool</artifactId>
-        <version>2.3.1</version>
+        <version>2.3.2</version>
     </parent>
 
     <modelVersion>4.0.0</modelVersion>

+ 1 - 1
blade-core-boot/src/main/resources/bootstrap.yml

@@ -75,7 +75,7 @@ mybatis-plus:
 swagger:
   title: SpringBlade 接口文档系统
   description: SpringBlade 接口文档系统
-  version: 2.3.1
+  version: 2.3.2
   license: Powered By SpringBlade
   licenseUrl: https://bladex.vip
   terms-of-service-url: https://bladex.vip

+ 1 - 1
blade-core-cloud/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <artifactId>blade-tool</artifactId>
         <groupId>org.springblade</groupId>
-        <version>2.3.1</version>
+        <version>2.3.2</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 

+ 1 - 1
blade-core-develop/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <artifactId>blade-tool</artifactId>
         <groupId>org.springblade</groupId>
-        <version>2.3.1</version>
+        <version>2.3.2</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 

+ 1 - 1
blade-core-launch/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <artifactId>blade-tool</artifactId>
         <groupId>org.springblade</groupId>
-        <version>2.3.1</version>
+        <version>2.3.2</version>
     </parent>
 
     <modelVersion>4.0.0</modelVersion>

+ 5 - 2
blade-core-launch/src/main/java/org/springblade/core/launch/BladeApplication.java

@@ -27,6 +27,7 @@ import org.springframework.util.StringUtils;
 
 import java.util.*;
 import java.util.function.Function;
+import java.util.stream.Collectors;
 
 /**
  * 项目启动器,搞定环境变量问题
@@ -100,8 +101,10 @@ public class BladeApplication {
 		props.setProperty("spring.cloud.nacos.config.file-extension", NacosConstant.NACOS_CONFIG_FORMAT);
 		props.setProperty("spring.cloud.sentinel.transport.dashboard", SentinelConstant.SENTINEL_ADDR);
 		// 加载自定义组件
-		ServiceLoader<LauncherService> loader = ServiceLoader.load(LauncherService.class);
-		loader.forEach(launcherService -> launcherService.launcher(builder, appName, profile));
+		List<LauncherService> launcherList = new ArrayList<>();
+		ServiceLoader.load(LauncherService.class).forEach(launcherList::add);
+		launcherList.stream().sorted(Comparator.comparing(LauncherService::getOrder)).collect(Collectors.toList())
+			.forEach(launcherService -> launcherService.launcher(builder, appName, profile));
 		return builder;
 	}
 

+ 5 - 1
blade-core-launch/src/main/java/org/springblade/core/launch/constant/AppConstant.java

@@ -25,7 +25,7 @@ public interface AppConstant {
 	/**
 	 * 应用版本
 	 */
-	String APPLICATION_VERSION = "2.3.1";
+	String APPLICATION_VERSION = "2.3.2";
 
 	/**
 	 * 基础包
@@ -68,6 +68,10 @@ public interface AppConstant {
 	 * 开发模块名称
 	 */
 	String APPLICATION_DEVELOP_NAME = APPLICATION_NAME_PREFIX + "develop";
+	/**
+	 * 资源模块名称
+	 */
+	String APPLICATION_RESOURCE_NAME = APPLICATION_NAME_PREFIX + "resource";
 	/**
 	 * 测试模块名称
 	 */

+ 23 - 1
blade-core-launch/src/main/java/org/springblade/core/launch/service/LauncherService.java

@@ -16,13 +16,14 @@
 package org.springblade.core.launch.service;
 
 import org.springframework.boot.builder.SpringApplicationBuilder;
+import org.springframework.core.Ordered;
 
 /**
  * launcher 扩展 用于一些组件发现
  *
  * @author Chill
  */
-public interface LauncherService {
+public interface LauncherService extends Ordered, Comparable<LauncherService>  {
 
 	/**
 	 * 启动时 处理 SpringApplicationBuilder
@@ -33,4 +34,25 @@ public interface LauncherService {
 	 */
 	void launcher(SpringApplicationBuilder builder, String appName, String profile);
 
+	/**
+	 * 获取排列顺序
+	 *
+	 * @return order
+	 */
+	@Override
+	default int getOrder() {
+		return Ordered.HIGHEST_PRECEDENCE;
+	}
+
+	/**
+	 * 对比排序
+	 *
+	 * @param o LauncherService
+	 * @return compare
+	 */
+	@Override
+	default int compareTo(LauncherService o) {
+		return Integer.compare(this.getOrder(), o.getOrder());
+	}
+
 }

+ 1 - 1
blade-core-log/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <artifactId>blade-tool</artifactId>
         <groupId>org.springblade</groupId>
-        <version>2.3.1</version>
+        <version>2.3.2</version>
     </parent>
 
     <modelVersion>4.0.0</modelVersion>

+ 1 - 1
blade-core-mybatis/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <artifactId>blade-tool</artifactId>
         <groupId>org.springblade</groupId>
-        <version>2.3.1</version>
+        <version>2.3.2</version>
     </parent>
 
     <modelVersion>4.0.0</modelVersion>

+ 32 - 0
blade-core-oss/pom.xml

@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>blade-tool</artifactId>
+        <groupId>org.springblade</groupId>
+        <version>2.3.2</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>blade-core-oss</artifactId>
+    <name>${project.artifactId}</name>
+    <version>${blade.tool.version}</version>
+    <packaging>jar</packaging>
+
+    <dependencies>
+        <!--Blade-->
+        <dependency>
+            <groupId>org.springblade</groupId>
+            <artifactId>blade-core-tool</artifactId>
+            <version>${blade.tool.version}</version>
+        </dependency>
+        <!--QiNiu-->
+        <dependency>
+            <groupId>com.qiniu</groupId>
+            <artifactId>qiniu-java-sdk</artifactId>
+            <version>7.2.18</version>
+        </dependency>
+    </dependencies>
+
+</project>

+ 248 - 0
blade-core-oss/src/main/java/org/springblade/core/oss/QiniuTemplate.java

@@ -0,0 +1,248 @@
+/**
+ * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com).
+ * <p>
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * <p>
+ * http://www.gnu.org/licenses/lgpl.html
+ * <p>
+ * 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.
+ */
+package org.springblade.core.oss;
+
+import com.qiniu.common.Zone;
+import com.qiniu.http.Response;
+import com.qiniu.storage.BucketManager;
+import com.qiniu.storage.UploadManager;
+import com.qiniu.storage.model.FileInfo;
+import com.qiniu.util.Auth;
+import lombok.AllArgsConstructor;
+import lombok.SneakyThrows;
+import org.springblade.core.oss.model.BladeFile;
+import org.springblade.core.oss.model.OssFile;
+import org.springblade.core.oss.props.OssProperties;
+import org.springblade.core.oss.rule.OssRule;
+import org.springblade.core.tool.utils.CollectionUtil;
+import org.springblade.core.tool.utils.Func;
+import org.springblade.core.tool.utils.StringPool;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.InputStream;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * QiniuTemplate
+ *
+ * @author Chill
+ */
+@AllArgsConstructor
+public class QiniuTemplate {
+	private Auth auth;
+	private UploadManager uploadManager;
+	private BucketManager bucketManager;
+	private OssProperties ossProperties;
+	private OssRule ossRule;
+
+
+	@SneakyThrows
+	public void makeBucket(String bucketName) {
+		if (!CollectionUtil.contains(bucketManager.buckets(), getBucketName(bucketName))) {
+			bucketManager.createBucket(getBucketName(bucketName), Zone.zone0().getRegion());
+		}
+	}
+
+
+	@SneakyThrows
+	public void removeBucket(String bucketName) {
+		bucketManager.deleteBucket(getBucketName(bucketName));
+	}
+
+
+	@SneakyThrows
+	public boolean bucketExists(String bucketName) {
+		return CollectionUtil.contains(bucketManager.buckets(), getBucketName(bucketName));
+	}
+
+
+	@SneakyThrows
+	public void copyFile(String bucketName, String fileName, String destBucketName) {
+		bucketManager.copy(getBucketName(bucketName), fileName, getBucketName(destBucketName), fileName);
+	}
+
+
+	@SneakyThrows
+	public void copyFile(String bucketName, String fileName, String destBucketName, String destFileName) {
+		bucketManager.copy(getBucketName(bucketName), fileName, getBucketName(destBucketName), destFileName);
+	}
+
+
+	@SneakyThrows
+	public OssFile statFile(String fileName) {
+		return statFile(ossProperties.getBucketName(), fileName);
+	}
+
+
+	@SneakyThrows
+	public OssFile statFile(String bucketName, String fileName) {
+		FileInfo stat = bucketManager.stat(getBucketName(bucketName), fileName);
+		OssFile ossFile = new OssFile();
+		ossFile.setName(stat.key);
+		ossFile.setName(Func.isEmpty(stat.key) ? fileName : stat.key);
+		ossFile.setLink(fileLink(ossFile.getName()));
+		ossFile.setHash(stat.hash);
+		ossFile.setLength(stat.fsize);
+		ossFile.setPutTime(new Date(stat.putTime / 10000));
+		ossFile.setContentType(stat.mimeType);
+		return ossFile;
+	}
+
+
+	@SneakyThrows
+	public String filePath(String fileName) {
+		return getBucketName().concat(StringPool.SLASH).concat(fileName);
+	}
+
+
+	@SneakyThrows
+	public String filePath(String bucketName, String fileName) {
+		return getBucketName(bucketName).concat(StringPool.SLASH).concat(fileName);
+	}
+
+
+	@SneakyThrows
+	public String fileLink(String fileName) {
+		return ossProperties.getEndpoint().concat(StringPool.SLASH).concat(fileName);
+	}
+
+
+	@SneakyThrows
+	public String fileLink(String bucketName, String fileName) {
+		return ossProperties.getEndpoint().concat(StringPool.SLASH).concat(fileName);
+	}
+
+
+	@SneakyThrows
+	public BladeFile putFile(MultipartFile file) {
+		return putFile(ossProperties.getBucketName(), file.getOriginalFilename(), file);
+	}
+
+
+	@SneakyThrows
+	public BladeFile putFile(String fileName, MultipartFile file) {
+		return putFile(ossProperties.getBucketName(), fileName, file);
+	}
+
+
+	@SneakyThrows
+	public BladeFile putFile(String bucketName, String fileName, MultipartFile file) {
+		return putFile(bucketName, fileName, file);
+	}
+
+
+	@SneakyThrows
+	public BladeFile putFile(String fileName, InputStream stream) {
+		return putFile(ossProperties.getBucketName(), fileName, stream);
+	}
+
+
+	@SneakyThrows
+	public BladeFile putFile(String bucketName, String fileName, InputStream stream) {
+		return put(bucketName, stream, fileName, false);
+	}
+
+	@SneakyThrows
+	public BladeFile put(String bucketName, InputStream stream, String key, boolean cover) {
+		makeBucket(bucketName);
+		key = getFileName(key);
+		// 覆盖上传
+		if (cover) {
+			uploadManager.put(stream, key, getUploadToken(bucketName, key), null, null);
+		} else {
+			Response response = uploadManager.put(stream, key, getUploadToken(bucketName), null, null);
+			int retry = 0;
+			int retryCount = 5;
+			while (response.needRetry() && retry < retryCount) {
+				response = uploadManager.put(stream, key, getUploadToken(bucketName), null, null);
+				retry++;
+			}
+		}
+		BladeFile file = new BladeFile();
+		file.setName(key);
+		file.setLink(fileLink(bucketName, key));
+		return file;
+	}
+
+
+	@SneakyThrows
+	public void removeFile(String fileName) {
+		bucketManager.delete(getBucketName(), fileName);
+	}
+
+
+	@SneakyThrows
+	public void removeFile(String bucketName, String fileName) {
+		bucketManager.delete(getBucketName(bucketName), fileName);
+	}
+
+
+	@SneakyThrows
+	public void removeFiles(List<String> fileNames) {
+		fileNames.forEach(this::removeFile);
+	}
+
+
+	@SneakyThrows
+	public void removeFiles(String bucketName, List<String> fileNames) {
+		fileNames.forEach(fileName -> removeFile(getBucketName(bucketName), fileName));
+	}
+
+	/**
+	 * 根据规则生成存储桶名称规则
+	 *
+	 * @return String
+	 */
+	private String getBucketName() {
+		return getBucketName(ossProperties.getBucketName());
+	}
+
+	/**
+	 * 根据规则生成存储桶名称规则
+	 *
+	 * @param bucketName 存储桶名称
+	 * @return String
+	 */
+	private String getBucketName(String bucketName) {
+		return ossRule.bucketName(bucketName);
+	}
+
+	/**
+	 * 根据规则生成文件名称规则
+	 *
+	 * @param originalFilename 原始文件名
+	 * @return string
+	 */
+	private String getFileName(String originalFilename) {
+		return ossRule.fileName(originalFilename);
+	}
+
+	/**
+	 * 获取上传凭证,普通上传
+	 */
+	public String getUploadToken(String bucketName) {
+		return auth.uploadToken(getBucketName(bucketName));
+	}
+
+	/**
+	 * 获取上传凭证,覆盖上传
+	 */
+	private String getUploadToken(String bucketName, String key) {
+		return auth.uploadToken(getBucketName(bucketName), key);
+	}
+
+}

+ 83 - 0
blade-core-oss/src/main/java/org/springblade/core/oss/config/OssConfiguration.java

@@ -0,0 +1,83 @@
+/**
+ * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com).
+ * <p>
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * <p>
+ * http://www.gnu.org/licenses/lgpl.html
+ * <p>
+ * 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.
+ */
+package org.springblade.core.oss.config;
+
+import com.qiniu.common.Zone;
+import com.qiniu.storage.BucketManager;
+import com.qiniu.storage.UploadManager;
+import com.qiniu.util.Auth;
+import lombok.AllArgsConstructor;
+import org.springblade.core.oss.QiniuTemplate;
+import org.springblade.core.oss.props.OssProperties;
+import org.springblade.core.oss.rule.BladeOssRule;
+import org.springblade.core.oss.rule.OssRule;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+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;
+
+/**
+ * Oss配置类
+ *
+ * @author Chill
+ */
+@Configuration
+@AllArgsConstructor
+@EnableConfigurationProperties(OssProperties.class)
+@ConditionalOnProperty(value = "oss.enable", havingValue = "true")
+public class OssConfiguration {
+
+	private OssProperties ossProperties;
+
+	@Bean
+	@ConditionalOnMissingBean(OssRule.class)
+	public OssRule ossRule() {
+		return new BladeOssRule();
+	}
+
+	@Bean
+	public com.qiniu.storage.Configuration qiniuConfiguration() {
+		return new com.qiniu.storage.Configuration(Zone.zone0());
+	}
+
+	@Bean
+	public Auth auth() {
+		return Auth.create(ossProperties.getAccessKey(), ossProperties.getSecretKey());
+	}
+
+	@Bean
+	@ConditionalOnBean(com.qiniu.storage.Configuration.class)
+	public UploadManager uploadManager(com.qiniu.storage.Configuration cfg) {
+		return new UploadManager(cfg);
+	}
+
+	@Bean
+	@ConditionalOnBean(com.qiniu.storage.Configuration.class)
+	public BucketManager bucketManager(com.qiniu.storage.Configuration cfg) {
+		return new BucketManager(auth(), cfg);
+	}
+
+	@Bean
+	@ConditionalOnMissingBean(QiniuTemplate.class)
+	@ConditionalOnBean({Auth.class, UploadManager.class, BucketManager.class, OssRule.class})
+	public QiniuTemplate qiniuTemplate(Auth auth, UploadManager uploadManager, BucketManager bucketManager, OssRule ossRule) {
+		return new QiniuTemplate(auth, uploadManager, bucketManager, ossProperties, ossRule);
+	}
+
+
+}

+ 35 - 0
blade-core-oss/src/main/java/org/springblade/core/oss/model/BladeFile.java

@@ -0,0 +1,35 @@
+/**
+ * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com).
+ * <p>
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * <p>
+ * http://www.gnu.org/licenses/lgpl.html
+ * <p>
+ * 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.
+ */
+package org.springblade.core.oss.model;
+
+import lombok.Data;
+
+/**
+ * BladeFile
+ *
+ * @author Chill
+ */
+@Data
+public class BladeFile {
+	/**
+	 * 文件地址
+	 */
+	private String link;
+	/**
+	 * 文件名
+	 */
+	private String name;
+}

+ 53 - 0
blade-core-oss/src/main/java/org/springblade/core/oss/model/OssFile.java

@@ -0,0 +1,53 @@
+/**
+ * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com).
+ * <p>
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * <p>
+ * http://www.gnu.org/licenses/lgpl.html
+ * <p>
+ * 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.
+ */
+package org.springblade.core.oss.model;
+
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * OssFile
+ *
+ * @author Chill
+ */
+@Data
+public class OssFile {
+	/**
+	 * 文件地址
+	 */
+	private String link;
+	/**
+	 * 文件名
+	 */
+	private String name;
+	/**
+	 * 文件hash值
+	 */
+	public String hash;
+	/**
+	 * 文件大小
+	 */
+	private long length;
+	/**
+	 * 文件上传时间
+	 */
+	private Date putTime;
+	/**
+	 * 文件contentType
+	 */
+	private String contentType;
+}

+ 65 - 0
blade-core-oss/src/main/java/org/springblade/core/oss/props/OssProperties.java

@@ -0,0 +1,65 @@
+/**
+ * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com).
+ * <p>
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * <p>
+ * http://www.gnu.org/licenses/lgpl.html
+ * <p>
+ * 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.
+ */
+package org.springblade.core.oss.props;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * Minio参数配置类
+ *
+ * @author Chill
+ */
+@Data
+@ConfigurationProperties(prefix = "oss")
+public class OssProperties {
+
+	/**
+	 * 是否启用
+	 */
+	private Boolean enable;
+
+	/**
+	 * 对象存储名称
+	 */
+	private String name;
+
+	/**
+	 * 是否开启租户模式
+	 */
+	private Boolean tenantMode;
+
+	/**
+	 * 对象存储服务的URL
+	 */
+	private String endpoint;
+
+	/**
+	 * Access key就像用户ID,可以唯一标识你的账户
+	 */
+	private String accessKey;
+
+	/**
+	 * Secret key是你账户的密码
+	 */
+	private String secretKey;
+
+	/**
+	 * 默认的存储桶名称
+	 */
+	private String bucketName = "bladex";
+
+}

+ 42 - 0
blade-core-oss/src/main/java/org/springblade/core/oss/rule/BladeOssRule.java

@@ -0,0 +1,42 @@
+/**
+ * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com).
+ * <p>
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * <p>
+ * http://www.gnu.org/licenses/lgpl.html
+ * <p>
+ * 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.
+ */
+package org.springblade.core.oss.rule;
+
+import lombok.AllArgsConstructor;
+import org.springblade.core.tool.utils.DateUtil;
+import org.springblade.core.tool.utils.FileUtil;
+import org.springblade.core.tool.utils.StringPool;
+import org.springblade.core.tool.utils.StringUtil;
+
+/**
+ * 默认存储桶生成规则
+ *
+ * @author Chill
+ */
+@AllArgsConstructor
+public class BladeOssRule implements OssRule {
+
+	@Override
+	public String bucketName(String bucketName) {
+		return bucketName;
+	}
+
+	@Override
+	public String fileName(String originalFilename) {
+		return "upload" + StringPool.SLASH + DateUtil.today() + StringPool.SLASH + StringUtil.randomUUID() + StringPool.DOT + FileUtil.getFileExtension(originalFilename);
+	}
+
+}

+ 41 - 0
blade-core-oss/src/main/java/org/springblade/core/oss/rule/OssRule.java

@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com).
+ * <p>
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * <p>
+ * http://www.gnu.org/licenses/lgpl.html
+ * <p>
+ * 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.
+ */
+package org.springblade.core.oss.rule;
+
+/**
+ * Oss通用规则
+ *
+ * @author Chill
+ */
+public interface OssRule {
+
+	/**
+	 * 获取存储桶规则
+	 *
+	 * @param bucketName 存储桶名称
+	 * @return String
+	 */
+	String bucketName(String bucketName);
+
+	/**
+	 * 获取文件名规则
+	 *
+	 * @param originalFilename 文件名
+	 * @return String
+	 */
+	String fileName(String originalFilename);
+
+}

+ 1 - 1
blade-core-secure/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <artifactId>blade-tool</artifactId>
         <groupId>org.springblade</groupId>
-        <version>2.3.1</version>
+        <version>2.3.2</version>
     </parent>
 
     <modelVersion>4.0.0</modelVersion>

+ 1 - 1
blade-core-swagger/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <artifactId>blade-tool</artifactId>
         <groupId>org.springblade</groupId>
-        <version>2.3.1</version>
+        <version>2.3.2</version>
     </parent>
 
     <modelVersion>4.0.0</modelVersion>

+ 1 - 1
blade-core-swagger/src/main/java/org/springblade/core/swagger/SwaggerProperties.java

@@ -53,7 +53,7 @@ public class SwaggerProperties {
 	/**
 	 * 版本
 	 **/
-	private String version = "2.3.1";
+	private String version = "2.3.2";
 	/**
 	 * 许可证
 	 **/

+ 31 - 0
blade-core-test/pom.xml

@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <groupId>org.springblade</groupId>
+        <artifactId>blade-tool</artifactId>
+        <version>2.3.2</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>blade-core-test</artifactId>
+    <name>${project.artifactId}</name>
+    <version>${blade.tool.version}</version>
+    <packaging>jar</packaging>
+
+    <dependencies>
+        <!-- Blade -->
+        <dependency>
+            <groupId>org.springblade</groupId>
+            <artifactId>blade-core-launch</artifactId>
+            <version>${blade.tool.version}</version>
+        </dependency>
+        <!-- Test -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+        </dependency>
+    </dependencies>
+
+</project>

+ 30 - 0
blade-core-test/src/main/java/org/springblade/core/test/BladeBaseTest.java

@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com).
+ * <p>
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * <p>
+ * http://www.gnu.org/licenses/lgpl.html
+ * <p>
+ * 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.
+ */
+
+package org.springblade.core.test;
+
+import org.junit.runner.RunWith;
+import org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests;
+
+/**
+ * boot test 基类
+ *
+ * @author L.cm
+ */
+@RunWith(BladeSpringRunner.class)
+public abstract class BladeBaseTest extends AbstractJUnit4SpringContextTests {
+
+}

+ 57 - 0
blade-core-test/src/main/java/org/springblade/core/test/BladeBootTest.java

@@ -0,0 +1,57 @@
+/**
+ * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com).
+ * <p>
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * <p>
+ * http://www.gnu.org/licenses/lgpl.html
+ * <p>
+ * 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.
+ */
+
+package org.springblade.core.test;
+
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.core.annotation.AliasFor;
+
+import java.lang.annotation.*;
+
+/**
+ * 简化 测试
+ *
+ * @author L.cm
+ */
+@Target({ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Inherited
+@SpringBootTest
+public @interface BladeBootTest {
+	/**
+	 * 服务名:appName
+	 * @return appName
+	 */
+	@AliasFor("appName")
+	String value() default "blade-test";
+	/**
+	 * 服务名:appName
+	 * @return appName
+	 */
+	@AliasFor("value")
+	String appName() default "blade-test";
+	/**
+	 * profile
+	 * @return profile
+	 */
+	String profile() default "dev";
+	/**
+	 * 启用 ServiceLoader 加载 launcherService
+	 * @return 是否启用
+	 */
+	boolean enableLoader() default false;
+}

+ 29 - 0
blade-core-test/src/main/java/org/springblade/core/test/BladeBootTestException.java

@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com).
+ * <p>
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * <p>
+ * http://www.gnu.org/licenses/lgpl.html
+ * <p>
+ * 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.
+ */
+
+package org.springblade.core.test;
+
+/**
+ * blade test 异常
+ *
+ * @author L.cm
+ */
+class BladeBootTestException extends RuntimeException {
+
+	BladeBootTestException(String message) {
+		super(message);
+	}
+}

+ 80 - 0
blade-core-test/src/main/java/org/springblade/core/test/BladeSpringRunner.java

@@ -0,0 +1,80 @@
+/**
+ * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com).
+ * <p>
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * <p>
+ * http://www.gnu.org/licenses/lgpl.html
+ * <p>
+ * 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.
+ */
+
+package org.springblade.core.test;
+
+
+import org.junit.runners.model.InitializationError;
+import org.springblade.core.launch.BladeApplication;
+import org.springblade.core.launch.constant.AppConstant;
+import org.springblade.core.launch.constant.NacosConstant;
+import org.springblade.core.launch.constant.SentinelConstant;
+import org.springblade.core.launch.service.LauncherService;
+import org.springframework.boot.builder.SpringApplicationBuilder;
+import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 设置启动参数
+ *
+ * @author L.cm
+ */
+public class BladeSpringRunner extends SpringJUnit4ClassRunner {
+
+	public BladeSpringRunner(Class<?> clazz) throws InitializationError {
+		super(clazz);
+		setUpTestClass(clazz);
+	}
+
+	private void setUpTestClass(Class<?> clazz) {
+		BladeBootTest bladeBootTest = AnnotationUtils.getAnnotation(clazz, BladeBootTest.class);
+		if (bladeBootTest == null) {
+			throw new BladeBootTestException(String.format("%s must be @BladeBootTest .", clazz));
+		}
+		String appName = bladeBootTest.appName();
+		String profile = bladeBootTest.profile();
+		boolean isLocalDev = BladeApplication.isLocalDev();
+		Properties props = System.getProperties();
+		props.setProperty("blade.env", profile);
+		props.setProperty("blade.name", appName);
+		props.setProperty("blade.is-local", String.valueOf(isLocalDev));
+		props.setProperty("blade.dev-mode", profile.equals(AppConstant.PROD_CODE) ? "false" : "true");
+		props.setProperty("blade.service.version", AppConstant.APPLICATION_VERSION);
+		props.setProperty("spring.application.name", appName);
+		props.setProperty("spring.profiles.active", profile);
+		props.setProperty("info.version", AppConstant.APPLICATION_VERSION);
+		props.setProperty("info.desc", appName);
+		props.setProperty("spring.cloud.nacos.discovery.server-addr", NacosConstant.NACOS_ADDR);
+		props.setProperty("spring.cloud.nacos.config.server-addr", NacosConstant.NACOS_ADDR);
+		props.setProperty("spring.cloud.nacos.config.prefix", NacosConstant.NACOS_CONFIG_PREFIX);
+		props.setProperty("spring.cloud.nacos.config.file-extension", NacosConstant.NACOS_CONFIG_FORMAT);
+		props.setProperty("spring.cloud.sentinel.transport.dashboard", SentinelConstant.SENTINEL_ADDR);
+		props.setProperty("spring.main.allow-bean-definition-overriding", "true");
+		// 加载自定义组件
+		if (bladeBootTest.enableLoader()) {
+			List<LauncherService> launcherList = new ArrayList<>();
+			SpringApplicationBuilder builder = new SpringApplicationBuilder(clazz);
+			ServiceLoader.load(LauncherService.class).forEach(launcherList::add);
+			launcherList.stream().sorted(Comparator.comparing(LauncherService::getOrder)).collect(Collectors.toList())
+				.forEach(launcherService -> launcherService.launcher(builder, appName, profile));
+		}
+		System.err.println(String.format("---[junit.test]:[%s]---启动中,读取到的环境变量:[%s]", appName, profile));
+	}
+
+}

+ 12 - 12
blade-core-tool/pom.xml

@@ -6,7 +6,7 @@
     <parent>
         <groupId>org.springblade</groupId>
         <artifactId>blade-tool</artifactId>
-        <version>2.3.1</version>
+        <version>2.3.2</version>
     </parent>
 
     <modelVersion>4.0.0</modelVersion>
@@ -56,17 +56,17 @@
             <artifactId>swagger-models</artifactId>
             <version>${swagger.models.version}</version>
         </dependency>
-		<!-- protostuff -->
-		<dependency>
-			<groupId>io.protostuff</groupId>
-			<artifactId>protostuff-core</artifactId>
-			<version>${protostuff.version}</version>
-		</dependency>
-		<dependency>
-			<groupId>io.protostuff</groupId>
-			<artifactId>protostuff-runtime</artifactId>
-			<version>${protostuff.version}</version>
-		</dependency>
+        <!-- protostuff -->
+        <dependency>
+            <groupId>io.protostuff</groupId>
+            <artifactId>protostuff-core</artifactId>
+            <version>${protostuff.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.protostuff</groupId>
+            <artifactId>protostuff-runtime</artifactId>
+            <version>${protostuff.version}</version>
+        </dependency>
     </dependencies>
 
 </project>

+ 36 - 0
blade-core-tool/src/main/java/org/springblade/core/tool/utils/CharPool.java

@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com).
+ * <p>
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * <p>
+ * http://www.gnu.org/licenses/lgpl.html
+ * <p>
+ * 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.
+ */
+package org.springblade.core.tool.utils;
+
+/**
+ * char 常量池
+ *
+ * @author L.cm
+ */
+public interface CharPool {
+
+	char UPPER_A		= 'A';
+	char LOWER_A		= 'a';
+	char UPPER_Z		= 'Z';
+	char LOWER_Z		= 'z';
+	char DOT			= '.';
+	char AT				= '@';
+	char LEFT_BRACE		= '{';
+	char RIGHT_BRACE	= '}';
+	char LEFT_BRACKET	= '(';
+	char RIGHT_BRACKET	= ')';
+
+}

+ 429 - 59
blade-core-tool/src/main/java/org/springblade/core/tool/utils/DateUtil.java

@@ -1,119 +1,264 @@
-/**
- * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com).
- * <p>
- * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- * <p>
- * http://www.gnu.org/licenses/lgpl.html
- * <p>
- * 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.
- */
 package org.springblade.core.tool.utils;
 
+import lombok.experimental.UtilityClass;
 import org.springframework.util.Assert;
 
 import java.text.ParseException;
+import java.time.*;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.Temporal;
+import java.time.temporal.TemporalAccessor;
+import java.time.temporal.TemporalAmount;
+import java.time.temporal.TemporalQuery;
 import java.util.Calendar;
 import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.TimeZone;
 
 /**
  * 日期工具类
  *
  * @author L.cm
  */
+@UtilityClass
 public class DateUtil {
 
+	public static final String PATTERN_DATETIME = "yyyy-MM-dd HH:mm:ss";
+	public static final String PATTERN_DATE = "yyyy-MM-dd";
+	public static final String PATTERN_TIME = "HH:mm:ss";
+	/**
+	 * 老 date 格式化
+	 */
+	public static final ConcurrentDateFormat DATETIME_FORMAT = ConcurrentDateFormat.of(PATTERN_DATETIME);
+	public static final ConcurrentDateFormat DATE_FORMAT = ConcurrentDateFormat.of(PATTERN_DATE);
+	public static final ConcurrentDateFormat TIME_FORMAT = ConcurrentDateFormat.of(PATTERN_TIME);
+	/**
+	 * java 8 时间格式化
+	 */
+	public static final DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern(DateUtil.PATTERN_DATETIME);
+	public static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern(DateUtil.PATTERN_DATE);
+	public static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern(DateUtil.PATTERN_TIME);
+
 	/**
-	 * 设置年
+	 * 添加
 	 *
-	 * @param date   时间
-	 * @param amount 年数,-1表示减少
+	 * @param date       时间
+	 * @param yearsToAdd 添加的年数
 	 * @return 设置后的时间
 	 */
-	public static Date setYears(Date date, int amount) {
-		return set(date, Calendar.YEAR, amount);
+	public static Date plusYears(Date date, int yearsToAdd) {
+		return DateUtil.set(date, Calendar.YEAR, yearsToAdd);
 	}
 
 	/**
-	 * 设置月
+	 * 添加
 	 *
-	 * @param date   时间
-	 * @param amount 月数,-1表示减少
+	 * @param date        时间
+	 * @param monthsToAdd 添加的月数
 	 * @return 设置后的时间
 	 */
-	public static Date setMonths(Date date, int amount) {
-		return set(date, Calendar.MONTH, amount);
+	public static Date plusMonths(Date date, int monthsToAdd) {
+		return DateUtil.set(date, Calendar.MONTH, monthsToAdd);
 	}
 
 	/**
-	 * 设置
+	 * 添加
 	 *
-	 * @param date   时间
-	 * @param amount 周数,-1表示减少
+	 * @param date       时间
+	 * @param weeksToAdd 添加的周数
 	 * @return 设置后的时间
 	 */
-	public static Date setWeeks(Date date, int amount) {
-		return set(date, Calendar.WEEK_OF_YEAR, amount);
+	public static Date plusWeeks(Date date, int weeksToAdd) {
+		return DateUtil.plus(date, Period.ofWeeks(weeksToAdd));
 	}
 
 	/**
-	 * 设置
+	 * 添加
 	 *
-	 * @param date   时间
-	 * @param amount 天数,-1表示减少
+	 * @param date      时间
+	 * @param daysToAdd 添加的天数
+	 * @return 设置后的时间
+	 */
+	public static Date plusDays(Date date, long daysToAdd) {
+		return DateUtil.plus(date, Duration.ofDays(daysToAdd));
+	}
+
+	/**
+	 * 添加小时
+	 *
+	 * @param date       时间
+	 * @param hoursToAdd 添加的小时数
+	 * @return 设置后的时间
+	 */
+	public static Date plusHours(Date date, long hoursToAdd) {
+		return DateUtil.plus(date, Duration.ofHours(hoursToAdd));
+	}
+
+	/**
+	 * 添加分钟
+	 *
+	 * @param date         时间
+	 * @param minutesToAdd 添加的分钟数
+	 * @return 设置后的时间
+	 */
+	public static Date plusMinutes(Date date, long minutesToAdd) {
+		return DateUtil.plus(date, Duration.ofMinutes(minutesToAdd));
+	}
+
+	/**
+	 * 添加秒
+	 *
+	 * @param date         时间
+	 * @param secondsToAdd 添加的秒数
+	 * @return 设置后的时间
+	 */
+	public static Date plusSeconds(Date date, long secondsToAdd) {
+		return DateUtil.plus(date, Duration.ofSeconds(secondsToAdd));
+	}
+
+	/**
+	 * 添加毫秒
+	 *
+	 * @param date        时间
+	 * @param millisToAdd 添加的毫秒数
 	 * @return 设置后的时间
 	 */
-	public static Date setDays(Date date, int amount) {
-		return set(date, Calendar.DATE, amount);
+	public static Date plusMillis(Date date, long millisToAdd) {
+		return DateUtil.plus(date, Duration.ofMillis(millisToAdd));
 	}
 
 	/**
-	 * 设置小时
+	 * 添加纳秒
+	 *
+	 * @param date       时间
+	 * @param nanosToAdd 添加的纳秒数
+	 * @return 设置后的时间
+	 */
+	public static Date plusNanos(Date date, long nanosToAdd) {
+		return DateUtil.plus(date, Duration.ofNanos(nanosToAdd));
+	}
+
+	/**
+	 * 日期添加时间量
 	 *
 	 * @param date   时间
-	 * @param amount 小时数,-1表示减少
+	 * @param amount 时间量
 	 * @return 设置后的时间
 	 */
-	public static Date setHours(Date date, int amount) {
-		return set(date, Calendar.HOUR_OF_DAY, amount);
+	public static Date plus(Date date, TemporalAmount amount) {
+		Instant instant = date.toInstant();
+		return Date.from(instant.plus(amount));
 	}
 
 	/**
-	 * 设置分钟
+	 * 减少年
+	 *
+	 * @param date  时间
+	 * @param years 减少的年数
+	 * @return 设置后的时间
+	 */
+	public static Date minusYears(Date date, int years) {
+		return DateUtil.set(date, Calendar.YEAR, -years);
+	}
+
+	/**
+	 * 减少月
 	 *
 	 * @param date   时间
-	 * @param amount 分钟数,-1表示减少
+	 * @param months 减少的月数
+	 * @return 设置后的时间
+	 */
+	public static Date minusMonths(Date date, int months) {
+		return DateUtil.set(date, Calendar.MONTH, -months);
+	}
+
+	/**
+	 * 减少周
+	 *
+	 * @param date  时间
+	 * @param weeks 减少的周数
+	 * @return 设置后的时间
+	 */
+	public static Date minusWeeks(Date date, int weeks) {
+		return DateUtil.minus(date, Period.ofWeeks(weeks));
+	}
+
+	/**
+	 * 减少天
+	 *
+	 * @param date 时间
+	 * @param days 减少的天数
+	 * @return 设置后的时间
+	 */
+	public static Date minusDays(Date date, long days) {
+		return DateUtil.minus(date, Duration.ofDays(days));
+	}
+
+	/**
+	 * 减少小时
+	 *
+	 * @param date  时间
+	 * @param hours 减少的小时数
+	 * @return 设置后的时间
+	 */
+	public static Date minusHours(Date date, long hours) {
+		return DateUtil.minus(date, Duration.ofHours(hours));
+	}
+
+	/**
+	 * 减少分钟
+	 *
+	 * @param date    时间
+	 * @param minutes 减少的分钟数
+	 * @return 设置后的时间
+	 */
+	public static Date minusMinutes(Date date, long minutes) {
+		return DateUtil.minus(date, Duration.ofMinutes(minutes));
+	}
+
+	/**
+	 * 减少秒
+	 *
+	 * @param date    时间
+	 * @param seconds 减少的秒数
 	 * @return 设置后的时间
 	 */
-	public static Date setMinutes(Date date, int amount) {
-		return set(date, Calendar.MINUTE, amount);
+	public static Date minusSeconds(Date date, long seconds) {
+		return DateUtil.minus(date, Duration.ofSeconds(seconds));
 	}
 
 	/**
-	 * 设置秒
+	 * 减少毫
 	 *
 	 * @param date   时间
-	 * @param amount 秒数,-1表示减少
+	 * @param millis 减少的毫秒数
+	 * @return 设置后的时间
+	 */
+	public static Date minusMillis(Date date, long millis) {
+		return DateUtil.minus(date, Duration.ofMillis(millis));
+	}
+
+	/**
+	 * 减少纳秒
+	 *
+	 * @param date  时间
+	 * @param nanos 减少的纳秒数
 	 * @return 设置后的时间
 	 */
-	public static Date setSeconds(Date date, int amount) {
-		return set(date, Calendar.SECOND, amount);
+	public static Date minusNanos(Date date, long nanos) {
+		return DateUtil.minus(date, Duration.ofNanos(nanos));
 	}
 
 	/**
-	 * 设置毫秒
+	 * 日期减少时间量
 	 *
 	 * @param date   时间
-	 * @param amount 毫秒数,-1表示减少
+	 * @param amount 时间量
 	 * @return 设置后的时间
 	 */
-	public static Date setMilliseconds(Date date, int amount) {
-		return set(date, Calendar.MILLISECOND, amount);
+	public static Date minus(Date date, TemporalAmount amount) {
+		Instant instant = date.toInstant();
+		return Date.from(instant.minus(amount));
 	}
 
 	/**
@@ -133,14 +278,6 @@ public class DateUtil {
 		return c.getTime();
 	}
 
-	public static final String PATTERN_DATETIME = "yyyy-MM-dd HH:mm:ss";
-	public static final String PATTERN_DATE = "yyyy-MM-dd";
-	public static final String PATTERN_TIME = "HH:mm:ss";
-
-	public static final ConcurrentDateFormat DATETIME_FORMAT = ConcurrentDateFormat.of(PATTERN_DATETIME);
-	public static final ConcurrentDateFormat DATE_FORMAT = ConcurrentDateFormat.of(PATTERN_DATE);
-	public static final ConcurrentDateFormat TIME_FORMAT = ConcurrentDateFormat.of(PATTERN_TIME);
-
 	/**
 	 * 日期时间格式化
 	 *
@@ -182,6 +319,47 @@ public class DateUtil {
 		return ConcurrentDateFormat.of(pattern).format(date);
 	}
 
+	/**
+	 * java8 日期时间格式化
+	 *
+	 * @param temporal 时间
+	 * @return 格式化后的时间
+	 */
+	public static String formatDateTime(TemporalAccessor temporal) {
+		return DATETIME_FORMATTER.format(temporal);
+	}
+
+	/**
+	 * java8 日期时间格式化
+	 *
+	 * @param temporal 时间
+	 * @return 格式化后的时间
+	 */
+	public static String formatDate(TemporalAccessor temporal) {
+		return DATE_FORMATTER.format(temporal);
+	}
+
+	/**
+	 * java8 时间格式化
+	 *
+	 * @param temporal 时间
+	 * @return 格式化后的时间
+	 */
+	public static String formatTime(TemporalAccessor temporal) {
+		return TIME_FORMATTER.format(temporal);
+	}
+
+	/**
+	 * java8 日期格式化
+	 *
+	 * @param temporal 时间
+	 * @param pattern  表达式
+	 * @return 格式化后的时间
+	 */
+	public static String format(TemporalAccessor temporal, String pattern) {
+		return DateTimeFormatter.ofPattern(pattern).format(temporal);
+	}
+
 	/**
 	 * 将字符串转换为时间
 	 *
@@ -213,4 +391,196 @@ public class DateUtil {
 		}
 	}
 
+	/**
+	 * 将字符串转换为时间
+	 *
+	 * @param dateStr 时间字符串
+	 * @param pattern 表达式
+	 * @return 时间
+	 */
+	public static <T> T parse(String dateStr, String pattern, TemporalQuery<T> query) {
+		return DateTimeFormatter.ofPattern(pattern).parse(dateStr, query);
+	}
+
+	/**
+	 * 时间转 Instant
+	 *
+	 * @param dateTime 时间
+	 * @return Instant
+	 */
+	public static Instant toInstant(LocalDateTime dateTime) {
+		return dateTime.atZone(ZoneId.systemDefault()).toInstant();
+	}
+
+	/**
+	 * Instant 转 时间
+	 *
+	 * @param instant Instant
+	 * @return Instant
+	 */
+	public static LocalDateTime toDateTime(Instant instant) {
+		return LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
+	}
+
+	/**
+	 * 转换成 date
+	 *
+	 * @param dateTime LocalDateTime
+	 * @return Date
+	 */
+	public static Date toDate(LocalDateTime dateTime) {
+		return Date.from(DateUtil.toInstant(dateTime));
+	}
+
+	/**
+	 * 转换成 date
+	 *
+	 * @param localDate LocalDate
+	 * @return Date
+	 */
+	public static Date toDate(final LocalDate localDate) {
+		return Date.from(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
+	}
+
+	/**
+	 * Converts local date time to Calendar.
+	 */
+	public static Calendar toCalendar(final LocalDateTime localDateTime) {
+		return GregorianCalendar.from(ZonedDateTime.of(localDateTime, ZoneId.systemDefault()));
+	}
+
+	/**
+	 * localDateTime 转换成毫秒数
+	 *
+	 * @param localDateTime LocalDateTime
+	 * @return long
+	 */
+	public static long toMilliseconds(final LocalDateTime localDateTime) {
+		return localDateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
+	}
+
+	/**
+	 * localDate 转换成毫秒数
+	 *
+	 * @param localDate LocalDate
+	 * @return long
+	 */
+	public static long toMilliseconds(LocalDate localDate) {
+		return toMilliseconds(localDate.atStartOfDay());
+	}
+
+	/**
+	 * 转换成java8 时间
+	 *
+	 * @param calendar 日历
+	 * @return LocalDateTime
+	 */
+	public static LocalDateTime fromCalendar(final Calendar calendar) {
+		TimeZone tz = calendar.getTimeZone();
+		ZoneId zid = tz == null ? ZoneId.systemDefault() : tz.toZoneId();
+		return LocalDateTime.ofInstant(calendar.toInstant(), zid);
+	}
+
+	/**
+	 * 转换成java8 时间
+	 *
+	 * @param instant Instant
+	 * @return LocalDateTime
+	 */
+	public static LocalDateTime fromInstant(final Instant instant) {
+		return LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
+	}
+
+	/**
+	 * 转换成java8 时间
+	 *
+	 * @param date Date
+	 * @return LocalDateTime
+	 */
+	public static LocalDateTime fromDate(final Date date) {
+		return LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
+	}
+
+	/**
+	 * 转换成java8 时间
+	 *
+	 * @param milliseconds 毫秒数
+	 * @return LocalDateTime
+	 */
+	public static LocalDateTime fromMilliseconds(final long milliseconds) {
+		return LocalDateTime.ofInstant(Instant.ofEpochMilli(milliseconds), ZoneId.systemDefault());
+	}
+
+	/**
+	 * 比较2个时间差,跨度比较小
+	 *
+	 * @param startInclusive 开始时间
+	 * @param endExclusive   结束时间
+	 * @return 时间间隔
+	 */
+	public static Duration between(Temporal startInclusive, Temporal endExclusive) {
+		return Duration.between(startInclusive, endExclusive);
+	}
+
+	/**
+	 * 比较2个时间差,跨度比较大,年月日为单位
+	 *
+	 * @param startDate 开始时间
+	 * @param endDate   结束时间
+	 * @return 时间间隔
+	 */
+	public static Period between(LocalDate startDate, LocalDate endDate) {
+		return Period.between(startDate, endDate);
+	}
+
+	/**
+	 * 比较2个 时间差
+	 *
+	 * @param startDate 开始时间
+	 * @param endDate   结束时间
+	 * @return 时间间隔
+	 */
+	public static Duration between(Date startDate, Date endDate) {
+		return Duration.between(startDate.toInstant(), endDate.toInstant());
+	}
+
+	/**
+	 * 将秒数转换为日时分秒
+	 *
+	 * @param second 秒数
+	 * @return 时间
+	 */
+	public static String secondToTime(Long second) {
+		// 判断是否为空
+		if (second == null || second == 0L) {
+			return StringPool.EMPTY;
+		}
+		//转换天数
+		long days = second / 86400;
+		//剩余秒数
+		second = second % 86400;
+		//转换小时
+		long hours = second / 3600;
+		//剩余秒数
+		second = second % 3600;
+		//转换分钟
+		long minutes = second / 60;
+		//剩余秒数
+		second = second % 60;
+		if (days > 0) {
+			return StringUtil.format("{}天{}小时{}分{}秒", days, hours, minutes, second);
+		} else {
+			return StringUtil.format("{}小时{}分{}秒", hours, minutes, second);
+		}
+	}
+
+	/**
+	 * 获取今天的日期
+	 *
+	 * @return 时间
+	 */
+	public static String today() {
+		return format(new Date(), "yyyyMMdd");
+	}
+
 }

+ 385 - 0
blade-core-tool/src/main/java/org/springblade/core/tool/utils/FileUtil.java

@@ -0,0 +1,385 @@
+/*
+ *      Copyright (c) 2018-2028, DreamLu All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are met:
+ *
+ *  Redistributions of source code must retain the above copyright notice,
+ *  this list of conditions and the following disclaimer.
+ *  Redistributions in binary form must reproduce the above copyright
+ *  notice, this list of conditions and the following disclaimer in the
+ *  documentation and/or other materials provided with the distribution.
+ *  Neither the name of the dreamlu.net developer nor the names of its
+ *  contributors may be used to endorse or promote products derived from
+ *  this software without specific prior written permission.
+ *  Author: DreamLu 卢春梦 (596392912@qq.com)
+ */
+
+package org.springblade.core.tool.utils;
+
+import lombok.experimental.UtilityClass;
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+import org.springframework.util.FileSystemUtils;
+import org.springframework.util.PatternMatchUtils;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.*;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 文件工具类
+ *
+ * @author L.cm
+ */
+@UtilityClass
+public class FileUtil extends org.springframework.util.FileCopyUtils {
+
+	/**
+	 * 默认为true
+	 *
+	 * @author L.cm
+	 */
+	public static class TrueFilter implements FileFilter, Serializable {
+		private static final long serialVersionUID = -6420452043795072619L;
+
+		public final static TrueFilter TRUE = new TrueFilter();
+
+		@Override
+		public boolean accept(File pathname) {
+			return true;
+		}
+	}
+
+	/**
+	 * 扫描目录下的文件
+	 *
+	 * @param path 路径
+	 * @return 文件集合
+	 */
+	public static List<File> list(String path) {
+		File file = new File(path);
+		return list(file, TrueFilter.TRUE);
+	}
+
+	/**
+	 * 扫描目录下的文件
+	 *
+	 * @param path   路径
+	 * @param fileNamePattern 文件名 * 号
+	 * @return 文件集合
+	 */
+	public static List<File> list(String path, final String fileNamePattern) {
+		File file = new File(path);
+		return list(file, pathname -> {
+			String fileName = pathname.getName();
+			return PatternMatchUtils.simpleMatch(fileNamePattern, fileName);
+		});
+	}
+
+	/**
+	 * 扫描目录下的文件
+	 *
+	 * @param path   路径
+	 * @param filter 文件过滤
+	 * @return 文件集合
+	 */
+	public static List<File> list(String path, FileFilter filter) {
+		File file = new File(path);
+		return list(file, filter);
+	}
+
+	/**
+	 * 扫描目录下的文件
+	 *
+	 * @param file 文件
+	 * @return 文件集合
+	 */
+	public static List<File> list(File file) {
+		List<File> fileList = new ArrayList<>();
+		return list(file, fileList, TrueFilter.TRUE);
+	}
+
+	/**
+	 * 扫描目录下的文件
+	 *
+	 * @param file   文件
+	 * @param fileNamePattern Spring AntPathMatcher 规则
+	 * @return 文件集合
+	 */
+	public static List<File> list(File file, final String fileNamePattern) {
+		List<File> fileList = new ArrayList<>();
+		return list(file, fileList, pathname -> {
+			String fileName = pathname.getName();
+			return PatternMatchUtils.simpleMatch(fileNamePattern, fileName);
+		});
+	}
+
+	/**
+	 * 扫描目录下的文件
+	 *
+	 * @param file   文件
+	 * @param filter 文件过滤
+	 * @return 文件集合
+	 */
+	public static List<File> list(File file, FileFilter filter) {
+		List<File> fileList = new ArrayList<>();
+		return list(file, fileList, filter);
+	}
+
+	/**
+	 * 扫描目录下的文件
+	 *
+	 * @param file   文件
+	 * @param filter 文件过滤
+	 * @return 文件集合
+	 */
+	private static List<File> list(File file, List<File> fileList, FileFilter filter) {
+		if (file.isDirectory()) {
+			File[] files = file.listFiles();
+			if (files != null) {
+				for (File f : files) {
+					list(f, fileList, filter);
+				}
+			}
+		} else {
+			// 过滤文件
+			boolean accept = filter.accept(file);
+			if (file.exists() && accept) {
+				fileList.add(file);
+			}
+		}
+		return fileList;
+	}
+
+	/**
+	 * 获取文件后缀名
+	 * @param fullName 文件全名
+	 * @return {String}
+	 */
+	public static String getFileExtension(String fullName) {
+		Assert.notNull(fullName, "file fullName is null.");
+		String fileName = new File(fullName).getName();
+		int dotIndex = fileName.lastIndexOf('.');
+		return (dotIndex == -1) ? "" : fileName.substring(dotIndex + 1);
+	}
+
+	/**
+	 * 获取文件名,去除后缀名
+	 * @param file 文件
+	 * @return {String}
+	 */
+	public static String getNameWithoutExtension(String file) {
+		Assert.notNull(file, "file is null.");
+		String fileName = new File(file).getName();
+		int dotIndex = fileName.lastIndexOf(CharPool.DOT);
+		return (dotIndex == -1) ? fileName : fileName.substring(0, dotIndex);
+	}
+
+	/**
+	 * Returns the path to the system temporary directory.
+	 *
+	 * @return the path to the system temporary directory.
+	 */
+	public static String getTempDirPath() {
+		return System.getProperty("java.io.tmpdir");
+	}
+
+	/**
+	 * Returns a {@link File} representing the system temporary directory.
+	 *
+	 * @return the system temporary directory.
+	 */
+	public static File getTempDir() {
+		return new File(getTempDirPath());
+	}
+
+	/**
+	 * Reads the contents of a file into a String.
+	 * The file is always closed.
+	 *
+	 * @param file the file to read, must not be {@code null}
+	 * @return the file contents, never {@code null}
+	 */
+	public static String readToString(final File file) {
+		return readToString(file, Charsets.UTF_8);
+	}
+
+	/**
+	 * Reads the contents of a file into a String.
+	 * The file is always closed.
+	 *
+	 * @param file     the file to read, must not be {@code null}
+	 * @param encoding the encoding to use, {@code null} means platform default
+	 * @return the file contents, never {@code null}
+	 */
+	public static String readToString(final File file, final Charset encoding) {
+		try (InputStream in = Files.newInputStream(file.toPath())) {
+			return IoUtil.toString(in, encoding);
+		} catch (IOException e) {
+			throw Exceptions.unchecked(e);
+		}
+	}
+
+	/**
+	 * Reads the contents of a file into a String.
+	 * The file is always closed.
+	 *
+	 * @param file     the file to read, must not be {@code null}
+	 * @return the file contents, never {@code null}
+	 */
+	public static byte[] readToByteArray(final File file) {
+		try (InputStream in = Files.newInputStream(file.toPath())) {
+			return IoUtil.toByteArray(in);
+		} catch (IOException e) {
+			throw Exceptions.unchecked(e);
+		}
+	}
+
+	/**
+	 * Writes a String to a file creating the file if it does not exist.
+	 *
+	 * @param file the file to write
+	 * @param data the content to write to the file
+	 */
+	public static void writeToFile(final File file, final String data) {
+		writeToFile(file, data, Charsets.UTF_8, false);
+	}
+
+	/**
+	 * Writes a String to a file creating the file if it does not exist.
+	 *
+	 * @param file   the file to write
+	 * @param data   the content to write to the file
+	 * @param append if {@code true}, then the String will be added to the
+	 *               end of the file rather than overwriting
+	 */
+	public static void writeToFile(final File file, final String data, final boolean append){
+		writeToFile(file, data, Charsets.UTF_8, append);
+	}
+
+	/**
+	 * Writes a String to a file creating the file if it does not exist.
+	 *
+	 * @param file     the file to write
+	 * @param data     the content to write to the file
+	 * @param encoding the encoding to use, {@code null} means platform default
+	 */
+	public static void writeToFile(final File file, final String data, final Charset encoding) {
+		writeToFile(file, data, encoding, false);
+	}
+
+	/**
+	 * Writes a String to a file creating the file if it does not exist.
+	 *
+	 * @param file     the file to write
+	 * @param data     the content to write to the file
+	 * @param encoding the encoding to use, {@code null} means platform default
+	 * @param append   if {@code true}, then the String will be added to the
+	 *                 end of the file rather than overwriting
+	 */
+	public static void writeToFile(final File file, final String data, final Charset encoding, final boolean append) {
+		try (OutputStream out = new FileOutputStream(file, append)) {
+			IoUtil.write(data, out, encoding);
+		} catch (IOException e) {
+			throw Exceptions.unchecked(e);
+		}
+	}
+
+	/**
+	 * 转成file
+	 * @param multipartFile MultipartFile
+	 * @param file File
+	 */
+	public static void toFile(MultipartFile multipartFile, final File file) {
+		try {
+			FileUtil.toFile(multipartFile.getInputStream(), file);
+		} catch (IOException e) {
+			throw Exceptions.unchecked(e);
+		}
+	}
+
+	/**
+	 * 转成file
+	 * @param in InputStream
+	 * @param file File
+	 */
+	public static void toFile(InputStream in, final File file) {
+		try (OutputStream out = new FileOutputStream(file)) {
+			FileUtil.copy(in, out);
+		} catch (IOException e) {
+			throw Exceptions.unchecked(e);
+		}
+	}
+
+	/**
+	 * Moves a file.
+	 * <p>
+	 * When the destination file is on another file system, do a "copy and delete".
+	 *
+	 * @param srcFile  the file to be moved
+	 * @param destFile the destination file
+	 * @throws NullPointerException if source or destination is {@code null}
+	 * @throws IOException          if source or destination is invalid
+	 * @throws IOException          if an IO error occurs moving the file
+	 */
+	public static void moveFile(final File srcFile, final File destFile) throws IOException {
+		Assert.notNull(srcFile, "Source must not be null");
+		Assert.notNull(destFile, "Destination must not be null");
+		if (!srcFile.exists()) {
+			throw new FileNotFoundException("Source '" + srcFile + "' does not exist");
+		}
+		if (srcFile.isDirectory()) {
+			throw new IOException("Source '" + srcFile + "' is a directory");
+		}
+		if (destFile.exists()) {
+			throw new IOException("Destination '" + destFile + "' already exists");
+		}
+		if (destFile.isDirectory()) {
+			throw new IOException("Destination '" + destFile + "' is a directory");
+		}
+		final boolean rename = srcFile.renameTo(destFile);
+		if (!rename) {
+			FileUtil.copy(srcFile, destFile);
+			if (!srcFile.delete()) {
+				FileUtil.deleteQuietly(destFile);
+				throw new IOException("Failed to delete original file '" + srcFile + "' after copy to '" + destFile + "'");
+			}
+		}
+	}
+
+	/**
+	 * Deletes a file, never throwing an exception. If file is a directory, delete it and all sub-directories.
+	 * <p>
+	 * The difference between File.delete() and this method are:
+	 * <ul>
+	 * <li>A directory to be deleted does not have to be empty.</li>
+	 * <li>No exceptions are thrown when a file or directory cannot be deleted.</li>
+	 * </ul>
+	 *
+	 * @param file file or directory to delete, can be {@code null}
+	 * @return {@code true} if the file or directory was deleted, otherwise
+	 * {@code false}
+	 */
+	public static boolean deleteQuietly(@Nullable final File file) {
+		if (file == null) {
+			return false;
+		}
+		try {
+			if (file.isDirectory()) {
+				FileSystemUtils.deleteRecursively(file);
+			}
+		} catch (final Exception ignored) {
+		}
+
+		try {
+			return file.delete();
+		} catch (final Exception ignored) {
+			return false;
+		}
+	}
+
+}

+ 1 - 1
blade-core-tool/src/main/java/org/springblade/core/tool/utils/StringPool.java

@@ -27,7 +27,7 @@ public interface StringPool {
 	String AT				= "@";
 	String ASTERISK			= "*";
 	String STAR				= ASTERISK;
-	char SLASH				= '/';
+	String SLASH				= "/";
 	char BACK_SLASH			= '\\';
 	String DOUBLE_SLASH		= "#//";
 	String COLON			= ":";

+ 6 - 4
pom.xml

@@ -5,7 +5,7 @@
 
     <groupId>org.springblade</groupId>
     <artifactId>blade-tool</artifactId>
-    <version>2.3.1</version>
+    <version>2.3.2</version>
     <packaging>pom</packaging>
     <name>blade-tool</name>
     <description>
@@ -36,19 +36,19 @@
     </scm>
 
     <properties>
-        <blade.tool.version>2.3.1</blade.tool.version>
+        <blade.tool.version>2.3.2</blade.tool.version>
 
         <java.version>1.8</java.version>
         <maven.plugin.version>3.8.0</maven.plugin.version>
         <swagger.version>2.9.2</swagger.version>
         <swagger.models.version>1.5.21</swagger.models.version>
-        <swagger.bootstrapui.version>1.9.3</swagger.bootstrapui.version>
+        <swagger.bootstrapui.version>1.9.4</swagger.bootstrapui.version>
         <mybatis.plus.version>3.1.0</mybatis.plus.version>
         <curator.framework.version>4.0.1</curator.framework.version>
         <protostuff.version>1.6.0</protostuff.version>
         <disruptor.version>3.4.2</disruptor.version>
         <spring.boot.admin.version>2.1.4</spring.boot.admin.version>
-        <mica.auto.version>1.0.1</mica.auto.version>
+        <mica.auto.version>1.1.0</mica.auto.version>
         <alibaba.cloud.version>0.9.0.RELEASE</alibaba.cloud.version>
 
         <spring.boot.version>2.1.5.RELEASE</spring.boot.version>
@@ -68,7 +68,9 @@
         <module>blade-core-mybatis</module>
         <module>blade-core-secure</module>
         <module>blade-core-swagger</module>
+        <module>blade-core-test</module>
         <module>blade-core-tool</module>
+        <module>blade-core-oss</module>
     </modules>
 
     <dependencyManagement>