springboot响应文件流文件给浏览器+前端下载
springboot响应文件流文件给浏览器+前端下载
1.controller:
@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("所选样本集无有效文件!")));}}}
2.serviceImpl:
@Overridepublic FileDownloadVo ybGlYbSetDownload(List<String> pkIds) {// 文件根目录FileDownloadVo ret = new FileDownloadVo();// 获取样本集信息List<TileSet> ybSetList = tileSetMapper.selectList(new LambdaQueryWrapper<TileSet>().in(TileSet::getPkId, pkIds));// 查询到的样本集,影像压缩成一个zipif (ybSetList.size() > 0) {try {// 获取当前日期并格式化为 yyyyMMddString 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 文件转为 FileInputStreamret.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) {// 获取样本切片fileList<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.前端下载:
/*** 【样本集管理】-样本集导出*/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;}
}