Bootstrap

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("所选样本集无有效文件!")));
        }
    }

}

文件下载对象FileDownloadVo:

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

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

2.serviceImpl:

@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.前端下载:


/**
   * 【样本集管理】-样本集导出
   */
  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;
    }
}


悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;