| | |
| | | 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; |
| | |
| | | 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(); |
| | |
| | | } |
| | | 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); |