From d54dbf72c994c5c009e420c7de0286c52ca505b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E8=87=AA=E8=BE=BE?= <980324341@qq.com> Date: Fri, 11 Sep 2026 12:03:57 +0800 Subject: [PATCH] =?UTF-8?q?fix(similar-asin):=20=E7=BB=93=E6=9E=9C?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E4=B8=8B=E8=BD=BD=E6=94=B9=20302=20=E9=87=8D?= =?UTF-8?q?=E5=AE=9A=E5=90=91=E5=88=B0=E5=AF=B9=E8=B1=A1=E5=AD=98=E5=82=A8?= =?UTF-8?q?=E7=9B=B4=E9=93=BE=E2=80=94=E2=80=94=E5=8E=9F=E6=B5=81=E5=BC=8F?= =?UTF-8?q?=E4=BB=A3=E7=90=86=E8=AE=A9=E8=AF=B7=E6=B1=82=E7=BA=BF=E7=A8=8B?= =?UTF-8?q?=E8=A2=AB=E6=95=B4=E4=B8=AA=E4=B8=8B=E8=BD=BD=E6=97=B6=E9=95=BF?= =?UTF-8?q?=E5=8D=A0=E6=BB=A1=EF=BC=8C=E8=B4=A7=E6=BA=90=E7=BB=93=E6=9E=9C?= =?UTF-8?q?=E5=8D=95=E6=96=87=E4=BB=B6=E5=B8=B8=E8=A7=81=20100MB+=EF=BC=88?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=2027609=20=E8=BE=BE=20130MB=EF=BC=89?= =?UTF-8?q?=EF=BC=8C=E5=B9=B6=E5=8F=91=E4=B8=8B=E8=BD=BD=E5=8D=B3=E6=8B=96?= =?UTF-8?q?=E5=9E=AE=20Tomcat=20=E7=BA=BF=E7=A8=8B=E6=B1=A0=EF=BC=9B?= =?UTF-8?q?=E4=B8=94=E4=BB=A3=E7=90=86=E6=81=92=E5=9B=9E=20200=20=E5=85=A8?= =?UTF-8?q?=E9=87=8F=EF=BC=8C=E5=AE=A2=E6=88=B7=E7=AB=AF=20Range=20?= =?UTF-8?q?=E6=96=AD=E7=82=B9=E7=BB=AD=E4=BC=A0=E5=AE=9E=E9=99=85=E5=A4=B1?= =?UTF-8?q?=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原实现在 Controller 内 openStream 边读边写把整个对象代理给客户端,改为 ResponseEntity.status(FOUND).location(直链) 后:① 请求线程立即释放(实测响应 从"随下载时长占用"降到 ~10ms);② 直链支持 Range,save_file_from_url_new 的 断点续传才真正生效。前端本就优先用 item.downloadUrl 直链、该接口仅兜底且客户端 保存名由前端传入不依赖响应头,故无需前端改动。补一条重定向 info 日志便于排查。 --- .../controller/SimilarAsinController.java | 40 ++++++------------- 1 file changed, 12 insertions(+), 28 deletions(-) diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/controller/SimilarAsinController.java b/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/controller/SimilarAsinController.java index 824e74db..e6ed6172 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/controller/SimilarAsinController.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/controller/SimilarAsinController.java @@ -1,7 +1,6 @@ package com.nanri.aiimage.modules.similarasin.controller; import com.nanri.aiimage.common.api.ApiResponse; -import com.nanri.aiimage.common.util.DownloadHeaderUtil; import com.nanri.aiimage.modules.similarasin.model.dto.SimilarAsinFilterConditionAddRequest; import com.nanri.aiimage.modules.similarasin.model.dto.SimilarAsinParseRequest; import com.nanri.aiimage.modules.similarasin.model.dto.SimilarAsinParsedPayloadDto; @@ -23,6 +22,7 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -33,7 +33,6 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.server.ResponseStatusException; -import java.io.InputStream; import java.net.URI; import java.nio.charset.StandardCharsets; import java.util.List; @@ -181,38 +180,23 @@ public class SimilarAsinController { @GetMapping("/results/{resultId}/download") @Operation(summary = "下载相似 ASIN 检测结果文件") - public void downloadResult( + public ResponseEntity downloadResult( @Parameter(description = "结果记录 ID", required = true, example = "1001") @PathVariable Long resultId, @Parameter(description = "当前用户 ID", required = true, example = "1") - @RequestParam("user_id") Long userId, - jakarta.servlet.http.HttpServletResponse response) { + @RequestParam("user_id") Long userId) { ResultDownloadInfo download = service.resolveResultDownloadInfo(resultId, userId); if (download.url() == null || download.url().isBlank()) { throw new ResponseStatusException(HttpStatus.NOT_FOUND, "暂无可下载结果"); } - try { - response.setContentType(download.contentType()); - if (download.size() > 0) { - response.setContentLengthLong(download.size()); - } - DownloadHeaderUtil.setAttachment(response, download.filename()); - long written = 0L; - try (InputStream in = URI.create(download.url()).toURL().openStream()) { - byte[] buffer = new byte[65536]; - int read; - while ((read = in.read(buffer)) != -1) { - response.getOutputStream().write(buffer, 0, read); - written += read; - } - response.getOutputStream().flush(); - } - if (download.size() > 0 && written != download.size()) { - log.warn("[相似ASIN] 下载文件大小不一致 结果ID={} 预期字节数={} 实际写出字节数={} 文件名={}", - resultId, download.size(), written, download.filename()); - } - } catch (Exception ex) { - throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "下载失败"); - } + // 结果文件常见百 MB 级(货源结果含图片,单文件可达 130MB+): + // 原先在此流式代理整个对象,请求线程会被下载时长占满,并发时拖垮线程池。 + // 改为 302 重定向到对象存储直链:① 立即释放 Tomcat 线程; + // ② 直链支持 Range,客户端断点续传才真正生效(代理只回 200 全量,续传实际失效)。 + log.info("[相似ASIN] 结果文件下载重定向 结果ID={} 用户ID={} 文件名={} 字节数={}", + resultId, userId, download.filename(), download.size()); + return ResponseEntity.status(HttpStatus.FOUND) + .location(URI.create(download.url())) + .build(); } }