From 7db504cbc6d08e50666c86a3dbd6c7da5147b630 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期六, 17 十月 2026 13:28:09 +0800
Subject: [PATCH] fix(web): 修复客户端断开连接时的异常处理
---
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/qrexport/MesProQrExportTaskServiceImpl.java | 118 ++++++++++++++++++++++++++++++++++++++++++++---------------
1 files changed, 88 insertions(+), 30 deletions(-)
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/qrexport/MesProQrExportTaskServiceImpl.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/qrexport/MesProQrExportTaskServiceImpl.java
index 4bb7f10..711cdda 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/qrexport/MesProQrExportTaskServiceImpl.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/qrexport/MesProQrExportTaskServiceImpl.java
@@ -14,18 +14,21 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
-import org.springframework.util.StreamUtils;
import org.springframework.validation.annotation.Validated;
+import org.springframework.web.context.request.async.AsyncRequestNotUsableException;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
-import java.nio.file.Files;
+import java.io.OutputStream;
import java.time.LocalDateTime;
import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.RejectedExecutionException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_BAG_CODE_NOT_EXISTS;
@@ -60,6 +63,11 @@
MesProQrExportTaskStatusEnum.CANCELED.getStatus(),
MesProQrExportTaskStatusEnum.FAILED.getStatus(),
MesProQrExportTaskStatusEnum.DOWNLOADED.getStatus());
+
+ private static final Pattern BYTE_RANGE = Pattern.compile("^bytes=(\\d+)-(\\d*)$");
+
+ /** 闃叉瀹氭椂娓呯悊浠诲姟鍒犻櫎姝e湪浼犺緭鐨� ZIP銆� */
+ private final ConcurrentHashMap<Long, Integer> activeDownloads = new ConcurrentHashMap<>();
@Resource
private MesProQrExportTaskMapper taskMapper;
@@ -156,25 +164,86 @@
}
@Override
- public void download(Long id, HttpServletResponse response) throws IOException {
+ public void download(Long id, String range, HttpServletResponse response) throws IOException {
MesProQrExportTaskDO task = getTask(id);
if (!MesProQrExportTaskStatusEnum.FINISHED.getStatus().equals(task.getStatus())) {
throw exception(PRO_QR_EXPORT_TASK_STATUS_INVALID);
}
- File file = resolveTaskFile(task);
- response.setContentType("application/zip");
- response.setHeader("Content-Disposition",
- "attachment;filename=" + HttpUtils.encodeUtf8(task.getTaskName() + ".zip"));
- boolean success = false;
- try (InputStream in = new BufferedInputStream(new FileInputStream(file))) {
- StreamUtils.copy(in, response.getOutputStream());
- response.getOutputStream().flush();
- success = true;
- } finally {
- // 浠呭湪瀹屾暣浼犺緭鍚庡垹闄わ紱涓柇鏃朵繚鐣欙紝浜ょ敱娓呯悊浠诲姟鍦ㄤ繚鐣欐湡鍚庡垹闄わ紝渚夸簬鐭椂闂村唴閲嶈瘯
- if (success) {
- deleteAfterDownload(task, file);
+ activeDownloads.compute(id, (key, count) -> count == null ? 1 : count + 1);
+ try {
+ File file = resolveTaskFile(task);
+ long fileSize = file.length();
+ long[] bounds = resolveRange(range, fileSize);
+ response.setHeader("Accept-Ranges", "bytes");
+ response.setHeader("X-Accel-Buffering", "no");
+ response.setHeader("Cache-Control", "private, no-store");
+ response.setHeader("Access-Control-Expose-Headers", "Content-Length, Content-Range, Accept-Ranges");
+ if (bounds == null) {
+ response.setStatus(416);
+ response.setHeader("Content-Range", "bytes */" + fileSize);
+ return;
}
+ long start = bounds[0];
+ long end = bounds[1];
+ long length = end - start + 1;
+ response.setContentType("application/zip");
+ response.setHeader("Content-Disposition",
+ "attachment;filename=" + HttpUtils.encodeUtf8(task.getTaskName() + ".zip"));
+ if (range != null && !range.isBlank()) {
+ response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
+ response.setHeader("Content-Range", "bytes " + start + "-" + end + "/" + fileSize);
+ }
+ response.setContentLengthLong(length);
+ long sent = 0;
+ try (InputStream in = new BufferedInputStream(new FileInputStream(file))) {
+ in.skipNBytes(start);
+ OutputStream out = response.getOutputStream();
+ byte[] buffer = new byte[64 * 1024];
+ long remaining = length;
+ while (remaining > 0) {
+ int read = in.read(buffer, 0, (int) Math.min(buffer.length, remaining));
+ if (read < 0) {
+ throw new IOException("浜岀淮鐮佸鍑烘枃浠跺湪涓嬭浇杩囩▼涓鎴柇");
+ }
+ out.write(buffer, 0, read);
+ remaining -= read;
+ sent += read;
+ }
+ out.flush();
+ } catch (AsyncRequestNotUsableException e) {
+ // 娴忚鍣ㄥ彇娑堛�佺綉缁滄柇寮�鎴栦唬鐞嗗叧闂繛鎺ユ椂锛屽搷搴斿凡缁忎笉鍙啓锛屼笉鑳藉啀杩斿洖 JSON 閿欒銆�
+ // ZIP 淇濈暀鍒版竻鐞嗘椂闂达紝鍓嶇鍙噸璇曞綋鍓嶅垎娈碉紱璁板綍鑼冨洿涓庤繘搴︿究浜庢帓鏌ラ绻佹柇寮�銆�
+ log.warn("[download][瀹㈡埛绔腑鏂� taskId={}, range={}, sent={}/{} bytes: {}]",
+ id, range, sent, length, e.getMessage());
+ } catch (IOException e) {
+ // ZIP 鍝嶅簲澶村凡缁忓啓鍏ワ紝涓嶈兘浜ょ粰鍏ㄥ眬寮傚父澶勭悊鍣ㄥ簭鍒楀寲 CommonResult銆�
+ // Content-Length 涓嶅尮閰嶄細璁╁鎴风鍒ゅ畾鏈澶辫触骞堕噸璇曘��
+ log.error("[download][浼犺緭澶辫触 taskId={}, range={}, sent={}/{} bytes]",
+ id, range, sent, length, e);
+ }
+ } finally {
+ activeDownloads.computeIfPresent(id, (key, count) -> count > 1 ? count - 1 : null);
+ }
+ }
+
+ /** 鍙敮鎸佸崟涓棴鍖洪棿锛涗笉鍚堟硶鐨� Range 杩斿洖 416锛岄伩鍏嶈浼犳暣鍖呫�� */
+ private long[] resolveRange(String range, long fileSize) {
+ if (range == null || range.isBlank()) {
+ return new long[]{0, fileSize - 1};
+ }
+ Matcher matcher = BYTE_RANGE.matcher(range);
+ if (!matcher.matches() || fileSize <= 0) {
+ return null;
+ }
+ try {
+ long start = Long.parseLong(matcher.group(1));
+ long end = matcher.group(2).isEmpty() ? fileSize - 1 : Long.parseLong(matcher.group(2));
+ if (start >= fileSize || end < start) {
+ return null;
+ }
+ return new long[]{start, Math.min(end, fileSize - 1)};
+ } catch (NumberFormatException e) {
+ return null;
}
}
@@ -185,6 +254,9 @@
List<MesProQrExportTaskDO> list = taskMapper.selectExpiredFinalList(FINAL_STATUSES, deadline);
int count = 0;
for (MesProQrExportTaskDO task : list) {
+ if (activeDownloads.containsKey(task.getId())) {
+ continue;
+ }
if (StrUtil.isBlank(task.getFilePath())) {
continue;
}
@@ -278,20 +350,6 @@
throw exception(PRO_QR_EXPORT_TASK_FILE_MISSING);
}
return file;
- }
-
- /**
- * 涓嬭浇瀹屾垚鍗冲垹闄や复鏃舵枃浠讹細鍗曟瀵煎嚭鍙揪涓� GB锛岀暀鐫�浼氳繀閫熸妸纾佺洏鍚冩弧
- */
- private void deleteAfterDownload(MesProQrExportTaskDO task, File file) {
- try {
- Files.deleteIfExists(file.toPath());
- } catch (IOException e) {
- log.warn("[deleteAfterDownload][鍒犻櫎涓存椂鏂囦欢澶辫触 {}]", file.getAbsolutePath(), e);
- }
- taskMapper.clearFilePath(task.getId());
- taskMapper.updateStatus(task.getId(), MesProQrExportTaskStatusEnum.DOWNLOADED.getStatus(),
- null, null, null, null);
}
}
--
Gitblit v1.9.3