• 在线问题反馈模块实战(二十):实现文件批量导出到zip压缩包中功能


    👨‍🎓作者:bug菌

    ✏️博客:CSDN掘金

    💌公众号:猿圈奇妙屋

    🚫特别声明:原创不易,转载请附上原文出处链接和本文声明,谢谢配合。

    🙏版权声明:文章里可能部分文字或者图片来源于互联网或者百度百科,如有侵权请联系bug菌处理。 

    一、前言🔥

           接下来的这几期,bug菌想跟大家分享一下自己昨天刚接到一个临时的需求,热乎着呢,想分享一下自己是如何面对临时需求并制定整个开发周期,其中包括从梳理业务到创建业务表再到实现业务逻辑形成闭环再到与前端对接,其中会穿插一些业务拓展及功能性拓展,这一条龙流程在线与大家一起见证,分享给刚入门的小伙伴,希望对你们有所帮助。

    环境说明:idea2019.3 + springboot2.3.1.REALSE + mybati-plus3.2.0 + mysql5.6 + jdk1.8

           若小伙伴们在批阅文章的过程中觉得文章对您有一丝丝帮助,还请别吝啬您手里的赞呀,大胆的把文章点亮👍吧,您的点赞三连(收藏⭐️+关注👨‍🎓+留言📃)就是对bug菌我创作道路上最好的鼓励与支持😘。时光不弃🏃🏻‍♀️,创作不停💕,加油☘️ 

    二、需求描述🔥

            此需求完全是针对管理员个人而开放的,需求方要求能将所有人的反馈文件导出到一个指定的文件夹中,并且最好是能导出一个.zip的压缩包,这样就方便它挨个挨个浏览查阅,也方便运维人员针对文件进行备份存档。

            我一听,这其实也是io操作的一种,虽然不是很常用,但是基本想实现该需求,也是简单的为此,我还是基于文件流的写法来逐一实现如何将批量实现文件的zip压缩,如果你也遇到的了这个需求并且没有啥思路,不用担心,你接下来只需要根据我写的实现逻辑,即可轻松带你解决你的需求问题,如果你是想接触了解,我写的也是非常详细,实现及测试,就地解决你的一切阅读所带来的不便。

            接下来,废话不多说,直接上代码。

    三、代码实现🔥

    1️⃣定义Controller请求

            首先我们先定义个接口请求,子路径名顾名思义,就是最好定义为能够见名知意的接口路径名,比如我这该需求是直接将图片导出,那我直接定义为export-questions-images即可。

    1. /**
    2. * 所有问题反馈截图导出成zip(压缩包)
    3. */
    4. @GetMapping("/export-questions-images")
    5. @ApiOperation(value = "所有问题反馈截图导出成zip(压缩包)", notes = "所有问题反馈截图导出成zip(压缩包)")
    6. public void exportQuestionsImages(HttpServletResponse response) {
    7. commonService.exportQuestions(response);
    8. }

    2️⃣定义接口方法solveQuestion()

    1. /**
    2. * 所有问题反馈截图导出成zip(压缩包)
    3. */
    4. void exportQuestions(HttpServletResponse response);

    3️⃣实现exportQuestions()方法

            如下是核心实现方法,具体实现思路就是,进行了两次文件压缩,具体操作就是:先是对完整的个人文件夹进行分类,然后将对于子文件的文件添加进子文件夹中,然后遍历对每一个子文件夹进行压缩,然后再将所有的压缩包存放到一个父文件夹中,接着对父文件夹进行压缩,最后将父压缩包导出即可。

            涉及所有代码,若是有不清楚的地方,麻烦多研究几遍,我相信,你一定能学的明白的。 

    1. /**
    2. * 所有问题反馈截图导出成zip(压缩包)
    3. */
    4. @Override
    5. public void exportQuestions(HttpServletResponse response){
    6. //1、判断父级目录是否存在
    7. File rootPath = new File(questionRootPath);
    8. if (!rootPath.exists()) {
    9. //创建父目录文件夹
    10. rootPath.mkdirs();
    11. }
    12. //2、查询问题反馈表中有上传过截图的数据
    13. List questions = userQuestionsService.getQuestions();
    14. //3、一次压缩
    15. //先将所有人的截图放到同一文件夹中并压缩
    16. questions.forEach(p -> {
    17. //子文件夹命名
    18. String userId = p.getCreatorAccountId();
    19. //获取截图父级目录
    20. String parentPath = questionImg + userId;
    21. //将多路径转成图片数组
    22. String[] paths = p.getFilePaths().split(",");
    23. // 创建临时路径,存放压缩文件
    24. String zipFilePath = rootPath + "/" + userId + ".zip";
    25. //压缩输出流,包装流,将临时文件输出流包装成压缩流,将所有文件输出到这里,打成zip包
    26. try {
    27. ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(zipFilePath));
    28. // 循环调用压缩文件方法,将一个一个需要下载的文件打入压缩文件包
    29. for (String path : paths) {
    30. //拼接文件全路径
    31. String imagePath = parentPath + "/" + path;
    32. //判断文件是否存在
    33. File file = new File(imagePath);
    34. if (!file.exists()) {
    35. continue;
    36. }
    37. try {
    38. //将文件添加到指定的压缩包中
    39. uploader.fileToZip(imagePath, zipOut);
    40. } catch (IOException e) {
    41. e.printStackTrace();
    42. }
    43. }
    44. // 压缩完成后,关闭压缩流
    45. zipOut.close();
    46. } catch (IOException e) {
    47. e.printStackTrace();
    48. }
    49. });
    50. //4、二次压缩
    51. //二次压缩(压缩父级目录)
    52. String downloadPath = rootPath + "/" + "问题反馈截图.zip";
    53. try {
    54. ZipOutputStream zipOut1 = new ZipOutputStream(new FileOutputStream(downloadPath));
    55. //遍历获取所有的父级目录
    56. questions.forEach(p -> {
    57. //拼接父级目录
    58. String path = rootPath + "/" + p.getCreatorAccountId() + ".zip";
    59. //获取存放文件的跟路径
    60. try {
    61. uploader.fileToZip(path, zipOut1);
    62. } catch (IOException e) {
    63. e.printStackTrace();
    64. }
    65. });
    66. // 压缩完成后,关闭压缩流
    67. zipOut1.close();
    68. //拼接下载默认名称并转为uft-8格式
    69. response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode("在线问题反馈截图.zip", StandardCharsets.UTF_8.name()));
    70. //该流不可以手动关闭,手动关闭下载会出问题,下载完成后会自动关闭
    71. ServletOutputStream outputStream = response.getOutputStream();
    72. FileInputStream inputStream = new FileInputStream(downloadPath);
    73. // copy方法为文件复制,在这里直接实现了下载效果
    74. IOUtils.copy(inputStream, outputStream);
    75. } catch (IOException e) {
    76. e.printStackTrace();
    77. }
    78. //下载完成之后,本地删掉这个zip
    79. File fileTempZip = new File(downloadPath);
    80. fileTempZip.delete();
    81. }

    4️⃣实现getQuestions()方法

            如下是确定数据来源,将图片地址不为空的数据查询出来。 

    1. /**
    2. * 查询已上传过截图的问题反馈
    3. */
    4. List getQuestions();
    1. /**
    2. * 查询已上传过截图的问题反馈
    3. */
    4. @Override
    5. public List getQuestions() {
    6. LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
    7. wrapper.isNotNull(UserQuestionsEntity::getFilePaths);//只查询截图路径不为空的数据
    8. return this.list(wrapper);
    9. }

    5️⃣实现文件写入压缩包方法fileToZip()

            如下是实现单个文件被压缩成zip的功能方法。

    1. /**
    2. * 将一个文件写入压缩包(一次只压缩一次)
    3. *
    4. * @param filePath 文件路径
    5. * @param zipOut 压缩路径
    6. */
    7. public void fileToZip(String filePath, ZipOutputStream zipOut) throws IOException {
    8. // 需要压缩的文件
    9. File file = new File(filePath);
    10. // 获取文件名称,如果有特殊命名需求,可以将参数列表拓展,传fileName
    11. String fileName = file.getName();
    12. FileInputStream fileInput = new FileInputStream(filePath);
    13. // 缓冲
    14. byte[] bufferArea = new byte[1024 * 10];
    15. BufferedInputStream bufferStream = new BufferedInputStream(fileInput, 1024 * 10);
    16. // 将当前文件作为一个zip实体写入压缩流,fileName代表压缩文件中的文件名称
    17. zipOut.putNextEntry(new ZipEntry(fileName));
    18. int length = 0;
    19. // 最常规IO操作,不必紧张
    20. while ((length = bufferStream.read(bufferArea, 0, 1024 * 10)) != -1) {
    21. zipOut.write(bufferArea, 0, length);
    22. }
    23. //关闭流
    24. fileInput.close();
    25. // 需要注意的是缓冲流必须要关闭流,否则输出无效
    26. bufferStream.close();
    27. // 压缩流不必关闭,使用完后再关
    28. }

    6️⃣定义全局路径配置

    yaml文件配置:

    1. #自定义配置项
    2. review:
    3. #文件存放地址
    4. file:
    5. question-zip-path: ./template/question-zip/
    6. question-img: ./template/question-img/

    获取方式: 

    1. @Value("${review.file.question-zip-path}")
    2. private String questionRootPath;
    3. @Value("${review.file.question-img}")
    4. private String questionImg;

    7️⃣涉及存放地址展示

             如下是我直接存放在项目根目录下的template文件夹下。

            大概你们可以参考一下,观摩我的代码然后对照该目录结构,这样你们方便理解。 

    四、测试🔥

            接下来,我们就对该接口进行测试,由于我是将该接口加入了token白名单,所以我们就不需要通过设置接口请求头了。我们只需要在浏览器输入完整访问地址即可,

    比如如下演示:

            输入地址后,我们直接浏览器回车,我们可以看看到浏览器左下角会弹出一个xxx.zip的压缩包下载,这就证明我们起码成功了一半。

            接下来,我们再检查一下,具体的文件夹子个数及子文件夹具体images数量,核实一下是否与数据库数据一致?经我查验,都是完整导出完好无损的。

            正常给大家看下我后台查询数据所存储数据库的原本记录格式吧。也方便大家核对子文件压缩包数量是否一致。

            具体给大家看一眼,对于admin该条记录而言,该用户是共上传了两个截图,所以在我们的导出包中对于admin.zip目录里应该就是对于的这两xxx.jpg图片才是,我给大家打开核实一下。

    大家请看:

            最后看下控制台,是否有导出异常信息?很正常,除了查询接口sql打印无其他打印内容,证明代码导出不存在显性问题,大家可以正常拿去使用借鉴啦。

     ... ...

            好啦,以上就是这期的所有内容啦,你们学废了么?如果对你有所帮助,还请不要忘记给bug菌[三连支持]哟。如果想获得更多的学习资源或者想和更多的技术爱好者一起交流,可以关注我的公众号『猿圈奇妙屋』,后台回复关键词领取学习资料、大厂面经、面试模板等海量资源,就等你来拿。

    五、往期热文推荐🔥

            对于问题反馈模块实战开发,我完整的梳理了每一期的教学及链接地址,仅供参考:希望能对你们有所帮助。

            如上是整整二十期内容,每一期都是干货,对于一个模块的开发,如何一点一滴打造并测试部署上线,我再说一遍,这不是演习,是实战!是实战!是实战!

            若你们觉得只是需要了解其中某个知识点或者业务的话,也不反对,你就选择其中的几期进行学习就好,反正都已经完结啦;我只希望你们能有所收获,有所成长,也就不枉我苦心每天下班后给大家总结更新。

    六、文末🔥

            如果你还想要学习更多,小伙伴们大可关注bug菌专门为你们创建的专栏《springboot零基础入门教学》,都是我一手打下的江山,持续更新中,希望能帮助到更多小伙伴们。

           我是bug菌,一名想走👣出大山改变命运的程序猿。接下来的路还很长,都等待着我们去突破、去挑战。来吧,小伙伴们,我们一起加油!未来皆可期,fighting!

            最后送大家两句我很喜欢的话,与诸君共勉!


    ☘️做你想做的人,没有时间限制,只要愿意,什么时候都可以start。

    🍀你能从现在开始改变,也可以一成不变,这件事,没有规矩可言,你可以活出最精彩的自己。


    ​​​​​​

    💌如果文章对您有所帮助,就请留下您的吧!(#^.^#);

    💝如果喜欢bug菌分享的文章,就请给bug菌点个关注吧!(๑′ᴗ‵๑)づ╭❤~;

    💗如果对文章有任何疑问,还请文末留言或者加群吧【QQ交流群:708072830】;

    💞鉴于个人经验有限,所有观点及技术研点,如有异议,请直接回复参与讨论(请勿发表攻击言论,谢谢);

    💕版权声明:原创不易,转载请附上原文出处链接和本文声明,版权所有,盗版必究!!!谢谢。

  • 相关阅读:
    48、线程
    [附源码]计算机毕业设计基于SpringBoot的实验填报管理系统
    服务器之间免密登录
    重新整理汇编—————汇编的基础理论前置篇
    10. 同步控制指令
    如何高效管理接口文档
    Stable Diffusion中的embedding
    【微服务 32】你为Spring Cloud整合Seata、Nacos实现分布式事务案例跑不起来苦恼过吗(巨细排坑版)【云原生】
    远程连接ubuntu的mysql服务报错10061的解决方案
    【记录】Truenas scale|Truenas 的 SSH 服务连不上 VScode,终端能连上
  • 原文地址:https://blog.csdn.net/weixin_43970743/article/details/125788430