|
@@ -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;
|
|
|
}
|