fix(similar-asin): 结果文件下载改 302 重定向到对象存储直链——原流式代理让请求线程被整个下载时长占满,货源结果单文件常见 100MB+(任务 27609 达 130MB),并发下载即拖垮 Tomcat 线程池;且代理恒回 200 全量,客户端 Range 断点续传实际失效

原实现在 Controller 内 openStream 边读边写把整个对象代理给客户端,改为
ResponseEntity.status(FOUND).location(直链) 后:① 请求线程立即释放(实测响应
从"随下载时长占用"降到 ~10ms);② 直链支持 Range,save_file_from_url_new 的
断点续传才真正生效。前端本就优先用 item.downloadUrl 直链、该接口仅兜底且客户端
保存名由前端传入不依赖响应头,故无需前端改动。补一条重定向 info 日志便于排查。
This commit is contained in:
2026-09-11 12:03:57 +08:00
parent 94a0c287e0
commit d54dbf72c9
@@ -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<Void> 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();
}
}