<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      基于 Word 模板占位符的動態(tài)文檔生成實踐(源碼+保姆版)

      一、基于 Word 模板占位符的動態(tài)文檔生成技術

      ?? 作者:古渡藍按

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

      ?? 簡介

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


      二、添加依賴: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 模板

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

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

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

      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 可以換成自己對應的實體或者需要填寫到數(shù)據(jù)庫的對象
              // 正常邏輯是,這個order 是需要查后臺數(shù)據(jù),然后返回order對象,再在后續(xù)做模板和值 映射,類似下列代碼,
              // 這一步最好放到實現(xiàn)類去寫,這里只是為了方便
              //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層核心實現(xiàn)代碼

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

      @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);
      
                  // 創(chuàng)建表格(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());
      
                  // 第三行:規(guī)格(電壓、電流、數(shù)量)
                  setCellText(table.getRow(2).getCell(0), "規(guī)格");
                  setCellText(table.getRow(2).getCell(1), "電壓:" + order.getVoltage());
                  setCellText(table.getRow(2).getCell(2), "電流:" + order.getCurrent());
                  setCellText(table.getRow(2).getCell(3), "數(shù)量:" + 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. 構建數(shù)據(jù)映射
                      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. 輸出為字節(jié)數(shù)組
                      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、占位符被拆分問題(未能正確顯示數(shù)值)

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

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

      例如下面圖片,編號未能正確顯示

      image-20251029163259173

      ? 問題場景:

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

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

      那么 Word 內部可能存儲為:

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

      而替換邏輯是 Run 處理

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

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

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

      解決方案

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

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

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

        當前邏輯只處理單個 Run,無法處理被拆分的占位符??梢愿挠酶训姆桨福?/p>

        方案 A:合并段落所有文本,整體替換(簡單但會丟失格式,不推薦,會破壞原有樣式)

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

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

      • 次要可能原因排查

        • ? 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 文檔
        • 會根據(jù)文件內容的“真實類型”(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. 確認生成的字節(jié)數(shù)組確實是合法 .docx

        • docBytes 保存到本地文件:Files.write(Paths.get("test.docx"), docBytes);
        • 用 Word 能正常打開嗎?如果打不開 → 說明生成邏輯有誤(不是 ZIP 問題,是文件損壞)
      2. 不要用 application/zipapplication/octet-stream
        即使內容是 ZIP 結構,也必須聲明為 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 的字節(jié)范圍),無法表示中文字符(Unicode 超出 255),于是拋出異常。

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

      HTTP 協(xié)議規(guī)定:響應頭中的非 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();
      
      

      六、接口驗證

      可以訪問接口:http://127.0.0.1:8199/api/generate-word2?no=27202SCRW250006

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

      image-20251029164051702

      posted @ 2025-10-29 17:27  古渡藍按  閱讀(243)  評論(0)    收藏  舉報
      主站蜘蛛池模板: 亚洲精品中文字幕二区| 青青草无码免费一二三区| 久久夜色撩人精品国产av| 国产香蕉九九久久精品免费| 国产免费毛卡片| 免费人成再在线观看视频| 中国女人内谢69xxxx| 国产亚洲色视频在线| 国产乱理伦片在线观看| 国产99青青成人A在线| 国产精品疯狂输出jk草莓视频| 国产精品美腿一区在线看| 亚洲精品一二三区在线看| 中国美女a级毛片| 性人久久久久| 99热久久这里只有精品| 午夜福利精品国产二区| 色欲久久久天天天综合网精品| 国产精品视频一区二区噜噜| 激情啪啪啪一区二区三区| 在线中文一区字幕对白| 91人妻熟妇在线视频| 亚洲精品国产无套在线观| 18禁无遮挡啪啪无码网站| 成人免费看片又大又黄| 青青国产揄拍视频| 午夜福利理论片高清在线| 国产首页一区二区不卡| 亚洲精品国精品久久99热| 自拍偷拍第一区二区三区| 白白发布视频一区二区视频| 最好看的中文字幕国语| 国产亚洲精品午夜福利| 国产三级黄色的在线观看| 日韩深夜视频在线观看| 日韩人妻无码精品无码中文字幕 | 国产福利社区一区二区| 亚洲一卡2卡三卡四卡精品| 丰满人妻熟妇乱又伦精品劲| 色悠久久网国产精品99| 国产偷自视频区视频|