自学内容网 自学内容网

3.Java中根据用户需求将业务数据详情中的文件用压缩包的形式导出

继上一个需求后呢,最近又在另一个项目中遇到了一个相似的需求,就是用户要求将业务数据详情中的所有文件全部下载下来方便查看。

需求描述:

a、业务详情文件

b、实现效果

此时业务详情页面有导出按钮,要求导出时,要求所有文件打包成zip压缩包返回给用户,实现效果如下:

c、实现过程

1.添加pom依赖

<dependency>
  <groupId>org.docx4j</groupId>
  <artifactId>docx4j-JAXB-MOXy</artifactId>
  <version>8.3.13</version>
</dependency>
<dependency>
  <groupId>org.apache.logging.log4j</groupId>
  <artifactId>log4j-core</artifactId>
</dependency>
<dependency>
  <groupId>org.apache.poi</groupId>
  <artifactId>poi-ooxml</artifactId>
  <version>5.2.5</version>
</dependency>
<dependency>
  <groupId>commons-io</groupId>
  <artifactId>commons-io</artifactId>
  <version>2.13.0</version>
</dependency>
<dependency>
  <groupId>cn.hutool</groupId>
  <artifactId>hutool-all</artifactId>
  <version>5.8.32</version>
  <scope>compile</scope>
</dependency>

2.代码实现

a、文件操作工具类

package com.yinhai.stagetwo.util;

import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ZipUtil;
import com.yinhai.stagetwo.entity.export.ProjectData;
import org.apache.poi.xwpf.usermodel.*;
import org.docx4j.jaxb.Context;
import org.docx4j.openpackaging.exceptions.Docx4JException;
import org.docx4j.openpackaging.exceptions.InvalidFormatException;
import org.docx4j.openpackaging.packages.WordprocessingMLPackage;
import org.docx4j.openpackaging.parts.PartName;
import org.docx4j.openpackaging.parts.WordprocessingML.AlternativeFormatInputPart;
import org.docx4j.openpackaging.parts.WordprocessingML.MainDocumentPart;
import org.docx4j.relationships.Relationship;
import org.docx4j.wml.CTAltChunk;

import javax.xml.bind.JAXBException;
import java.io.*;
import java.lang.reflect.Field;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;

/**
 * @author gc
 * @date 2025/1/6 14:11
 */
public class GenerateOnWordTemplateKit {

    public static final String TEMPLATE_PATH = "本地项目地址 + /src/main/resources/template/";
  
    public static final String FILE_PATH = TEMPLATE_PATH + "file/";
    public static final String OUTPUT_PATH = TEMPLATE_PATH + "output/";

    public static void main(String[] args) throws Exception {
        // 程序入口,主要作用是为多个项目分别根据模版组装数据后合并成一个word
        // 1. 准备数据(模拟从数据库中查询,key为项目名称,value为项目数据)
        Map<String, ProjectData> dataMap = queryData();
        // 2. 为每个项目生成一个以项目名为名称的文件夹,文件夹内包括word文档、附件
        // 2.1 创建一个临时文件夹
        String tempDirPath = FileUtil.mkParentDirs("temp/" + UUID.randomUUID()).getAbsolutePath();
        // 2.2 在这个临时文件夹下面,根据数据生成多个项目文件夹及word文档、附件
        generateAllWordDocument(dataMap, tempDirPath);
        // 3. 打包整个临时文件夹为zip压缩包
        File projectZipFile = compressIntoAZipFile(tempDirPath);
        System.out.println("压缩包路径保存:" + projectZipFile.getAbsolutePath());
        // 4. 删除所有临时文件
        deleteAllTempFiles(tempDirPath);
    }

    /**
     * 打包整个临时文件夹为zip压缩包
     *
     * @param tempDirPath 需要打包的文件夹目录
     * @return 压缩包文件
     */
    public static File compressIntoAZipFile(String tempDirPath) {
        return ZipUtil.zip(tempDirPath, OUTPUT_PATH + "慈善奖附件_" + DateUtil.format(new Date(), DatePattern.PURE_DATETIME_PATTERN) + ".zip");
    }

    /**
     * 删除所有临时文件
     *
     * @param tempDirPath 临时文件夹路径
     */
    public static void deleteAllTempFiles(String tempDirPath) {
        FileUtil.del(tempDirPath);
    }

    /**
     * 生成所有项目文档
     *
     * @param dataMap     所有项目信息,key为项目名称,value为项目数据
     * @param tempDirPath 临时目录,用于存储多个临时Word文档
     * @throws Exception 异常
     */
    public static void generateAllWordDocument(Map<String, ProjectData> dataMap, String tempDirPath) throws Exception {
        for (Map.Entry<String, ProjectData> entry : dataMap.entrySet()) {
            // 2.2 创建一个以项目名为名称的文件夹
            File projectDir = new File(tempDirPath + "/" + entry.getKey());
            if (!projectDir.exists() && !projectDir.mkdirs()) {
                throw new RuntimeException("文件夹[" + projectDir.getAbsolutePath() + "]创建失败");
            }
            // 2.3 为每个项目生成一个word文档
            ProjectData projectData = entry.getValue();
            generateWordDocument(projectData, projectDir);
        }
    }

    /**
     * 生成项目word文档
     *
     * @param projectData 项目信息
     * @param projectDir  临时目录,用于存储多个临时Word文档
     * @throws Exception 异常
     */
    private static void generateWordDocument(ProjectData projectData, File projectDir) throws Exception {
        // 创建一个列表,用于存储多个临时Word文档地址
        List<String> tempWordList = new ArrayList<>();

        tempWordList.add(createWordMLPackage(TEMPLATE_PATH + "kqcs_project/template_1.docx", projectData, projectDir));

        tempWordList.add(createWordMLPackage(TEMPLATE_PATH + "kqcs_project/template_3.docx", projectData, projectDir));

        tempWordList.add(createWordMLPackage(TEMPLATE_PATH + "kqcs_project/template_3.docx", projectData, projectDir));

        mergeDocuments(projectData.getProjectName(), tempWordList, projectDir);

        // 删除临时文件
        tempWordList.forEach(FileUtil::del);
    }

    /**
     * 模拟从数据库中查询数据
     *
     * @return 返回一个模拟数据
     */
    private static Map<String, ProjectData> queryData() {
        Map<String, ProjectData> dataMap = new HashMap<>();
        for (int i = 1; i <= 2; i++) {
            // 创建模拟项目数据
            ProjectData project = new ProjectData();
            project.setProjectName("“友邻有爱”社区养老互助服务项目-" + i);

            dataMap.put(project.getProjectName(), project);
        }
        return dataMap;
    }

    /**
     * 生成word文档
     *
     * @param templatePath word模版路径
     * @param dataObj      数据对象
     * @param tempDirFile  临时目录
     * @return 临时文件路径
     * @throws Docx4JException 异常
     * @throws JAXBException   异常
     * @throws IOException     异常
     */
    private static String createWordMLPackage(String templatePath, Object dataObj, File tempDirFile) throws Docx4JException, JAXBException, IOException {
        WordprocessingMLPackage wordMLPackage = WordprocessingMLPackage.load(new File(templatePath));
        // 填充数据到模板
        Map<String, String> dataMap = objectToMap(dataObj);
        wordMLPackage.getMainDocumentPart().variableReplace(dataMap);
        // 保存新的Word文档
        String templateFileExtension = templatePath.substring(templatePath.lastIndexOf(".") + 1);
        File tempFile = new File(tempDirFile.getAbsolutePath() + "/" + UUID.randomUUID() + "." + templateFileExtension);
        if (!tempFile.exists() && !tempFile.createNewFile()) {
            throw new RuntimeException("文件[" + tempFile.getAbsolutePath() + "]创建失败");
        }
        wordMLPackage.save(tempFile);
        return tempFile.getAbsolutePath();
    }

    /**
     * 将对象转换为Map
     *
     * @param dataObj 对象
     * @return 返回一个Map
     */
    private static Map<String, String> objectToMap(Object dataObj) {
        if (null == dataObj) {
            return MapUtil.empty();
        }
        Map<String, String> dataMap = new HashMap<>();
        // 使用反射获取dataObj的所有字段
        Field[] fields = dataObj.getClass().getDeclaredFields();
        for (Field field : fields) {
            // 设置访问权限,以便可以访问私有字段
            field.setAccessible(true);
            try {
                // 获取字段值
                Object value = field.get(dataObj);
                // 如果字段值是String类型,则添加到dataMap中
                if (value instanceof String) {
                    dataMap.put(field.getName(), (String) value);
                }
            } catch (IllegalAccessException e) {
                // 忽略无法访问的字段
            }
        }
        return dataMap;
    }

    /**
     * 合并文档
     *
     * @param fileName    合并之后的word文档名称
     * @param wordList    word列表
     * @param tempDirFile 临时目录
     * @throws Exception 异常
     */
    public static void mergeDocuments(String fileName, List<String> wordList, File tempDirFile) throws Exception {
        File tempFile = new File(tempDirFile.getAbsolutePath() + "/" + fileName + ".docx");
        if (!tempFile.exists() && !tempFile.createNewFile()) {
            throw new RuntimeException("文件[" + tempFile.getAbsolutePath() + "]创建失败");
        }
        // 保存合并后的文档
        mergeDoc(wordList, tempFile.getAbsolutePath());
    }

    /**
     * 模拟插入附件
     *
     * @param fileList    文件地址列表
     * @param tempDirFile 临时目录
     * @return 返回一个word文档列表地址
     * @throws Exception 异常
     */
    private static List<String> simulateInsertingAttachments(List<String> fileList, File tempDirFile) throws Exception {
        List<String> wordList = new ArrayList<>();

        // 创建一个新的Word文档
        XWPFDocument document = new XWPFDocument();

        // 创建一个段落
        XWPFParagraph paragraph = document.createParagraph();

        for (int i = 0; i < fileList.size(); i++) {
            String filePath = fileList.get(i);
            String fileName = getFileName(filePath);
            InputStream inputStream = downloadFile(filePath);
            String hyperlinkUrl = "./附件/" + fileName;

            FileUtil.writeFromStream(inputStream, new File(tempDirFile + hyperlinkUrl));

            // 超链接地址,可以是网络地址、本地文件相对路径
            XWPFHyperlinkRun hyperlinkRun = paragraph.createHyperlinkRun(hyperlinkUrl);
            hyperlinkRun.setText(fileName);
            hyperlinkRun.setColor("0000FF");
            // 设置下划线
            hyperlinkRun.setUnderline(UnderlinePatterns.SINGLE);

            // 插入一个换行,除了最后一次循环
            if (i < fileList.size() - 1) {
                paragraph.createRun().addBreak(BreakType.TEXT_WRAPPING);
            }
        }

        // 将文档保存到文件
        String tempFilePath = tempDirFile.getAbsolutePath() + "/" + UUID.randomUUID() + ".docx";
        try (FileOutputStream out = new FileOutputStream(tempFilePath)) {
            document.write(out);
        }

        // 关闭文档
        document.close();
        wordList.add(tempFilePath);
        return wordList;
    }

    /**
     * 从文件路径中提取文件名(包括扩展名)
     *
     * @param filePath 文件路径
     * @return 文件名(包括扩展名)
     */
    public static String getFileName(String filePath) {
        if (filePath == null || filePath.isEmpty()) {
            return null;
        }
        // 使用 Paths 来解析文件路径
        return Paths.get(filePath).getFileName().toString();
    }

    /**
     * 下载文件
     *
     * @param filePath 文件路径
     * @return 输入流
     * @throws IOException 异常
     */
    public static InputStream downloadFile(String filePath) throws IOException {
        // 这里应该是下载附件的逻辑,为了模拟效果直接从本地读取
        return Files.newInputStream(Paths.get(filePath));
    }

    /**
     * 下载图片到本地
     *
     * @param imageUrl 图片的URL
     * @param savePath 本地保存路径(包括文件名)
     * @throws IOException 如果发生I/O错误
     */
    public static void downloadImage(String imageUrl, String savePath) throws IOException {
        // 创建URL对象
        URL url = new URL(imageUrl);
        // 打开连接
        HttpURLConnection httpConn = (HttpURLConnection) url.openConnection();
        // 允许输入流(默认为true)
        httpConn.setDoInput(true);
        // 不允许输出流(因为我们只是下载)
        httpConn.setDoOutput(false);
        // 连接服务器
        httpConn.connect();

        // 获取响应码
        int responseCode = httpConn.getResponseCode();
        if (responseCode == HttpURLConnection.HTTP_OK) { // 成功
            // 获取输入流
            try (InputStream inputStream = httpConn.getInputStream();
                 OutputStream outputStream = new FileOutputStream(savePath)) {

                // 创建一个缓冲区
                byte[] buffer = new byte[4096];
                int bytesRead;

                // 读取数据并写入文件
                while ((bytesRead = inputStream.read(buffer)) != -1) {
                    outputStream.write(buffer, 0, bytesRead);
                }
            }
        } else {
            throw new IOException("Failed to download image, HTTP response code: " + responseCode);
        }

        // 断开连接
        httpConn.disconnect();
    }

    /**
     * 合并word文件
     *
     * @param wordList    待合并文件集合
     * @param outFilePath 输出文件路径
     */
    public static void mergeDoc(List<String> wordList, String outFilePath) throws IOException, Docx4JException {
        List<InputStream> streamList = new ArrayList<>();
        for (String wordPath : wordList) {
            streamList.add(Files.newInputStream(Paths.get(wordPath)));
        }
        mergeDocStream(streamList, new FileOutputStream(outFilePath));
    }

    /**
     * 合并word文件
     *
     * @param streamList 文件流列表
     * @param out        输出流
     * @throws Docx4JException 异常
     * @throws IOException     异常
     */
    private static void mergeDocStream(List<InputStream> streamList, FileOutputStream out) throws Docx4JException, IOException {
        // 创建一个空的临时Word文档作为基础
        WordprocessingMLPackage target = WordprocessingMLPackage.createPackage();
        int chunkId = 0;
        // 将streamList中的每个文档插入到target中
        for (InputStream inputStream : streamList) {
            if (inputStream != null) {
                insertDoc(target.getMainDocumentPart(), inputStream, chunkId++);
            }
        }
        target.save(out);
        out.close();

    }

    /**
     * 插入文档
     *
     * @param mainDocumentPart mainDocumentPart
     * @param inputStream      inputStream
     * @param chunkId          chunkId
     * @throws InvalidFormatException 异常
     */
    private static void insertDoc(MainDocumentPart mainDocumentPart, InputStream inputStream, int chunkId) throws InvalidFormatException {
        PartName partName = new PartName("/part" + chunkId + ".docx");
        AlternativeFormatInputPart afiPart = new AlternativeFormatInputPart(partName);
        afiPart.setBinaryData(inputStream);
        Relationship relationship = mainDocumentPart.addTargetPart(afiPart);
        CTAltChunk chunk = Context.getWmlObjectFactory().createCTAltChunk();
        chunk.setId(relationship.getId());
        mainDocumentPart.addObject(chunk);
    }

}

b、控制层

@ApiOperation(value = "详情附件统一导出")
@GetMapping("export")
@ApiImplicitParams({
        @ApiImplicitParam(name = "id", value = "主键"),
        @ApiImplicitParam(name = "type", value = "设立模板类型")
})
public void export(HttpServletResponse response, String id, String type) throws Exception {
    if (StrUtil.isBlank(id)) {
        throw new FlowException("申请人编号不能为空!");
    }
    if (StrUtil.isBlank(type)) {
        throw new FlowException("申请人填报模板类型不能为空!");
    }
    String filePath = kam03Service.export(response, id, type);
    File file = new File(filePath);

    // 检查文件是否存在
    if (!file.exists()) {
        response.sendError(HttpServletResponse.SC_NOT_FOUND, "File not found");
        return;
    }

    // 设置响应头
    String fileName = file.getName();
    String encodedFileName = URLEncoder.encode(fileName, "UTF-8"); 
    response.setContentType("application/zip");
    response.setHeader("Content-Disposition", "attachment; filename*=utf-8''" + encodedFileName);
    System.out.println("file.getName() =================== " + file.getName());
    response.setContentLengthLong(file.length());

    // 获取响应输出流
    try (OutputStream outputStream = response.getOutputStream();
         FileInputStream fileInputStream = new FileInputStream(file)) {

        // 将文件内容写入响应输出流
        byte[] buffer = new byte[4096];
        int bytesRead;
        while ((bytesRead = fileInputStream.read(buffer)) != -1) {
            outputStream.write(buffer, 0, bytesRead);
        }
        outputStream.flush();
    }
}
业务接口层
String export(HttpServletResponse response, String id, String type) throws IOException;

c、业务逻辑层

@Override
public String export(HttpServletResponse response, String id, String type) throws IOException {
    // 1.创建一个临时文件夹
    String tempDirPath = FileUtil.mkParentDirs("temp/" + UUID.randomUUID()).getAbsolutePath();
    // 2.创建一个存储附件的文件夹
    File projectDir = new File(tempDirPath + "/" + "附件");
    if (!projectDir.exists() && !projectDir.mkdirs()) {
        throw new RuntimeException("文件夹[" + projectDir.getAbsolutePath() + "]创建失败");
    }
    // 3.查询出来所有的详情文件信息
    List<Kap17> list;
    //  1.个人捐赠奖
    if (StrUtil.equals(type, ZjcsIConstants.BKAM0108_01)) {
        list = Kap17Mapper.selectList(new LambdaQueryWrapper<Kap17>()
                .eq(Kap17::getBkap1702, id)
                .in(Kap17::getBkap1705, ZjcsIConstants.BUSINESS_FILE_TYPE_LIST_01));
        // 2.机构捐赠奖
    } else if (StrUtil.equals(type, ZjcsIConstants.BKAM0108_02)) {
        list = Kap17Mapper.selectList(new LambdaQueryWrapper<Kap17>()
                .eq(Kap17::getBkap1702, id)
                .in(Kap17::getBkap1705, ZjcsIConstants.BUSINESS_FILE_TYPE_LIST_02));
        // 3.慈善项目、信托奖
    } else if (StrUtil.equals(type, ZjcsIConstants.BKAM0108_03)) {
        list = Kap17Mapper.selectList(new LambdaQueryWrapper<Kap17>()
                .eq(Kap17::getBkap1702, id)
                .in(Kap17::getBkap1705, ZjcsIConstants.BUSINESS_FILE_TYPE_LIST_03));
        // 4.志愿服务奖
    } else if (StrUtil.equals(type, ZjcsIConstants.BKAM0108_04)) {
        list = Kap17Mapper.selectList(new LambdaQueryWrapper<Kap17>()
                .eq(Kap17::getBkap1702, id)
                .in(Kap17::getBkap1705, ZjcsIConstants.BUSINESS_FILE_TYPE_LIST_04));
        // 5.慈善楷模奖
    } else if (StrUtil.equals(type, ZjcsIConstants.BKAM0108_05)) {
        list = Kap17Mapper.selectList(new LambdaQueryWrapper<Kap17>()
                .eq(Kap17::getBkap1702, id)
                .in(Kap17::getBkap1705, ZjcsIConstants.BUSINESS_FILE_TYPE_LIST_05));
        // 6.乡村振兴奖
    } else if (StrUtil.equals(type, ZjcsIConstants.BKAM0108_06)) {
        list = Kap17Mapper.selectList(new LambdaQueryWrapper<Kap17>()
                .eq(Kap17::getBkap1702, id)
                .in(Kap17::getBkap1705, ZjcsIConstants.BUSINESS_FILE_TYPE_LIST_06));
    } else {
        LOGGER.error("设立模板不匹配,请稍后重试!");
        throw new FlowException("设立模板未匹配,请稍后重试!");
    }

    if (CollUtil.isEmpty(list)) {
        LOGGER.error("附件信息为空!");
        throw new FlowException("附件信息为空!");
    }

    // 4.然后将所有的文件下载到本地
    List<String> filePathList = new ArrayList<>();
    for (Kap17 kap17 : list) {
        if (StrUtil.isNotBlank(kap17.getBkap1704()) && StrUtil.isNotBlank(kap17.getBkap1703())) {
            // key : 202501/03/16/1875093026709995520.jpg
            // 现在访问文件有两种方式,不知道之前怎么设计的,需要考虑两种情况,可能是一期的旧数据迁移导致的
            String savePath = GenerateOnWordTemplateKit.FILE_PATH + kap17.getBkap1703();
            if (kap17.getBkap1704().contains(ZjcsIConstants.FILE_URL_REGEX)) {
                String imageUrl =  ZjcsIConstants.URL_22 + kap17.getBkap1704();
                GenerateOnWordTemplateKit.downloadImage(imageUrl, savePath);
            } else {
                String imageUrl =  kap17.getBkap1704().replace("/zjcsmsb/local/", "");
                // oss阿里云存储服务器上下载
                fileReadService.downloadFile(imageUrl, kap17.getBkap1703());
            }

            filePathList.add(savePath);
        }
    }

    // 5.然后将所有本地的文件写入到本地指定文件夹中
    for (String filePathStr : filePathList) {
        String fileName = GenerateOnWordTemplateKit.getFileName(filePathStr);
        InputStream inputStream = GenerateOnWordTemplateKit.downloadFile(filePathStr);
        String hyperlinkUrl = "./" + fileName;
        FileUtil.writeFromStream(inputStream, new File(projectDir + hyperlinkUrl));
    }

    // 6.将本地的文件打包成zip
    File projectZipFile = GenerateOnWordTemplateKit.compressIntoAZipFile(tempDirPath);
    System.out.println("压缩包路径保存:" + projectZipFile.getAbsolutePath());

    // 7. 删除所有临时文件
    GenerateOnWordTemplateKit.deleteAllTempFiles(tempDirPath);

    return projectZipFile.getAbsolutePath();
}

d、阿里云oss文件下载方法(部分)

/*
* @Param fileUrl 文件url
* @Param originalName 原文件名
**/
@Override
public void downloadFile(String fileUrl, String originalName) {
    String key = "local/" + fileUrl;
    File localFile = new File(GenerateOnWordTemplateKit.FILE_PATH, originalName);

    AliOssCloudUtil ossCloudUtil = new AliOssCloudUtil();
    try (OSSObject ossObject = ossCloudUtil.getOssObject(ZjcsIConstants.bucketName, key);
         OutputStream outputStream = new FileOutputStream(localFile);
         InputStream inputstream = ossObject.getObjectContent()) {
        // 使用缓冲流进行拷贝
        byte[] buffer = new byte[1024];
        int bytesRead;
        while ((bytesRead = inputstream.read(buffer)) != -1) {
            outputStream.write(buffer, 0, bytesRead);
        }
        outputStream.flush();
        System.out.println("localFile.getAbsolutePath() ============== " + localFile.getAbsolutePath());
    } catch (IOException e) {
        log.warn("文件下载失败", e);
        throw new RuntimeException(e);
    }
}

3.静态文件

4.前端调用示例

<div style="text-align: right;">
        <ta-button  class="mt-10" @click="openWinExcel" type="primary" icon="to-top">导出附件</ta-button>
</div>
// 导出所有附件信息
openWinExcel() {
  const id = this.primaryKey;
  const type = this.templateId;
  window.open(faceConfig.basePath + '/s2/kam03/export?id=' + id + '&type=' + type,'_blank');
},

5.实现效果


原文地址:https://blog.csdn.net/qq_61026557/article/details/145160191

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!