Ver Fonte

优化数据导出模块逻辑

woody há 11 meses atrás
pai
commit
ca1883f043

+ 15 - 3
framework-export/src/main/java/com/chelvc/framework/export/ExportHandler.java

@@ -17,12 +17,18 @@ import lombok.NonNull;
  */
 public interface ExportHandler {
     /**
-     * 判断导出任务是否完成
+     * 初始化处理器
+     */
+    default void initialize() {
+    }
+
+    /**
+     * 获取导出任务状态
      *
      * @param id 导出任务ID
-     * @return true/false
+     * @return 导出任务状态
      */
-    boolean isCompleted(long id);
+    com.chelvc.framework.export.Exporting.Status status(long id);
 
     /**
      * 下载数据导出文件
@@ -67,4 +73,10 @@ public interface ExportHandler {
     <T> long export(String filename, BiConsumer<Long, Integer> listener,
                     BiFunction<Integer, Integer, Collection<T>> provider, ExcelUtils.Writer<T> writer,
                     String... titles);
+
+    /**
+     * 销毁处理器
+     */
+    default void destroy() {
+    }
 }

+ 62 - 0
framework-export/src/main/java/com/chelvc/framework/export/Exporting.java

@@ -0,0 +1,62 @@
+package com.chelvc.framework.export;
+
+import java.io.Serializable;
+
+import com.chelvc.framework.common.model.Enumerable;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.experimental.SuperBuilder;
+
+/**
+ * 数据导出对象
+ *
+ * @author Woody
+ * @date 2024/5/11
+ */
+@Data
+@SuperBuilder
+@NoArgsConstructor
+@AllArgsConstructor
+public class Exporting implements Serializable {
+    /**
+     * 文件名称
+     */
+    private String filename;
+
+    /**
+     * 导出状态
+     */
+    private Status status;
+
+    /**
+     * 导出状态枚举
+     */
+    @Getter
+    public enum Status implements Enumerable {
+        /**
+         * 运行中
+         */
+        RUNNING("运行中"),
+
+        /**
+         * 已失败
+         */
+        FAILED("已失败"),
+
+        /**
+         * 已完成
+         */
+        COMPLETED("已完成");
+
+        /**
+         * 状态描述
+         */
+        private final String description;
+
+        Status(String description) {
+            this.description = description;
+        }
+    }
+}

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

@@ -0,0 +1,29 @@
+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.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.core.RedisTemplate;
+
+/**
+ * 数据导出配置
+ *
+ * @author Woody
+ * @date 2024/5/10
+ */
+@Configuration
+@RequiredArgsConstructor(onConstructor = @__(@Autowired))
+public class ExportConfigurer {
+    private final com.chelvc.framework.export.config.ExportProperties properties;
+    private final RedisTemplate<String, Object> redisTemplate;
+
+    @ConditionalOnMissingBean(ExportHandler.class)
+    @Bean(initMethod = "initialize", destroyMethod = "destroy")
+    public ExportHandler exportHandler() {
+        return new DefaultExportHandler(this.redisTemplate, this.properties);
+    }
+}

+ 61 - 51
framework-export/src/main/java/com/chelvc/framework/export/DefaultExportHandler.java → framework-export/src/main/java/com/chelvc/framework/export/support/DefaultExportHandler.java

@@ -1,4 +1,4 @@
-package com.chelvc.framework.export;
+package com.chelvc.framework.export.support;
 
 import java.io.File;
 import java.io.FileOutputStream;
@@ -22,20 +22,17 @@ 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.StringUtils;
 import com.chelvc.framework.common.util.ThreadUtils;
+import com.chelvc.framework.export.ExportHandler;
+import com.chelvc.framework.export.Exporting;
 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.RequiredArgsConstructor;
 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;
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.data.redis.core.RedisTemplate;
-import org.springframework.stereotype.Component;
 
 /**
  * 数据导出默认实现
@@ -44,19 +41,25 @@ import org.springframework.stereotype.Component;
  * @date 2024/5/9
  */
 @Slf4j
-@Component
-@RequiredArgsConstructor(onConstructor = @__(@Autowired))
 public class DefaultExportHandler implements ExportHandler {
-    private final ExportProperties properties;
+    private final String directory;
+    private final Duration duration;
     private final RedisTemplate<String, Object> redisTemplate;
 
+    public DefaultExportHandler(@NonNull RedisTemplate<String, Object> redisTemplate,
+                                @NonNull ExportProperties properties) {
+        this.directory = properties.getDirectory();
+        this.duration = Duration.ofSeconds(properties.getExpiration());
+        this.redisTemplate = redisTemplate;
+    }
+
     /**
      * 清理导出文件
      */
     private void clear() {
         int batch = 100;
         List<File> files = Lists.newArrayListWithCapacity(batch);
-        FileUtils.files(this.properties.getDirectory(), file -> {
+        FileUtils.files(this.directory, file -> {
             files.add(file);
             if (files.size() == batch) {
                 this.clear(files);
@@ -96,34 +99,31 @@ public class DefaultExportHandler implements ExportHandler {
     }
 
     /**
-     * 获取任务状态
+     * 获取导出任务信息
      *
-     * @param id 任务ID
-     * @return 任务状态信息
+     * @param id 导出任务ID
+     * @return 导出任务信息
      */
-    private Pair<String, Boolean> pair(Serializable id) {
-        String value = (String) this.redisTemplate.opsForValue().get(this.key(id));
-        if (StringUtils.isEmpty(value)) {
-            return null;
-        }
-        String[] splits = value.split(":");
-        return Pair.of(splits[0], Boolean.valueOf(splits[1]));
+    private Exporting exporting(Serializable id) {
+        return (Exporting) this.redisTemplate.opsForValue().get(this.key(id));
     }
 
     /**
-     * 更新任务状态
+     * 设置导出任务信息
      *
-     * @param id       任务ID
-     * @param pair     任务状态信息
-     * @param duration 有效时间
+     * @param id       导出任务ID
+     * @param filename 文件名称
+     * @param status   导出状态
+     * @param duration 任务有效期
      */
-    private void pair(Serializable id, Pair<String, Boolean> pair, Duration duration) {
-        String value = pair.getLeft() + ":" + pair.getRight();
-        this.redisTemplate.opsForValue().set(this.key(id), value, duration);
+    private void exporting(Serializable id, String filename, Exporting.Status status, Duration duration) {
+        Exporting exporting = Exporting.builder().filename(filename).status(status).build();
+        this.redisTemplate.opsForValue().set(this.key(id), exporting, duration);
     }
 
     @PostConstruct
     public void initialize() {
+        // 初始化导出文件清理线程
         ThreadUtils.run(() -> {
             while (!Thread.currentThread().isInterrupted()) {
                 try {
@@ -137,19 +137,18 @@ public class DefaultExportHandler implements ExportHandler {
     }
 
     @Override
-    public boolean isCompleted(long id) {
-        Boolean completed = ObjectUtils.ifNull(this.pair(id), Pair::getRight);
-        return Boolean.TRUE.equals(completed);
+    public Exporting.Status status(long id) {
+        return ObjectUtils.ifNull(this.exporting(id), Exporting::getStatus);
     }
 
     @Override
     public void download(long id, @NonNull HttpServletResponse response) throws IOException {
-        Pair<String, Boolean> pair = this.pair(id);
-        AssertUtils.available(pair, () -> ApplicationContextHolder.getMessage("Export.Expired"));
-        boolean completed = Boolean.TRUE.equals(ObjectUtils.ifNull(pair, Pair::getRight));
+        Exporting exporting = this.exporting(id);
+        AssertUtils.available(exporting, () -> ApplicationContextHolder.getMessage("Export.Expired"));
+        boolean completed = ObjectUtils.ifNull(exporting, Exporting::getStatus) == Exporting.Status.COMPLETED;
         AssertUtils.available(completed, () -> ApplicationContextHolder.getMessage("Export.Uncompleted"));
-        String filename = ObjectUtils.ifNull(pair, Pair::getLeft);
-        HttpUtils.write(response, new File(this.properties.getDirectory(), String.valueOf(id)), filename);
+        String filename = ObjectUtils.ifNull(exporting, Exporting::getFilename);
+        HttpUtils.write(response, new File(this.directory, String.valueOf(id)), filename);
     }
 
     @Override
@@ -160,28 +159,39 @@ public class DefaultExportHandler implements ExportHandler {
         long id = RedisContextHolder.identity();
 
         // 初始化数据导出状态
-        this.pair(id, Pair.of(filename, false), Duration.ofDays(1));
+        this.exporting(id, filename, Exporting.Status.RUNNING, 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);
-            }
+            int volume;
+            try {
+                // 将数据写入工作薄
+                Workbook workbook = new SXSSFWorkbook();
+                volume = ExcelUtils.write(workbook, provider, writer, titles);
+
+                // 保存导出文件
+                File file = new File(this.directory, String.valueOf(id));
+                try (OutputStream output = new FileOutputStream(file)) {
+                    workbook.write(output);
+                    workbook.close();
+                }
+
+                // 更新导出任务完成状态
+                this.exporting(id, filename, Exporting.Status.COMPLETED, this.duration);
+            } catch (Exception e) {
+                log.error("Export execute failed", e);
 
-            // 更新数据导出任务状态
-            this.pair(id, Pair.of(filename, true), Duration.ofSeconds(this.properties.getExpiration()));
+                // 更新导出任务失败状态
+                this.exporting(id, filename, Exporting.Status.FAILED, this.duration);
+                return;
+            }
 
             // 数据导出完成监听回调
-            listener.accept(id, volume);
+            try {
+                listener.accept(id, volume);
+            } catch (Exception e) {
+                log.error("Export listener callback failed", e);
+            }
         });
         return id;
     }