Просмотр исходного кода

新增数据导出模块;优化文件上传逻辑;

woody 1 год назад
Родитель
Сommit
56b807d0eb

+ 10 - 37
framework-common/src/main/java/com/chelvc/framework/common/util/FileUtils.java

@@ -34,14 +34,14 @@ public final class FileUtils {
     public static final String TEMPORARY = System.getProperty("java.io.tmpdir");
 
     /**
-     * 文件上传目录
+     * 文件导出目录
      */
-    public static final String UPLOAD = new File(TEMPORARY, "upload").getPath();
+    public static final String EXPORT = new File(TEMPORARY, "export").getPath();
 
     /**
-     * 文件导出目录
+     * 文件上传目录
      */
-    public static final String EXPORT = new File(TEMPORARY, "export").getPath();
+    public static final String UPLOAD = new File(TEMPORARY, "upload").getPath();
 
     /**
      * 文件下载目录
@@ -49,18 +49,18 @@ public final class FileUtils {
     public static final String DOWNLOAD = new File(TEMPORARY, "download").getPath();
 
     static {
-        // 初始化文件上传目录
-        File upload = new File(UPLOAD);
-        if (!upload.exists() && !upload.mkdirs()) {
-            log.warn("Make upload dir failed: {}", UPLOAD);
-        }
-
         // 初始化文件导出目录
         File export = new File(EXPORT);
         if (!export.exists() && !export.mkdirs()) {
             log.warn("Make export dir failed: {}", EXPORT);
         }
 
+        // 初始化文件上传目录
+        File upload = new File(UPLOAD);
+        if (!upload.exists() && !upload.mkdirs()) {
+            log.warn("Make upload dir failed: {}", UPLOAD);
+        }
+
         // 初始化文件下载目录
         File download = new File(DOWNLOAD);
         if (!download.exists() && !download.mkdirs()) {
@@ -71,33 +71,6 @@ public final class FileUtils {
     private FileUtils() {
     }
 
-    /**
-     * 获取文件上传目录
-     *
-     * @return 文件目录
-     */
-    public static File getUploadDirectory() {
-        return new File(UPLOAD);
-    }
-
-    /**
-     * 获取数据导出目录
-     *
-     * @return 文件目录
-     */
-    public static File getExportDirectory() {
-        return new File(EXPORT);
-    }
-
-    /**
-     * 获取文件下载目录
-     *
-     * @return 文件目录
-     */
-    public static File getDownloadDirectory() {
-        return new File(DOWNLOAD);
-    }
-
     /**
      * 获取文件后缀名
      *

+ 34 - 0
framework-export/pom.xml

@@ -0,0 +1,34 @@
+<?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.chelvc.framework</groupId>
+        <artifactId>framework-dependencies</artifactId>
+        <version>1.0.0-RELEASE</version>
+        <relativePath/>
+    </parent>
+
+    <artifactId>framework-export</artifactId>
+    <version>1.0.0-RELEASE</version>
+
+    <properties>
+        <framework-base.version>1.0.0-RELEASE</framework-base.version>
+        <framework-redis.version>1.0.0-RELEASE</framework-redis.version>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.chelvc.framework</groupId>
+            <artifactId>framework-base</artifactId>
+            <version>${framework-base.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.chelvc.framework</groupId>
+            <artifactId>framework-redis</artifactId>
+            <version>${framework-redis.version}</version>
+        </dependency>
+    </dependencies>
+</project>

+ 82 - 0
framework-export/src/main/java/com/chelvc/framework/export/ExportHandler.java

@@ -0,0 +1,82 @@
+package com.chelvc.framework.export;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
+import javax.servlet.http.HttpServletResponse;
+
+import com.chelvc.framework.common.util.ExcelUtils;
+import lombok.NonNull;
+
+/**
+ * 数据导出操作接口
+ *
+ * @author Woody
+ * @date 2024/5/9
+ */
+public interface ExportHandler {
+    /**
+     * 判断导出任务是否完成
+     *
+     * @param id 导出任务ID
+     * @return true/false
+     */
+    boolean isCompleted(long id);
+
+    /**
+     * 下载数据导出文件
+     *
+     * @param id       导出任务ID
+     * @param response Http响应对象
+     * @throws IOException I/O操作异常
+     */
+    void download(long id, HttpServletResponse response) throws IOException;
+
+    /**
+     * 异步导出数据
+     *
+     * @param filename 文件名称
+     * @param provider 数据生产者
+     * @param writer   Excel对象实例写入接口
+     * @param titles   表头数组
+     * @param <T>      数据类型
+     * @return 导出任务ID
+     */
+    default <T> long export(@NonNull String filename, @NonNull BiFunction<Integer, Integer, Collection<T>> provider,
+                            @NonNull ExcelUtils.Writer<T> writer, @NonNull String... titles) {
+        return this.export(
+                filename,
+                (id, volume) -> {
+                },
+                provider, writer, titles
+        );
+    }
+
+    /**
+     * 异步导出数据
+     *
+     * @param filename 文件名称
+     * @param listener 导出完成监听器
+     * @param provider 数据生产者
+     * @param writer   Excel对象实例写入接口
+     * @param titles   表头数组
+     * @param <T>      数据类型
+     * @return 导出任务ID
+     */
+    <T> long export(String filename, BiConsumer<Long, Integer> listener,
+                    BiFunction<Integer, Integer, Collection<T>> provider, ExcelUtils.Writer<T> writer,
+                    String... titles);
+
+    /**
+     * 初始化处理器
+     */
+    default void initialize() {
+    }
+
+    /**
+     * 销毁处理器
+     */
+    default void destroy() {
+    }
+}

+ 25 - 0
framework-export/src/main/java/com/chelvc/framework/export/config/ExportConfigurer.java

@@ -0,0 +1,25 @@
+package com.chelvc.framework.export.config;
+
+import com.chelvc.framework.export.ExportHandler;
+import com.chelvc.framework.export.support.DefaultExportHandler;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 数据导出配置
+ *
+ * @author Woody
+ * @date 2024/5/10
+ */
+@Configuration
+@RequiredArgsConstructor(onConstructor = @__(@Autowired))
+public class ExportConfigurer {
+    private final ExportProperties properties;
+
+    @Bean(initMethod = "initialize", destroyMethod = "destroy")
+    public ExportHandler exportHandler() {
+        return new DefaultExportHandler(this.properties);
+    }
+}

+ 27 - 0
framework-export/src/main/java/com/chelvc/framework/export/config/ExportProperties.java

@@ -0,0 +1,27 @@
+package com.chelvc.framework.export.config;
+
+import com.chelvc.framework.common.util.FileUtils;
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 数据导出配置
+ *
+ * @author Woody
+ * @date 2024/5/9
+ */
+@Data
+@Configuration
+@ConfigurationProperties("export")
+public class ExportProperties {
+    /**
+     * 文件存储目录
+     */
+    private String directory = FileUtils.EXPORT;
+
+    /**
+     * 文件过期时间(秒)
+     */
+    private int expiration = 10 * 60;
+}

+ 159 - 0
framework-export/src/main/java/com/chelvc/framework/export/support/DefaultExportHandler.java

@@ -0,0 +1,159 @@
+package com.chelvc.framework.export.support;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.Serializable;
+import java.time.Duration;
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
+import java.util.stream.Collectors;
+import javax.servlet.http.HttpServletResponse;
+
+import com.chelvc.framework.base.context.ApplicationContextHolder;
+import com.chelvc.framework.base.context.ThreadContextHolder;
+import com.chelvc.framework.base.util.HttpUtils;
+import com.chelvc.framework.common.util.AssertUtils;
+import com.chelvc.framework.common.util.ExcelUtils;
+import com.chelvc.framework.common.util.FileUtils;
+import com.chelvc.framework.common.util.ObjectUtils;
+import com.chelvc.framework.common.util.ThreadUtils;
+import com.chelvc.framework.export.ExportHandler;
+import com.chelvc.framework.export.config.ExportProperties;
+import com.chelvc.framework.redis.context.RedisContextHolder;
+import com.google.common.collect.Lists;
+import lombok.NonNull;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.poi.ss.usermodel.Workbook;
+import org.apache.poi.xssf.streaming.SXSSFWorkbook;
+
+/**
+ * 数据导出默认实现
+ *
+ * @author Woody
+ * @date 2024/5/9
+ */
+@Slf4j
+public class DefaultExportHandler implements ExportHandler {
+    private final ExportProperties properties;
+
+    public DefaultExportHandler(@NonNull ExportProperties properties) {
+        this.properties = properties;
+    }
+
+    /**
+     * 清理导出文件
+     */
+    private void clear() {
+        int batch = 100;
+        List<File> files = Lists.newArrayListWithCapacity(batch);
+        FileUtils.files(this.properties.getDirectory(), file -> {
+            files.add(file);
+            if (files.size() == batch) {
+                this.clear(files);
+                files.clear();
+            }
+        });
+        if (ObjectUtils.notEmpty(files)) {
+            this.clear(files);
+        }
+    }
+
+    /**
+     * 清理导出文件
+     *
+     * @param files 文件列表
+     */
+    private void clear(List<File> files) {
+        List<String> keys = files.stream().map(File::getName).map(this::key).collect(Collectors.toList());
+        List<?> values = RedisContextHolder.getRedisTemplate().opsForValue().multiGet(keys);
+        if (ObjectUtils.notEmpty(values)) {
+            for (int i = 0; i < values.size(); i++) {
+                if (Objects.isNull(values.get(i))) {
+                    FileUtils.delete(files.get(i));
+                }
+            }
+        }
+    }
+
+    /**
+     * 构建任务标识
+     *
+     * @param id 任务ID
+     * @return 任务标识
+     */
+    private String key(Serializable id) {
+        return "exporting:" + id;
+    }
+
+    @Override
+    public void initialize() {
+        ThreadUtils.run(() -> {
+            while (!Thread.currentThread().isInterrupted()) {
+                try {
+                    this.clear();
+                } catch (Exception e) {
+                    log.error("Export file clear failed", e);
+                }
+                ThreadUtils.sleep(60 * 1000);
+            }
+        });
+    }
+
+    @Override
+    public boolean isCompleted(long id) {
+        Pair<?, ?> pair = (Pair<?, ?>) RedisContextHolder.getRedisTemplate().opsForValue().get(this.key(id));
+        return Boolean.TRUE.equals(ObjectUtils.ifNull(pair, Pair::getRight));
+    }
+
+    @Override
+    public void download(long id, @NonNull HttpServletResponse response) throws IOException {
+        Pair<?, ?> pair = (Pair<?, ?>) RedisContextHolder.getRedisTemplate().opsForValue().get(this.key(id));
+        AssertUtils.available(pair, () -> ApplicationContextHolder.getMessage("Export.Expired"));
+        boolean completed = Boolean.TRUE.equals(ObjectUtils.ifNull(pair, Pair::getRight));
+        AssertUtils.available(completed, () -> ApplicationContextHolder.getMessage("Export.Uncompleted"));
+        String filename = (String) ObjectUtils.ifNull(pair, Pair::getLeft);
+        HttpUtils.write(response, new File(this.properties.getDirectory(), String.valueOf(id)), filename);
+    }
+
+    @Override
+    public <T> long export(@NonNull String filename, @NonNull BiConsumer<Long, Integer> listener,
+                           @NonNull BiFunction<Integer, Integer, Collection<T>> provider,
+                           @NonNull ExcelUtils.Writer<T> writer, @NonNull String... titles) {
+        // 生成导出任务ID
+        long id = RedisContextHolder.identity();
+
+        // 初始化数据导出状态
+        String key = this.key(id);
+        RedisContextHolder.getRedisTemplate().opsForValue().set(key, Pair.of(filename, false), Duration.ofDays(1));
+
+        // 异步执行导出任务
+        ThreadContextHolder.execute(() -> {
+            // 将数据写入工作薄
+            Workbook workbook = new SXSSFWorkbook();
+            int volume = ExcelUtils.write(workbook, provider, writer, titles);
+
+            // 保存导出文件
+            File file = new File(this.properties.getDirectory(), String.valueOf(id));
+            try (OutputStream output = new FileOutputStream(file)) {
+                workbook.write(output);
+                workbook.close();
+            } catch (IOException e) {
+                throw new RuntimeException(e);
+            }
+
+            // 更新数据导出任务状态
+            Duration duration = Duration.ofSeconds(this.properties.getExpiration());
+            RedisContextHolder.getRedisTemplate().opsForValue().set(key, Pair.of(filename, true), duration);
+
+            // 数据导出完成监听回调
+            listener.accept(id, volume);
+        });
+        return id;
+    }
+}

+ 6 - 0
framework-upload/src/main/java/com/chelvc/framework/upload/UploadHandler.java

@@ -41,6 +41,12 @@ public interface UploadHandler {
      */
     String upload(InputStream stream, String suffix, long length) throws IOException;
 
+    /**
+     * 初始化处理器
+     */
+    default void initialize() {
+    }
+
     /**
      * 销毁处理器
      */

+ 4 - 4
framework-upload/src/main/java/com/chelvc/framework/upload/config/UploadConfigurer.java

@@ -5,8 +5,8 @@ import java.util.stream.Collectors;
 
 import com.chelvc.framework.common.util.ObjectUtils;
 import com.chelvc.framework.upload.UploadHandler;
+import com.chelvc.framework.upload.support.DefaultUploadHandler;
 import com.chelvc.framework.upload.support.DelegatingUploadHandler;
-import com.chelvc.framework.upload.support.SimpleUploadHandler;
 import com.chelvc.framework.upload.support.TencentUploadHandler;
 import lombok.RequiredArgsConstructor;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -32,15 +32,15 @@ public class UploadConfigurer {
      */
     private UploadHandler initializeUploadHandler(UploadProperties.Client client) {
         UploadProperties.Channel channel = client.getChannel();
-        if (channel == UploadProperties.Channel.LOCAL) {
-            return new SimpleUploadHandler(client);
+        if (channel == UploadProperties.Channel.DEFAULT) {
+            return new DefaultUploadHandler(client);
         } else if (channel == UploadProperties.Channel.TENCENT) {
             return new TencentUploadHandler(client, this.properties.getHttpclient());
         }
         throw new UnsupportedOperationException(String.valueOf(channel));
     }
 
-    @Bean(destroyMethod = "destroy")
+    @Bean(initMethod = "initialize", destroyMethod = "destroy")
     public UploadHandler uploadHandler() {
         List<UploadHandler> handlers = this.properties.getClients().stream()
                 .map(this::initializeUploadHandler).collect(Collectors.toList());

+ 2 - 2
framework-upload/src/main/java/com/chelvc/framework/upload/config/UploadProperties.java

@@ -33,9 +33,9 @@ public class UploadProperties {
      */
     public enum Channel {
         /**
-         * 本地
+         * 默认
          */
-        LOCAL,
+        DEFAULT,
 
         /**
          * 腾讯云

+ 3 - 3
framework-upload/src/main/java/com/chelvc/framework/upload/support/SimpleUploadHandler.java → framework-upload/src/main/java/com/chelvc/framework/upload/support/DefaultUploadHandler.java

@@ -13,16 +13,16 @@ import com.chelvc.framework.upload.config.UploadProperties;
 import lombok.NonNull;
 
 /**
- * 文件上传简单实现
+ * 文件上传默认实现
  *
  * @author Woody
  * @date 2024/1/30
  */
-public class SimpleUploadHandler implements UploadHandler {
+public class DefaultUploadHandler implements UploadHandler {
     private final String path;
     private final String domain;
 
-    public SimpleUploadHandler(@NonNull UploadProperties.Client properties) {
+    public DefaultUploadHandler(@NonNull UploadProperties.Client properties) {
         this.path = Objects.requireNonNull(properties.getPath());
         this.domain = Objects.requireNonNull(properties.getDomain());
     }

+ 1 - 0
pom.xml

@@ -34,5 +34,6 @@
         <module>framework-cloud-nacos-feign</module>
         <module>framework-cloud-feign-client</module>
         <module>framework-cloud-kubernetes</module>
+        <module>framework-export</module>
     </modules>
 </project>