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-server/src/main/resources/static/trace.html                                                                                                  |    2 
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/qrexport/MesProQrExportTaskServiceImpl.java                                 |   13 ++++++
 yudao-framework/yudao-spring-boot-starter-web/src/test/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandlerClientAbortTest.java |   40 ++++++++++++++++++++
 yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java                |   10 +++++
 4 files changed, 64 insertions(+), 1 deletions(-)

diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java
index 0eb7f5e..905697d 100644
--- a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java
+++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java
@@ -17,6 +17,7 @@
 import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
 import com.google.common.util.concurrent.UncheckedExecutionException;
 import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
 import jakarta.validation.ConstraintViolation;
 import jakarta.validation.ConstraintViolationException;
 import jakarta.validation.ValidationException;
@@ -34,6 +35,7 @@
 import org.springframework.web.bind.MissingServletRequestParameterException;
 import org.springframework.web.bind.annotation.ExceptionHandler;
 import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.context.request.async.AsyncRequestNotUsableException;
 import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
 import org.springframework.web.multipart.MaxUploadSizeExceededException;
 import org.springframework.web.servlet.NoHandlerFoundException;
@@ -67,6 +69,14 @@
 
     private final ApiErrorLogCommonApi apiErrorLogApi;
 
+    /** 瀹㈡埛绔凡鏂紑鏃跺搷搴斾笉鍙啀鍐欙紱娉ㄥ叆 HttpServletResponse 浣� Spring MVC 鐩存帴缁撴潫澶勭悊銆� */
+    @ExceptionHandler(AsyncRequestNotUsableException.class)
+    public void asyncRequestNotUsableExceptionHandler(HttpServletRequest request,
+                                                      HttpServletResponse response,
+                                                      AsyncRequestNotUsableException ex) {
+        log.debug("[asyncRequestNotUsableExceptionHandler][瀹㈡埛绔凡鏂紑 uri={}]", request.getRequestURI());
+    }
+
     /**
      * 澶勭悊鎵�鏈夊紓甯革紝涓昏鏄彁渚涚粰 Filter 浣跨敤
      * 鍥犱负 Filter 涓嶈蛋 SpringMVC 鐨勬祦绋嬶紝浣嗘槸鎴戜滑鍙堥渶瑕佸厹搴曞鐞嗗紓甯革紝鎵�浠ヨ繖閲屾彁渚涗竴涓叏閲忕殑寮傚父澶勭悊杩囩▼锛屼繚鎸侀�昏緫缁熶竴銆�
diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/test/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandlerClientAbortTest.java b/yudao-framework/yudao-spring-boot-starter-web/src/test/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandlerClientAbortTest.java
new file mode 100644
index 0000000..e38d829
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-web/src/test/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandlerClientAbortTest.java
@@ -0,0 +1,40 @@
+package cn.iocoder.yudao.framework.web.core.handler;
+
+import jakarta.servlet.http.HttpServletResponse;
+import org.junit.jupiter.api.Test;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.context.request.async.AsyncRequestNotUsableException;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+
+import java.io.IOException;
+
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+class GlobalExceptionHandlerClientAbortTest {
+
+    @Test
+    void doesNotSerializeJsonAfterZipResponseBecomesUnusable() throws Exception {
+        MockMvcBuilders.standaloneSetup(new ZipController())
+                .setControllerAdvice(new GlobalExceptionHandler("test", null))
+                .build()
+                .perform(get("/zip"))
+                .andExpect(status().isOk())
+                .andExpect(content().bytes(new byte[0]));
+    }
+
+    @RestController
+    static class ZipController {
+
+        @GetMapping("/zip")
+        public void download(HttpServletResponse response) throws IOException {
+            response.setContentType("application/zip");
+            response.flushBuffer();
+            throw new AsyncRequestNotUsableException("client disconnected");
+        }
+
+    }
+
+}
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 e6adab6..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
@@ -15,6 +15,7 @@
 import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
+import org.springframework.web.context.request.async.AsyncRequestNotUsableException;
 
 import java.io.BufferedInputStream;
 import java.io.File;
@@ -193,6 +194,7 @@
                 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();
@@ -205,8 +207,19 @@
                     }
                     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);
diff --git a/yudao-server/src/main/resources/static/trace.html b/yudao-server/src/main/resources/static/trace.html
index d89fb73..fd796ac 100644
--- a/yudao-server/src/main/resources/static/trace.html
+++ b/yudao-server/src/main/resources/static/trace.html
@@ -327,7 +327,7 @@
         </div>
         <div class="contact-item">
           <span class="ico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9.5 12 3l9 6.5V20a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><path d="M9.5 22v-6h5v6"/></svg></span>
-          <span class="txt"><span class="lbl">鍏徃缃戝潃</span><div class="val"><a href="https://www.talimu.net" target="_blank" rel="noopener">www.talimu.net</a></div></span>
+          <span class="txt"><span class="lbl">鍏徃缃戝潃</span><div class="val"><a href="https://www.tahezy.com" target="_blank" rel="noopener">www.tahezy.com</a></div></span>
         </div>
         <div class="contact-item">
           <span class="ico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 11.5a8.4 8.4 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.4 8.4 0 0 1-3.8-.9L3 21l1.9-5.7a8.4 8.4 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.4 8.4 0 0 1 3.8-.9h.5a8.5 8.5 0 0 1 8 8z"/></svg></span>

--
Gitblit v1.9.3