Java Spring Boot根据Word模板和动态数据生成Word文件

XML 复制代码
  <!-- 添加 Apache POI 相关的依赖库,用于处理Word文档 -->
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-ooxml</artifactId>
            <version>5.2.3</version>
        </dependency>
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-ooxml-schemas</artifactId>
            <version>4.1.2</version>
        </dependency>
复制代码
WordController
java 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;

@RestController
public class WordController {

    @Autowired
    private WordService wordService;

    @PostMapping("/generateWord")
    public String generateWord(HttpServletResponse response, @RequestBody Map<String, Object> requestData) {
        String templatePath = "E:\\word\\定标评审文件.docx"; // 模板文件路径
        try {
            wordService.fillTemplate(response,templatePath, requestData);
            return "Word文档生成成功!";
        } catch (IOException e) {
            return "生成文档时发生错误!" + e.getMessage();
        }
    }
}
复制代码
WordService
java 复制代码
import lombok.Cleanup;
import org.apache.poi.xwpf.usermodel.*;
import org.springframework.stereotype.Service;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@Service
public class WordService {

    /**
     * 填充Word模板,支持简单变量替换和列表循环
     *
     * @param templatePath 模板路径
     * @param data         数据
     * @throws IOException IO异常
     */
    public void fillTemplate(HttpServletResponse response, String templatePath, Map<String, Object> data) throws IOException {
        try (XWPFDocument document = new XWPFDocument(new FileInputStream(templatePath))) {
            processTableLoops(document, data);

            for (XWPFParagraph paragraph : document.getParagraphs()) {
                replaceInParagraph(paragraph, data);
            }
            response.setHeader("Content-disposition", "attachment; filename=" + URLEncoder.encode("testout.docx", "UTF-8"));
            response.setHeader("content-type", "application/octet-stream");
            response.setCharacterEncoding("UTF-8");
            @Cleanup ServletOutputStream outputStream = null;
            try {
                outputStream = response.getOutputStream();
                document.write(outputStream);
            } catch (IOException e) {
                e.printStackTrace();
                throw e;
            } finally {
                document.close();
                if (outputStream != null) {
                    outputStream.close();
                }
            }
        }
    }

    /**
     * 在段落中替换简单变量
     */
    private void replaceInParagraph(XWPFParagraph paragraph, Map<String, Object> data) {
        // 获取段落完整文本
        String fullText = paragraph.getText();
        if (fullText == null || fullText.isEmpty()) {
            return;
        }

        // 检查是否包含任何占位符,避免不必要的处理
        boolean hasPlaceholder = false;
        for (String key : data.keySet()) {
            if (fullText.contains("{{" + key + "}}")) {
                hasPlaceholder = true;
                break;
            }
        }

        if (!hasPlaceholder) {
            return;
        }
        String newText = fullText;
        for (Map.Entry<String, Object> entry : data.entrySet()) {
            String key = entry.getKey();
            Object value = entry.getValue();
            // 跳过列表类型,列表已在表格处理中解决
            if (value instanceof List) {
                continue;
            }
            List<XWPFRun> runs = paragraph.getRuns();
            for (XWPFRun run : runs) {
                String text = run.getText(0);
                if("{{".equals(text)||"}}".equals(text)){
                    run.setText("", 0);
                }
                String placeholder =  key;
                System.out.println(text);
                if (text != null && text.contains(placeholder)) { // 替换文本条件
                    String newText2 = text.replace(placeholder,  value != null ? value.toString(): ""); // 替换文本
                    run.setText(newText2, 0);
                }
            }
        }
    }


    /**
     * 处理表格中的列表循环
     * 约定:模板表格中,包含 {{listKey.field}} 格式占位符的行将被视为模板行
     * 例如:userlist.userName -> 对应 data 中的 "userlist" 列表和字段 "userName"
     */
    private void processTableLoops(XWPFDocument document, Map<String, Object> data) {
        for (XWPFTable table : document.getTables()) {
            // 从下往上遍历行,避免删除行后索引错位
            for (int i = table.getNumberOfRows() - 1; i >= 0; i--) {
                XWPFTableRow row = table.getRow(i);

                // 检查该行是否包含列表占位符,例如 {{userlist.userName}}
                String rowText =getRowText(row);
                if (rowText == null || !rowText.contains("{{")) {
                    continue;
                }

                // 解析占位符,提取 listKey (如 userlist) 和 field (如 userName)
                // 这里简化处理:假设一行中所有的列表占位符都属于同一个 listKey
                String listKey = extractListKeyFromRow(row);

                if (listKey != null && data.containsKey(listKey)) {
                    Object listObj = data.get(listKey);
                    if (listObj instanceof List) {
                        List<?> itemList = (List<?>) listObj;
                        // 1. 为列表中的每个元素创建新行
                        for (Object item : itemList) {
                            if (item instanceof Map) {
                                Map<String, Object> itemData = (Map<String, Object>) item;
                                XWPFTableRow newRow = copyTableRow(table, row);
                                replaceInTableRow(newRow, itemData, listKey);
                            }
                        }

                        // 2. 删除原始的模板行
                        table.removeRow(i);
                    }
                }
            }
        }
    }

    private String getRowText(XWPFTableRow row) {
        StringBuilder sb = new StringBuilder();
        for (XWPFTableCell cell : row.getTableCells()) {
            for (XWPFParagraph paragraph : cell.getParagraphs()) {
                String text = paragraph.getText();
                if (text != null) {
                    sb.append(text);
                }
            }
        }
        return sb.toString();
    }

    private String extractListKeyFromRow(XWPFTableRow row) {
        String text = getRowText(row);
        // 简单正则匹配 {{key.field}}
        Pattern pattern = Pattern.compile("\\{\\{(\\w+)\\.\\w+\\}\\}");
        Matcher matcher = pattern.matcher(text);
        if (matcher.find()) {
            return matcher.group(1);
        }
        return null;
    }

    /**
     * 复制表格行
     */
    private XWPFTableRow copyTableRow(XWPFTable table, XWPFTableRow sourceRow) {
        // 创建新行
        XWPFTableRow newRow = table.createRow();

        // 复制单元格
        for (int i = 0; i < sourceRow.getTableCells().size(); i++) {
            XWPFTableCell sourceCell = sourceRow.getCell(i);
            XWPFTableCell newCell = newRow.getCell(i);
            if (newCell == null) {
                newCell = newRow.addNewTableCell();
            }
            // 复制单元格内容
            copyCellContent(sourceCell, newCell);
            // 复制单元格宽度等属性(可选)
            newCell.getCTTc().setTcPr(sourceCell.getCTTc().getTcPr());
        }
        return newRow;
    }

    /**
     * 复制单元格内容
     */
    private void copyCellContent(XWPFTableCell source, XWPFTableCell target) {
        // 清除目标单元格内容
        target.removeParagraph(0); // 至少有一个默认段落

        for (XWPFParagraph sp : source.getParagraphs()) {
            XWPFParagraph tp = target.addParagraph();
            // 复制段落属性
            tp.getCTP().setPPr(sp.getCTP().getPPr());

            for (XWPFRun sr : sp.getRuns()) {
                XWPFRun tr = tp.createRun();
                tr.getCTR().setRPr(sr.getCTR().getRPr()); // 复制样式
                tr.setText(sr.getText(0));
            }
        }
    }

    /**
     * 在表格行中替换变量
     * @param rowData 单个对象的数据
     * @param listKey 列表的键名,用于去除占位符前缀
     */
    private void replaceInTableRow(XWPFTableRow row, Map<String, Object> rowData, String listKey) {
        for (XWPFTableCell cell : row.getTableCells()) {
            for (XWPFParagraph paragraph : cell.getParagraphs()) {
                String fullText = paragraph.getText();
                if (fullText == null) continue;

                String newText = fullText;
                boolean changed = false;

                for (Map.Entry<String, Object> entry : rowData.entrySet()) {
                    String placeholder = "{{" + listKey + "." + entry.getKey() + "}}";
                    if (newText.contains(placeholder)) {
                        newText = newText.replace(placeholder, entry.getValue() != null ? entry.getValue().toString() : "");
                        changed = true;
                    }
                }

                if (changed) {
                    int runsSize = paragraph.getRuns().size();
                    for (int i = runsSize - 1; i >= 0; i--) {
                        paragraph.removeRun(i);
                    }
                    XWPFRun newRun = paragraph.createRun();
                    newRun.setText(newText);
                }
            }
        }
    }
}

请求JSON

java 复制代码
{
  "rfqName": "测算定标报告",
  "purCompanyName": "公司名称",
  "rfqType": "定标采购",
  "count": "3",
  "bidVendor": "***有限公司",
  "bidValue": "22.442",
  "list1": [
    {
      "candidateName": "aaa",
      "sort": 1
    },
    {
      "candidateName": "bbb",
      "sort": 2
    }
  ],
  "list2": [
    {
      "bidDer": "ccc",
      "bidAmount": 33
    },
    {
      "bidDer": "ddd",
      "bidAmount": 44
    }
  ]
}

返回文件流前端下载

javascript 复制代码
fetch('请求地址', {
    method: 'GET'
})
.then(response => response.blob()) // 获取blob链接
.then(blob => {
    const url = window.URL.createObjectURL(blob); // 创建一个临时的URL
    const a = document.createElement('a'); // 创建一个<a>元素
    a.style.display = 'none'; // 隐藏该元素
    a.href = url; // 设置href属性为目标URL
    a.download = '定标评审文件.docx'; // 设置下载后的文件名
    document.body.appendChild(a); // 将其添加到DOM中
    a.click(); // 触发点击事件开始下载
    window.URL.revokeObjectURL(url); // 释放之前创建的URL对象
    document.body.removeChild(a); // 下载完成后可以从DOM中移除该元素
})
.catch(error => console.error('文件下载失败:', error));

在Word中使用

相关推荐
逸Y 仙X1 小时前
文章二十八:ElasticSearch 运用指标聚合快速统计数值
java·大数据·elasticsearch·搜索引擎·全文检索
霸道流氓气质1 小时前
SpringBoot+LangChain4j+Ollama+MCP实现智能天气工具调用示例
java·spring boot·后端
sindyra1 小时前
享元模式(Flyweight Pattern)
java·开发语言·设计模式·享元模式·优缺点
这是程序猿1 小时前
设计模式入门:Java 单例模式(Singleton)详解,从入门到实战
java·单例模式·设计模式
codingPower1 小时前
ApplicationListener 和 SpringApplicationRunListener 深度解析对比
java·开发语言·spring boot
ch.ju1 小时前
Java Programming Chapter 2-Recursion of function
java·开发语言
铁皮哥1 小时前
【后端开发】RabbitMQ、RocketMQ、Kafka 怎么选?我从业务场景重新梳理了一遍
java·linux·数据库·分布式·kafka·rabbitmq·rocketmq
AC赳赳老秦1 小时前
数据库操作自动化:用 OpenClaw 对接 Navicat/DBeaver,实现数据备份、脱敏、日常操作自动化
java·运维·数据库·python·oracle·自动化·openclaw