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; package com.nanri.aiimage.modules.similarasin.controller;
import com.nanri.aiimage.common.api.ApiResponse; 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.SimilarAsinFilterConditionAddRequest;
import com.nanri.aiimage.modules.similarasin.model.dto.SimilarAsinParseRequest; import com.nanri.aiimage.modules.similarasin.model.dto.SimilarAsinParseRequest;
import com.nanri.aiimage.modules.similarasin.model.dto.SimilarAsinParsedPayloadDto; import com.nanri.aiimage.modules.similarasin.model.dto.SimilarAsinParsedPayloadDto;
@@ -23,6 +22,7 @@ import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; 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.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.ResponseStatusException;
import java.io.InputStream;
import java.net.URI; import java.net.URI;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.List; import java.util.List;
@@ -181,38 +180,23 @@ public class SimilarAsinController {
@GetMapping("/results/{resultId}/download") @GetMapping("/results/{resultId}/download")
@Operation(summary = "下载相似 ASIN 检测结果文件") @Operation(summary = "下载相似 ASIN 检测结果文件")
public void downloadResult( public ResponseEntity<Void> downloadResult(
@Parameter(description = "结果记录 ID", required = true, example = "1001") @Parameter(description = "结果记录 ID", required = true, example = "1001")
@PathVariable Long resultId, @PathVariable Long resultId,
@Parameter(description = "当前用户 ID", required = true, example = "1") @Parameter(description = "当前用户 ID", required = true, example = "1")
@RequestParam("user_id") Long userId, @RequestParam("user_id") Long userId) {
jakarta.servlet.http.HttpServletResponse response) {
ResultDownloadInfo download = service.resolveResultDownloadInfo(resultId, userId); ResultDownloadInfo download = service.resolveResultDownloadInfo(resultId, userId);
if (download.url() == null || download.url().isBlank()) { if (download.url() == null || download.url().isBlank()) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "暂无可下载结果"); throw new ResponseStatusException(HttpStatus.NOT_FOUND, "暂无可下载结果");
} }
try { // 结果文件常见百 MB 级(货源结果含图片,单文件可达 130MB+):
response.setContentType(download.contentType()); // 原先在此流式代理整个对象,请求线程会被下载时长占满,并发时拖垮线程池。
if (download.size() > 0) { // 改为 302 重定向到对象存储直链:① 立即释放 Tomcat 线程;
response.setContentLengthLong(download.size()); // ② 直链支持 Range,客户端断点续传才真正生效(代理只回 200 全量,续传实际失效)。
} log.info("[相似ASIN] 结果文件下载重定向 结果ID={} 用户ID={} 文件名={} 字节数={}",
DownloadHeaderUtil.setAttachment(response, download.filename()); resultId, userId, download.filename(), download.size());
long written = 0L; return ResponseEntity.status(HttpStatus.FOUND)
try (InputStream in = URI.create(download.url()).toURL().openStream()) { .location(URI.create(download.url()))
byte[] buffer = new byte[65536]; .build();
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, "下载失败");
}
} }
} }