springboot响应文件流文件给浏览器+前端下载

springboot响应文件流文件给浏览器+前端下载


1.controller:

java 复制代码
@Api(tags = {"【样本提取系统】-api"})
@RestController("YbtqYstbtqController")
@RequiredArgsConstructor
@RequestMapping("/ybtq-ystbtq")
@Slf4j
public class YbtqYstbtqController {

private final YbService ybService;

@ApiOperation(value = "【样本集管理】-样本集导出", notes = "export", produces = "application/octet-stream")
    @PostMapping("/ybgl-set-cloud-download")
    @OpLog("【样本集管理】-样本集导出")
    public void ybGlYbSetDownload(@RequestBody List<String> pkIds, HttpServletResponse response) throws Exception {
        FileDownloadVo fileDownloadVo = tileSetService.ybGlYbSetDownload(pkIds);
        if (null != fileDownloadVo && fileDownloadVo.getFileStream() != null) {
            try (InputStream inputStream = fileDownloadVo.getFileStream();
                 OutputStream outputStream = response.getOutputStream()) {
                //先将字符串以UTF-8转换成字节数组,再将字节数组以ISO-8859-1转换字符串(标准)
                String fileName = new String(fileDownloadVo.getFileName().getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1);
                response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName,"utf-8"));
                response.setContentType(fileDownloadVo.getContentType());
                response.setCharacterEncoding("UTF-8");
                // 响应文件给浏览器
                IOUtils.copy(inputStream, outputStream);
            } catch (Exception e) {
                e.printStackTrace();
            }
        } else{
            response.setContentType("application/json;charset=utf-8");
            response.getWriter().write(JSONUtil.toJsonStr(Result.error("所选样本集无有效文件!")));
        }
    }

}

文件下载对象FileDownloadVo:

java 复制代码
/**
 * 文件下载对象
 */
@Data
public class FileDownloadVo {
    /**
     * 文件名称
     */
    @ApiModelProperty("文件名称")
    private String fileName;
    /**
     * 文件content-type
     */
    @ApiModelProperty("content-type")
    private String contentType;

    /**
     * 文件流
     */
    @ApiModelProperty("文件流")
    private InputStream fileStream;
}

2.serviceImpl:

java 复制代码
@Override
    public FileDownloadVo ybGlYbSetDownload(List<String> pkIds) {
        // 文件根目录
        FileDownloadVo ret = new FileDownloadVo();
        // 获取样本集信息
        List<TileSet> ybSetList = tileSetMapper.selectList(new LambdaQueryWrapper<TileSet>().in(TileSet::getPkId, pkIds));
        // 查询到的样本集,影像压缩成一个zip
        if (ybSetList.size() > 0) {
            try {
                // 获取当前日期并格式化为 yyyyMMdd
                String currentDate = new SimpleDateFormat("yyyyMMdd").format(new Date());
                // 创建临时 ZIP 文件
                File tempZipFile = Files.createTempFile("样本集_" + currentDate + "_", ".zip").toFile();

                // 创建临时根文件夹用于存放样本文件夹
                File tempRootDir = Files.createTempDirectory("样本集_" + currentDate + "_").toFile();

                for (TileSet tileSet : ybSetList) {
                    // 为每个样本集创建一个文件夹
                    File sampleDir = new File(tempRootDir, tileSet.getTileName() + "_" + tileSet.getPkId());
                    sampleDir.mkdirs();

                    // 获取文件复制到临时目录下
                    setTitleSetFile(tileSet, sampleDir);
                }
                try (ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(tempZipFile))) {
                    // 遍历 tempRootDir 下的所有子文件夹并添加到 ZIP 文件中
                    File[] subDirs = tempRootDir.listFiles();
                    if (subDirs != null) {
                        for (File subDir : subDirs) {
                            UnZipUtils.zipFiles(subDir, subDir.getName(), zipOut); // 将每个子文件夹递归添加到 ZIP
                        }
                    }
                } finally {
                    // 删除临时文件夹及其内容
                    UnZipUtils.deleteDirectory(tempRootDir);
                }

                try {
                    // 将 ZIP 文件转为 FileInputStream
                    ret.setFileStream(new FileInputStream(tempZipFile));
                } catch (IOException e) {
                    e.printStackTrace();
                }

                // 异步任务:300秒后删除文件
                UnZipUtils.deleteAsyncFiles(tempZipFile);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        ret.setContentType("application/zip");
        ret.setFileName(String.valueOf(new Date().getTime()));
        return ret;
    }


// 获取文件复制到临时目录下
    private void setTitleSetFile(TileSet tileSet, File sampleDir) {
        String tileSetPath = tileSet.getTilePath();

        boolean isTileSetPath = tileSetPath != null && !Objects.equals(tileSetPath, "");
        if (isTileSetPath) {
            // 获取样本切片file
            List<File> tileSetFileList = UnZipUtils.getFileByImagePaths(tileSetPath);
            for (File file : tileSetFileList) {
                try {
                    Files.copy(file.toPath(), new File(sampleDir, file.getName()).toPath(), StandardCopyOption.REPLACE_EXISTING);
                } catch (Exception e) {
                    System.out.print("前时项影像文件路径无效:" + file.getPath());
                }
            }
        }
    }

/**
     * 辅助方法,用于从ZIP输入流中提取文件
     *
     * @param zis ZIP输入流
     * @param filePath 文件的完整路径
     * @throws IOException 如果发生I/O错误
     */
    public static void extractFile(ZipInputStream zis, String filePath) throws IOException {
        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(filePath));
        byte[] bytesIn = new byte[4096];
        int read = 0;
        while ((read = zis.read(bytesIn)) != -1) {
            bos.write(bytesIn, 0, read);
        }
        bos.close();
    }


public static void zipFiles(File fileToZip, String fileName, ZipOutputStream zipOut) throws IOException {
        if (fileToZip.isHidden()) return;

        if (fileToZip.isDirectory()) {
            if (fileName.endsWith("/")) {
                zipOut.putNextEntry(new ZipEntry(fileName));
                zipOut.closeEntry();
            } else {
                zipOut.putNextEntry(new ZipEntry(fileName + "/"));
                zipOut.closeEntry();
            }

            File[] children = fileToZip.listFiles();
            for (File childFile : children) {
                zipFiles(childFile, fileName + "/" + childFile.getName(), zipOut);
            }
            return;
        }

        try (FileInputStream fis = new FileInputStream(fileToZip)) {
            ZipEntry zipEntry = new ZipEntry(fileName);
            zipOut.putNextEntry(zipEntry);
            byte[] bytes = new byte[1024];
            int length;
            while ((length = fis.read(bytes)) >= 0) {
                zipOut.write(bytes, 0, length);
            }
        }
    }

public static void deleteDirectory(File file) {
        File[] contents = file.listFiles();
        if (contents != null) {
            for (File f : contents) {
                deleteDirectory(f);
            }
        }
        file.delete();
    }

// 异步任务-删除临时文件
    public static void deleteAsyncFiles(File tempZipFile) {
        executorService.submit(() -> {
            try {
                TimeUnit.SECONDS.sleep(300);
                Files.deleteIfExists(tempZipFile.toPath());
                System.out.println("临时压缩包已删除:" + tempZipFile.toPath());
            } catch (Exception e) {
                System.err.println("临时压缩包删除失败:" + e.getMessage());
            }
        });
    }

public static List<File> getFileByImagePaths(String imagePaths) {
        List<File> ret = new ArrayList<>();
        if (imagePaths != null && !imagePaths.equals("")) {
            List<String> preImagePathList = Arrays.asList(imagePaths.split(","));
            if (preImagePathList.size() > 0) {
                for (String preImagePath : preImagePathList) {
                    // 根据path获取文件
                    File file = new File(preImagePath);
                    ret.add(file);
                }
            }
        }
        return ret;
    }

3.前端下载:

js 复制代码
/**
   * 【样本集管理】-样本集导出
   */
  ybGlYbSetDownload(context = undefined, params = undefined, fileName) {
    const url = `${this.interfaceUrl}/ybtq-ystbtq/ybgl-set-cloud-download`;
    return ajaxHelperInstance.downloadFilePost(url, fileName, {}, params);
  }

async downloadFilePost(url, fileName, urlParam, data = {}, requestMethod = 'POST', vueContext = undefined, spinName = 'spinning', useStreamSaver = false) {
        url = this.prepareUrl(url, {...urlParam, ...data});
        data = convertDateParam(data);
        setVueContextSpinState(vueContext, spinName);
        const requestOptions = this.setupRequestOptions(requestMethod, data);

        try {
            let response;
            if (useStreamSaver) {
                await this.downloadWithStream(url, fileName, requestOptions);
            } else {
                response = await axios({url, ...requestOptions});
                await this.handleResponse(response, fileName);
            }
        } catch (error) {
            this.handleError(error);
        } finally {
            resetVueContextSpinState(vueContext, spinName);
        }
    }


prepareUrl(url, params) {
        const reqData = _.map(params, (val, prop) => ({name: prop, value: val}));
        const queryString = abp.utils.buildQueryString(reqData);
        return url + queryString;
    }

/**
 * 转换时间参数,主要处理表单传入参数含有moment对象的问题
 * @param obj 传入对象
 * @constructor
 */
function convertDateParam(obj) {
    _.each(obj, (val, prop) => {
        if (val instanceof moment) {
            obj[prop] = (val as any).format('YYYY-MM-DD HH:mm:ss');
        }

        if (val instanceof Date) {
            obj[prop] = moment(val)
                .format('YYYY-MM-DD HH:mm:ss');
        }
    });
    return obj;
}


function setVueContextSpinState(vueContext, spinName: string) {
    if (vueContext && vueContext[spinName] != undefined) {
        vueContext[spinName] = true;
    }
}


setupRequestOptions(method, data = null) {
        const headers = {
            'Content-Type': 'application/json',
            'Authorization': 'Bearer ' + window.abp.auth.getToken(),
        };
        return {
            method: method,
            headers: headers,
            responseType: 'blob',
            data: data ? JSON.stringify(data) : undefined,
        };
    }

/**
     * 通过fetch Api进行流式下载
     * @param url 下载地址
     * @param fileName 文件名称
     * @param options 下载选项
     */
    async downloadWithStream(url, fileName, options: any = {}) {
        const {timeout = 1000 * 60 * 180} = options;
        const response = await this.fetchWithTimeout(url, options, timeout);
        if (!response.ok) {
            notification.warning({
                message: '提示',
                description: '下载失败',
            });
            throw new Error('下载失败');
        }
        const fileStream = StreamSaver.createWriteStream(fileName);
        const writer = fileStream.getWriter();
        if (response.body.pipeTo) {
            writer.releaseLock();
            return response.body.pipeTo(fileStream);
        }

        const reader = response.body.getReader();
        const pump = () =>
            reader
                .read()
                .then(({value, done}) => (done ? writer.close() : writer.write(value).then(pump)));

        return pump();
    }

async handleResponse(response, fileName) {
        const contentType = response.headers['content-type'];
        console.log(contentType);
        if (contentType && contentType.includes('application/json')) {
            const reader = new FileReader();
            reader.onload = () => {
                try {
                    const jsonData = JSON.parse(reader.result as string);
                    if (jsonData && jsonData.message) {
                        notification.error({
                            message: '提示',
                            description: jsonData.message,
                        });
                    }
                } catch (error) {
                    console.error('解析json失败:', error);
                }
            };
            reader.readAsText(response.data);
        } else {
            const blob = new Blob([response.data]);
            const link = document.createElement('a');
            link.href = URL.createObjectURL(blob);
            link.download = fileName;
            link.click();
            URL.revokeObjectURL(link.href);
        }
    }


handleError(error) {
        console.error('下载失败:', error);
    }


function resetVueContextSpinState(vueContext, spinName: string) {
    if (vueContext && vueContext[spinName] == true) {
        vueContext[spinName] = false;
    }
}
相关推荐
想用offer打牌7 小时前
MCP (Model Context Protocol) 技术理解 - 第二篇
后端·aigc·mcp
崔庆才丨静觅7 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60618 小时前
完成前端时间处理的另一块版图
前端·github·web components
KYGALYX8 小时前
服务异步通信
开发语言·后端·微服务·ruby
掘了8 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅8 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅9 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
爬山算法9 小时前
Hibernate(90)如何在故障注入测试中使用Hibernate?
java·后端·hibernate
猫头虎9 小时前
如何排查并解决项目启动时报错Error encountered while processing: java.io.IOException: closed 的问题
java·开发语言·jvm·spring boot·python·开源·maven