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