中文字幕精品亚洲无线码二区,国产黄a三级三级三级看三级,亚洲七七久久桃花影院,丰满少妇被猛烈进入,国产小视频在线观看网站

基于 Word 模板(ban)占位符的動(dong)態(tai)文檔生成實踐(源碼(ma)+保姆版(ban))

一、基于 Word 模板占位符的動態文檔生成技術

?? 作者:古渡藍按

個人微信公眾號:微信公眾號(深入淺出談java)
感覺本篇對(dui)你(ni)有幫助(zhu)可以關注一下(xia),會不定期更新知識和(he)面試資(zi)料(liao)、技巧!!!

?? 簡介

在企業業務系統中,合同、工單、報告等 Word 文檔往往格式固定但內容動態。傳統硬編碼方式開發效率低、維護成本高。
本文介紹一種高效、靈活的解決方案:通過預定義 Word 模板中的 ${KEY} 占位符,結合后端數據自動填充生成最終文檔。該方法(fa)實(shi)現邏輯清晰(xi)、模板可(ke)(ke)由(you)非技術人員維護,顯著提(ti)升開發(fa)效率與系(xi)統可(ke)(ke)擴展性。以下是代碼實(shi)現步驟(zou)和(he)邏輯。


二、添加依賴:Apache POI

<!-- Apache POI for Word -->
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-ooxml</artifactId>
            <version>5.2.4</version>
        </dependency>

        <!-- Optional: For logging -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

三、制作代占位符的word 模板

打開需要生產的數據模板,在對應位置填寫占位符,類似下圖:占位符格式為:${XXXXX}

?? 注:占位符里面的必須和代碼中的key 值一樣

制作完成后,放到 src/main/resources/templates/ 目錄下(xia)作為模板文件:

src/main/resources/templates/production_order_template.docx

圖片示例

image-20251029155045954


四、編寫核心邏輯

Controller 代碼

@Slf4j
@RestController
public class ProductionOrderController {



    @Resource
    private WordGeneratorService productionOrderService;

    @GetMapping("/api/generate-word")
    public void generateWord(@RequestParam Long id, HttpServletResponse response) throws IOException {

        ProductionOrder order = new ProductionOrder();

        byte[] docBytes = productionOrderService.generateProductionOrderDoc(order);

        // 設置正確的 Content-Type
        response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
        response.setContentLength(docBytes.length);

        // ? 安全設置帶中文的文件名(關鍵!)
        String filename = "生產任務單_" + id + ".docx";
        String encodedFilename = URLEncoder.encode(filename, StandardCharsets.UTF_8).replace("+", "%20");

        // 使用 filename* 語法(RFC 5987):支持 UTF-8 文件名
        response.setHeader("Content-Disposition",
                "attachment; filename=\"" + encodedFilename + "\"; filename*=UTF-8''" + encodedFilename);

        // 寫入響應體
        response.getOutputStream().write(docBytes);
        response.getOutputStream().flush();
    }

    @GetMapping("/api/generate-word2")
    public void generateWord2(@RequestParam String no, HttpServletResponse response) throws IOException {
        // 這里的ProductionOrder 可以換成自己對應的實體或者需要填寫到數據庫的對象
        // 正常邏輯是,這個order 是需要查后臺數據,然后返回order對象,再在后續做模板和值 映射,類似下列代碼,
        // 這一步最好放到實現類去寫,這里只是為了方便
        //TODO:List<ProductionOrder> getProductDataList = this.list(
        //        new LambdaQueryWrapper<ProductionOrder>()
        //                .eq(ProductionOrder::getNo, no));
        ProductionOrder order = new ProductionOrder();

        // 改用模板生成
        byte[] docBytes = productionOrderService.generateFromTemplate(order);


        // 設置正確的 Content-Type
        response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
        response.setContentLength(docBytes.length);

        // ? 安全設置帶中文的文件名(關鍵!)
        String filename = "生產任務單_" + no + ".docx";
        String encodedFilename = URLEncoder.encode(filename, StandardCharsets.UTF_8).replace("+", "%20");

        // 使用 filename* 語法(RFC 5987):支持 UTF-8 文件名
        response.setHeader("Content-Disposition",
                "attachment; filename=\"" + encodedFilename + "\"; filename*=UTF-8''" + encodedFilename);

        // 寫入響應體
        response.getOutputStream().write(docBytes);
        response.getOutputStream().flush();
    }
}

Service層核心實現代碼

?? 注:這里就省去了接口層(需要可以自己加),直接放置的核心方法

@Service
public class WordGeneratorService {

    public byte[] generateProductionOrderDoc(ProductionOrder order) {
        try (XWPFDocument document = new XWPFDocument()) {
            // 標題
            XWPFParagraph titlePara = document.createParagraph();
            titlePara.setAlignment(ParagraphAlignment.CENTER);
            XWPFRun titleRun = titlePara.createRun();
            titleRun.setText("生產任務單申請表");
            titleRun.setFontSize(16);
            titleRun.setBold(true);

            // 創建表格(20列模擬原表寬度,實際按內容合并)
            XWPFTable table = document.createTable(5, 4);
            table.setWidth("100%");

            // 第一行:客戶單位 & 訂單號
            setCellText(table.getRow(0).getCell(0), "客戶單位:");
            setCellText(table.getRow(0).getCell(1), order.getCustomer());
            setCellText(table.getRow(0).getCell(2), "訂單號/合同編號:");
            setCellText(table.getRow(0).getCell(3), order.getOrderNo());

            // 第二行:產品名稱 & 型號
            setCellText(table.getRow(1).getCell(0), "產品名稱:");
            setCellText(table.getRow(1).getCell(1), order.getProductName());
            setCellText(table.getRow(1).getCell(2), "產品型號:");
            setCellText(table.getRow(1).getCell(3), order.getModel());

            // 第三行:規格(電壓、電流、數量)
            setCellText(table.getRow(2).getCell(0), "規格");
            setCellText(table.getRow(2).getCell(1), "電壓:" + order.getVoltage());
            setCellText(table.getRow(2).getCell(2), "電流:" + order.getCurrent());
            setCellText(table.getRow(2).getCell(3), "數量:" + order.getQuantity());

            // 第四行:生產周期
            setCellText(table.getRow(3).getCell(0), "生產周期");
            setCellText(table.getRow(3).getCell(1), "計劃出貨日期:" + order.getPlannedShipDate());
            setCellText(table.getRow(3).getCell(2), "銷售項目人:");
            setCellText(table.getRow(3).getCell(3), order.getSalesPerson());

            // 第五行:備注或其他
            setCellText(table.getRow(4).getCell(0), "其他要求:");
            table.getRow(4).getCell(1).getParagraphs().get(0);

            // 合并單元格(可選,簡化處理)
            // 實際復雜表格建議用模板或 Apache POI 高級合并

            ByteArrayOutputStream out = new ByteArrayOutputStream();
            document.write(out);
            return out.toByteArray();
        } catch (Exception e) {
            throw new RuntimeException("生成 Word 失敗", e);
        }
    }

    private void setCellText(XWPFTableCell cell, String text) {
        cell.setText(text);
        // 可選:設置字體
        for (XWPFParagraph p : cell.getParagraphs()) {
            for (XWPFRun r : p.getRuns()) {
                r.setFontFamily("宋體");
                r.setFontSize(10);
            }
        }
    }


    //方式二

    private static final String TEMPLATE_PATH = "templates/production_order_template.docx";

    public byte[] generateFromTemplate(ProductionOrder order) {
        try {
            // 1. 加載模板
            ClassPathResource resource = new ClassPathResource(TEMPLATE_PATH);
            try (InputStream is = resource.getInputStream();
                 ByteArrayOutputStream out = new ByteArrayOutputStream()) {

                XWPFDocument document = new XWPFDocument(is);

                // 2. 構建數據映射
                Map<String, String> data = new HashMap<>();
                data.put("customer", safeStr(order.getCustomer()));
                data.put("orderNo", safeStr(order.getOrderNo()));
                data.put("workOrderNo", safeStr(order.getWorkOrderNo()));
                data.put("productName", safeStr(order.getProductName()));
                data.put("model", safeStr(order.getModel()));
                data.put("voltage", safeStr(order.getVoltage()));
                data.put("current", safeStr(order.getCurrent()));
                data.put("quantity", safeStr(order.getQuantity() != null ? order.getQuantity().toString() : ""));
                data.put("plannedShipDate", safeStr(order.getPlannedShipDate()));
                data.put("salesPerson", safeStr(order.getSalesPerson()));
                
                
                //如果你希望某些字段只顯示“√”表示選中,可以在 Java 中這樣處理:
                data.put("hasEmbeddedSeal", order.isEmbeddedSeal() ? "√" : "");


                

                // 3. 替換所有段落中的占位符
                replaceInParagraphs(document.getParagraphs(), data);

                // 4. 替換表格中的占位符
                for (XWPFTable table : document.getTables()) {
                    for (XWPFTableRow row : table.getRows()) {
                        for (XWPFTableCell cell : row.getTableCells()) {
                            replaceInParagraphs(cell.getParagraphs(), data);
                        }
                    }
                }

                // 5. 輸出為字節數組
                document.write(out);
                return out.toByteArray();
            }
        } catch (Exception e) {
            throw new RuntimeException("生成 Word 文檔失敗", e);
        }
    }

    /**
     * 替換段落中的占位符
     */
    private void replaceInParagraphs(List<XWPFParagraph> paragraphs, Map<String, String> data) {
        for (XWPFParagraph para : paragraphs) {
            for (XWPFRun run : para.getRuns()) {
                if (run != null && run.getText(0) != null) {
                    String text = run.getText(0);
                    String replaced = replacePlaceholders(text, data);
                    if (!text.equals(replaced)) {
                        run.setText(replaced, 0);
                    }
                }
            }
        }
    }

    /**
     * 使用正則替換 ${key} 為 value
     */
    private String replacePlaceholders(String text, Map<String, String> data) {
        Pattern pattern = Pattern.compile("\\$\\{([^}]+)\\}");
        Matcher matcher = pattern.matcher(text);
        StringBuffer sb = new StringBuffer();
        while (matcher.find()) {
            String key = matcher.group(1);
            String replacement = data.getOrDefault(key, matcher.group(0)); // 未找到則保留原樣
            matcher.appendReplacement(sb, replacement == null ? "" : Matcher.quoteReplacement(replacement));
        }
        matcher.appendTail(sb);
        return sb.toString();
    }

    private String safeStr(String str) {
        return str == null ? "" : str;
    }
}

五、注意事項

?? 1、占位符被拆分問題(未能正確顯示數值)

Word 會因格式變化將 ${NO} 拆成多個 Run(如 ${N + O}),導致(zhi)無法匹(pi)配。這里不要用文本框(kuang)或(huo)藝術字(zi)等

Apache POI 在讀取 Word 文檔時,會將文本按格式(字體、顏色、加粗等)拆分成多個 XWPFRun 對象。

例如(ru)下(xia)面(mian)圖片,編號未能(neng)正確顯示(shi)

image-20251029163259173

? 問題場景:

如果在 Word 中輸入 ${NO} 時:

  • 中間不小心按了方向鍵、空格、Backspace
  • 或對部分字符設置了格式(比如只加粗了 N
  • 或從其他地方復制粘貼過來

那么(me) Word 內部(bu)可能存儲(chu)為:

Run1: "${N"
Run2: "O}"

而替換邏輯是 Run 處理

for (XWPFRun run : para.getRuns()) {
    String text = run.getText(0); // 只拿到 "${N" 或 "O}"
    // 無法匹配完整 "${NO}"
}

結果:${NO} 沒有被識別,也就不會被替換!

而其他占位符(如 ${SJBBH})可能是一次性輸入的,所以在一個 Run 里,能正常替換。

解決方案

  • 在模板中一次性輸入完整占位符,避免中途格式調整。(不要中途按方向鍵、不要設置局部格式)

    ?? 技巧:可以先輸入 ABC,確認它在一個 Run 里(比如全選后統一加粗),再替換成 ${NO}

  • 或使用更高級的跨 Run 合并替換算法(實現復雜)。

    當前邏(luo)輯只處(chu)(chu)理(li)(li)單個 Run,無法(fa)處(chu)(chu)理(li)(li)被拆(chai)分的占位符。可(ke)以改(gai)用(yong)更健壯的方案:

    方案 A:合并(bing)段(duan)落所(suo)有文本(ben),整(zheng)體替換(簡單但會丟失(shi)格式,不(bu)推薦(jian),會破(po)壞(huai)原有樣式)

    方案 B:使用遞歸或緩沖區拼接 Run(復雜),但對大多數項目來說,方法 1(規范模板輸入)是最高效、最可靠的

?? 調試技巧:如果替換失敗,可臨時打印 run.getText(0) 查看(kan)實際文本分段。

  • 次要可能原因排查

    • ? 1. 檢查 Java 實體類字段是否正確

    • 2. 檢查 Word 模板中是否真的是 ${NO}(大小寫敏感)

    • 檢查是否在表格 or 段落中?


??2、使用方式一,返回的是zip文件而不是word 文件

核心原因:

.docx 文件本質上就是一個 ZIP 壓縮包

  • Microsoft Office 2007 及以后的 .docx.xlsx.pptx 文件都采用 Open XML 格式
  • 這種格式實際上是將 XML、圖片、樣式等文件打包成一個 ZIP 壓縮包,只是擴展名改成了 .docx
  • 當用代碼生成.docx但沒有正確設置 HTTP 響應頭(Content-Type 和 Content-Disposition)
    • 瀏覽器無法識別這是 Word 文檔
    • 會根據文件內容的“真實類型”(ZIP)來處理
    • 于是自動下載為 .zip 文件,或提示“文件損壞”

解決方案

  • 設置正確的響應頭
HttpHeaders headers = new HttpHeaders();

// 1. 設置 Content-Type(MIME 類型)
headers.setContentType(MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.wordprocessingml.document"));

// 2. 設置 Content-Disposition(告訴瀏覽器這是附件,且文件名是 .docx)
headers.setContentDispositionFormData("attachment", "生產任務單.docx");

return new ResponseEntity<>(docBytes, headers, HttpStatus.OK);

? 常見錯誤寫法(會導致 ZIP 下載):

// 錯誤1:Content-Type 寫成 application/zip 或 application/octet-stream
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM); // ?

// 錯誤2:文件名沒有 .docx 后綴
headers.setContentDispositionFormData("attachment", "report"); // ? 下載為 report.zip

// 錯誤3:文件名包含非法字符(如 / \ : * ? " < > |)
headers.setContentDispositionFormData("attachment", "生產/任務單.docx"); // ? 可能被截斷或變 ZIP

?? 額外檢查點:

  1. 確認生成的字節數組確實是合法 .docx

    • docBytes 保存到本地文件:Files.write(Paths.get("test.docx"), docBytes);
    • 用 Word 能正常打開嗎?如果打不開 → 說明生成邏輯有誤(不是 ZIP 問題,是文件損壞)
  2. 不要用 application/zipapplication/octet-stream
    即使內容是 ZIP 結(jie)構,也必須聲明為(wei) Word 的 MIME 類型!


??3、使用瀏覽器直接請求報錯

報錯示例:

java.lang.IllegalArgumentException: The Unicode character [生] at code point [29,983] cannot be encoded as it is outside the permitted range of 0 to 255

根本原因
在設置 HTTP 響應頭(特別是 Content-Disposition 文件名)時,直接使用了包含中文字符(如“生產任務單.docx”)的字符串,而 Tomcat 在處理 HTTP 響應頭時,默認使用 ISO-8859-1 編碼(只支持 0–255 的(de)字(zi)節范圍),無(wu)法表(biao)示中文字(zi)符(fu)(Unicode 超出 255),于是拋出異常(chang)。

? 正確解決方案:對文件名進行 RFC 5987 / RFC 2231 兼容的編碼

HTTP 協議規定:響應頭中的非 ASCII 字符必須進行編碼。推薦使用 filename\* 語法(帶編碼聲明)

// 設置正確的 Content-Type
    response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
    response.setContentLength(docBytes.length);

    // ? 安全設置帶中文的文件名(關鍵!)
    String filename = "生產任務單_" + id + ".docx";
    String encodedFilename = URLEncoder.encode(filename, StandardCharsets.UTF_8).replace("+", "%20");

    // 使用 filename* 語法(RFC 5987):支持 UTF-8 文件名
    response.setHeader("Content-Disposition",
        "attachment; filename=\"" + encodedFilename + "\"; filename*=UTF-8''" + encodedFilename);

    // 寫入響應體
    response.getOutputStream().write(docBytes);
    response.getOutputStream().flush();


六、接口驗證

可以訪問接口:

這樣你的瀏覽器就會彈出下載頁面,并且獲(huo)取一個填充數據的word 文檔

image-20251029164051702

posted @ 2025-10-29 17:27  古渡藍按  閱讀(183)  評論(0)    收藏  舉報