|
|
@@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
|
import com.ruoyi.file.domain.FilesUpload;
|
|
|
import com.ruoyi.file.mapper.FilesMapper;
|
|
|
import com.usky.common.core.bean.CommonPage;
|
|
|
+import com.usky.common.core.exception.BusinessException;
|
|
|
import com.usky.common.security.utils.SecurityUtils;
|
|
|
import com.usky.vpp.domain.VppFileArchive;
|
|
|
import com.usky.vpp.mapper.VppFileArchiveMapper;
|
|
|
@@ -13,17 +14,19 @@ import com.usky.vpp.service.VppArchiveService;
|
|
|
import com.usky.vpp.service.vo.VppFileArchiveRequestVO;
|
|
|
import com.usky.vpp.service.vo.VppFileArchiveResponseVO;
|
|
|
import org.apache.commons.lang3.StringUtils;
|
|
|
+import org.apache.http.impl.client.CloseableHttpClient;
|
|
|
+import org.apache.http.impl.client.HttpClientBuilder;
|
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
|
import org.springframework.http.HttpHeaders;
|
|
|
import org.springframework.http.HttpStatus;
|
|
|
import org.springframework.http.MediaType;
|
|
|
-import org.springframework.http.ResponseEntity;
|
|
|
+import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
|
|
import org.springframework.stereotype.Service;
|
|
|
import org.springframework.web.client.RestTemplate;
|
|
|
|
|
|
import javax.servlet.ServletOutputStream;
|
|
|
import javax.servlet.http.HttpServletResponse;
|
|
|
-import java.io.ByteArrayOutputStream;
|
|
|
+import java.io.BufferedOutputStream;
|
|
|
import java.io.IOException;
|
|
|
import java.io.UnsupportedEncodingException;
|
|
|
import java.net.URLEncoder;
|
|
|
@@ -31,9 +34,16 @@ import java.nio.charset.StandardCharsets;
|
|
|
import java.time.LocalDate;
|
|
|
import java.time.LocalDateTime;
|
|
|
import java.time.format.DateTimeFormatter;
|
|
|
+import java.util.ArrayList;
|
|
|
import java.util.HashSet;
|
|
|
import java.util.List;
|
|
|
import java.util.Set;
|
|
|
+import java.util.concurrent.CompletableFuture;
|
|
|
+import java.util.concurrent.ExecutorService;
|
|
|
+import java.util.concurrent.LinkedBlockingQueue;
|
|
|
+import java.util.concurrent.ThreadPoolExecutor;
|
|
|
+import java.util.concurrent.TimeUnit;
|
|
|
+import java.util.stream.Collectors;
|
|
|
import java.util.zip.ZipEntry;
|
|
|
import java.util.zip.ZipOutputStream;
|
|
|
|
|
|
@@ -48,6 +58,37 @@ public class VppArchiveServiceImpl implements VppArchiveService {
|
|
|
@Autowired
|
|
|
private VppFileArchiveMapper vppFileArchiveMapper;
|
|
|
|
|
|
+ /**
|
|
|
+ * 连接池化的 RestTemplate,避免每次创建新连接
|
|
|
+ */
|
|
|
+ private final RestTemplate restTemplate;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 并行下载线程池
|
|
|
+ */
|
|
|
+ private static final ExecutorService DOWNLOAD_EXECUTOR = new ThreadPoolExecutor(
|
|
|
+ 4, 8, 60L, TimeUnit.SECONDS,
|
|
|
+ new LinkedBlockingQueue<>(200),
|
|
|
+ r -> {
|
|
|
+ Thread t = new Thread(r, "archive-download-");
|
|
|
+ t.setDaemon(true);
|
|
|
+ return t;
|
|
|
+ },
|
|
|
+ new ThreadPoolExecutor.CallerRunsPolicy()
|
|
|
+ );
|
|
|
+
|
|
|
+ {
|
|
|
+ // 配置 HttpClient 连接池(最大连接数、超时时间)
|
|
|
+ CloseableHttpClient httpClient = HttpClientBuilder.create()
|
|
|
+ .setMaxConnTotal(50)
|
|
|
+ .setMaxConnPerRoute(20)
|
|
|
+ .build();
|
|
|
+ HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient);
|
|
|
+ factory.setConnectTimeout(10_000);
|
|
|
+ factory.setReadTimeout(30_000);
|
|
|
+ this.restTemplate = new RestTemplate(factory);
|
|
|
+ }
|
|
|
+
|
|
|
@Override
|
|
|
public Boolean create(VppFileArchiveRequestVO vo) {
|
|
|
String username = SecurityUtils.getUsername();
|
|
|
@@ -126,7 +167,7 @@ public class VppArchiveServiceImpl implements VppArchiveService {
|
|
|
@Override
|
|
|
public Boolean upload(VppFileArchiveRequestVO vo) {
|
|
|
if (vo.getId() == null) {
|
|
|
- throw new RuntimeException("主键ID不能为空!");
|
|
|
+ throw new BusinessException("主键ID不能为空!");
|
|
|
}
|
|
|
|
|
|
LambdaQueryWrapper<VppFileArchive> queryWrapper = new LambdaQueryWrapper<>();
|
|
|
@@ -136,7 +177,7 @@ public class VppArchiveServiceImpl implements VppArchiveService {
|
|
|
VppFileArchive existingRecord = vppFileArchiveMapper.selectOne(queryWrapper);
|
|
|
|
|
|
if (existingRecord == null) {
|
|
|
- throw new RuntimeException("档案记录不存在或已被删除!");
|
|
|
+ throw new BusinessException("档案记录不存在或已被删除!");
|
|
|
}
|
|
|
|
|
|
// 根据 name + url 关联 FilesUpload 表,同步更新文件信息
|
|
|
@@ -153,7 +194,7 @@ public class VppArchiveServiceImpl implements VppArchiveService {
|
|
|
existingRecord.setFileType(filesUpload.getType());
|
|
|
existingRecord.setFileSize(filesUpload.getSize());
|
|
|
} else {
|
|
|
- throw new RuntimeException("未找到匹配的文件记录,请确认文件名与路径是否正确!");
|
|
|
+ throw new BusinessException("未找到匹配的文件记录,请确认文件名与路径是否正确!");
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -223,6 +264,25 @@ public class VppArchiveServiceImpl implements VppArchiveService {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
+ // ========== 预解析文件名(去重处理,必须在并行下载之前完成)==========
|
|
|
+ List<DownloadTask> tasks = new ArrayList<>();
|
|
|
+ Set<String> usedNames = new HashSet<>();
|
|
|
+ for (VppFileArchive archive : archives) {
|
|
|
+ String fileUrl = archive.getFileUrl();
|
|
|
+ String fileName = archive.getFileName();
|
|
|
+ if (StringUtils.isBlank(fileUrl) || StringUtils.isBlank(fileName)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ String zipEntryName = resolveDuplicateName(fileName, usedNames);
|
|
|
+ usedNames.add(zipEntryName);
|
|
|
+ tasks.add(new DownloadTask(zipEntryName, fileUrl));
|
|
|
+ }
|
|
|
+
|
|
|
+ if (tasks.isEmpty()) {
|
|
|
+ writeErrorResponse(response, HttpStatus.BAD_REQUEST.value(), "没有可下载的文件!");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
// ========== 构造 ZIP 文件名 ==========
|
|
|
String zipFileName = "电子档案_"
|
|
|
+ LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"))
|
|
|
@@ -233,36 +293,43 @@ public class VppArchiveServiceImpl implements VppArchiveService {
|
|
|
response.setHeader(HttpHeaders.CONTENT_DISPOSITION,
|
|
|
"attachment; filename=" + encodeFileName(zipFileName));
|
|
|
|
|
|
- // ========== 生成 ZIP 并写入响应流 ==========
|
|
|
- RestTemplate restTemplate = new RestTemplate();
|
|
|
- Set<String> usedNames = new HashSet<>();
|
|
|
+ // ========== 并行下载所有文件 ==========
|
|
|
+ List<CompletableFuture<DownloadTask>> futures = tasks.stream()
|
|
|
+ .map(task -> CompletableFuture.supplyAsync(() -> {
|
|
|
+ try {
|
|
|
+ byte[] bytes = restTemplate.getForObject(task.fileUrl, byte[].class);
|
|
|
+ task.fileBytes = bytes;
|
|
|
+ } catch (Exception e) {
|
|
|
+ task.error = e.getMessage();
|
|
|
+ }
|
|
|
+ return task;
|
|
|
+ }, DOWNLOAD_EXECUTOR))
|
|
|
+ .collect(Collectors.toList());
|
|
|
|
|
|
+ // ========== 生成 ZIP 并写入响应流 ==========
|
|
|
try (ServletOutputStream sos = response.getOutputStream();
|
|
|
- ZipOutputStream zos = new ZipOutputStream(sos)) {
|
|
|
-
|
|
|
- for (VppFileArchive archive : archives) {
|
|
|
- String fileUrl = archive.getFileUrl();
|
|
|
- String fileName = archive.getFileName();
|
|
|
-
|
|
|
- if (StringUtils.isBlank(fileUrl) || StringUtils.isBlank(fileName)) {
|
|
|
- continue;
|
|
|
- }
|
|
|
-
|
|
|
- String zipEntryName = resolveDuplicateName(fileName, usedNames);
|
|
|
- usedNames.add(zipEntryName);
|
|
|
+ BufferedOutputStream bos = new BufferedOutputStream(sos, 8192);
|
|
|
+ ZipOutputStream zos = new ZipOutputStream(bos)) {
|
|
|
|
|
|
+ for (CompletableFuture<DownloadTask> future : futures) {
|
|
|
+ DownloadTask task = future.join(); // 阻塞等待当前文件下载完成
|
|
|
try {
|
|
|
- byte[] fileBytes = restTemplate.getForObject(fileUrl, byte[].class);
|
|
|
- if (fileBytes != null && fileBytes.length > 0) {
|
|
|
- ZipEntry zipEntry = new ZipEntry(zipEntryName);
|
|
|
+ if (task.error != null) {
|
|
|
+ // 下载失败,写入错误信息
|
|
|
+ ZipEntry errorEntry = new ZipEntry(task.zipEntryName + ".error.txt");
|
|
|
+ zos.putNextEntry(errorEntry);
|
|
|
+ zos.write(("下载失败: " + task.error).getBytes(StandardCharsets.UTF_8));
|
|
|
+ zos.closeEntry();
|
|
|
+ } else if (task.fileBytes != null && task.fileBytes.length > 0) {
|
|
|
+ ZipEntry zipEntry = new ZipEntry(task.zipEntryName);
|
|
|
zos.putNextEntry(zipEntry);
|
|
|
- zos.write(fileBytes);
|
|
|
+ zos.write(task.fileBytes);
|
|
|
zos.closeEntry();
|
|
|
}
|
|
|
} catch (Exception e) {
|
|
|
- ZipEntry errorEntry = new ZipEntry(zipEntryName + ".error.txt");
|
|
|
+ ZipEntry errorEntry = new ZipEntry(task.zipEntryName + ".error.txt");
|
|
|
zos.putNextEntry(errorEntry);
|
|
|
- zos.write(("下载失败: " + e.getMessage()).getBytes(StandardCharsets.UTF_8));
|
|
|
+ zos.write(("写入ZIP失败: " + e.getMessage()).getBytes(StandardCharsets.UTF_8));
|
|
|
zos.closeEntry();
|
|
|
}
|
|
|
}
|
|
|
@@ -277,6 +344,21 @@ public class VppArchiveServiceImpl implements VppArchiveService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 下载任务内部类,承载文件名、URL、下载结果
|
|
|
+ */
|
|
|
+ private static class DownloadTask {
|
|
|
+ final String zipEntryName;
|
|
|
+ final String fileUrl;
|
|
|
+ volatile byte[] fileBytes;
|
|
|
+ volatile String error;
|
|
|
+
|
|
|
+ DownloadTask(String zipEntryName, String fileUrl) {
|
|
|
+ this.zipEntryName = zipEntryName;
|
|
|
+ this.fileUrl = fileUrl;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 处理 ZIP 内重名文件,自动添加编号后缀
|
|
|
* @param fileName 原始文件名
|