農(nóng)業(yè)網(wǎng)站素材中鐵建設(shè)集團有限公司下屬公司
鶴壁市浩天電氣有限公司
2026/01/24 12:29:21
農(nóng)業(yè)網(wǎng)站素材,中鐵建設(shè)集團有限公司下屬公司,萊蕪要出大事,青海省交通建設(shè)廳網(wǎng)站首頁文介紹一種高效、靈活的解決方案#xff1a;通過預(yù)定義 Word 模板中的 ${KEY} 占位符#xff0c;結(jié)合后端數(shù)據(jù)自動填充生成最終文檔。該方法實現(xiàn)邏輯清晰、模板可由非技術(shù)人員維護#xff0c;顯著提升開發(fā)效率與系統(tǒng)可擴展性。以下是代碼實現(xiàn)步驟和邏輯。二、添加依賴:Apach…文介紹一種高效、靈活的解決方案通過預(yù)定義 Word 模板中的 ${KEY} 占位符結(jié)合后端數(shù)據(jù)自動填充生成最終文檔。該方法實現(xiàn)邏輯清晰、模板可由非技術(shù)人員維護顯著提升開發(fā)效率與系統(tǒng)可擴展性。以下是代碼實現(xiàn)步驟和邏輯。二、添加依賴:Apache POI!-- Apache POI for Word --dependencygroupIdorg.apache.poi/groupIdartifactIdpoi-ooxml/artifactIdversion5.2.4/version/dependency!-- Optional: For logging --dependencygroupIdorg.projectlombok/groupIdartifactIdlombok/artifactIdoptionaltrue/optional/dependency三、制作代占位符的word 模板打開需要生產(chǎn)的數(shù)據(jù)模板在對應(yīng)位置填寫占位符類似下圖占位符格式為${XXXXX} 注占位符里面的必須和代碼中的key 值一樣制作完成后放到 src/main/resources/templates/ 目錄下作為模板文件src/main/resources/templates/production_order_template.docx圖片示例image-20251029155045954四、編寫核心邏輯Controller 代碼Slf4jRestControllerpublic class ProductionOrderController {Resourceprivate 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);// 設(shè)置正確的 Content-Typeresponse.setContentType(application/vnd.openxmlformats-officedocument.wordprocessingml.document);response.setContentLength(docBytes.length);// ? 安全設(shè)置帶中文的文件名關(guān)鍵String filename 生產(chǎn)任務(wù)單_ 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);// 寫入響應(yīng)體response.getOutputStream().write(docBytes);response.getOutputStream().flush();}GetMapping(/api/generate-word2)public void generateWord2(RequestParam String no, HttpServletResponse response) throws IOException {// 這里的ProductionOrder 可以換成自己對應(yīng)的實體或者需要填寫到數(shù)據(jù)庫的對象// 正常邏輯是這個order 是需要查后臺數(shù)據(jù)然后返回order對象再在后續(xù)做模板和值 映射,類似下列代碼// 這一步最好放到實現(xiàn)類去寫這里只是為了方便//TODO:ListProductionOrder getProductDataList this.list(// new LambdaQueryWrapperProductionOrder()// .eq(ProductionOrder::getNo, no));ProductionOrder order new ProductionOrder();// 改用模板生成byte[] docBytes productionOrderService.generateFromTemplate(order);// 設(shè)置正確的 Content-Typeresponse.setContentType(application/vnd.openxmlformats-officedocument.wordprocessingml.document);response.setContentLength(docBytes.length);// ? 安全設(shè)置帶中文的文件名關(guān)鍵String filename 生產(chǎn)任務(wù)單_ 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);// 寫入響應(yīng)體response.getOutputStream().write(docBytes);response.getOutputStream().flush();}}Service層核心實現(xiàn)代碼 注這里就省去了接口層需要可以自己加直接放置的核心方法Servicepublic 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(生產(chǎn)任務(wù)單申請表);titleRun.setFontSize(16);titleRun.setBold(true);// 創(chuàng)建表格20列模擬原表寬度實際按內(nèi)容合并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());// 第二行產(chǎn)品名稱 型號setCellText(table.getRow(1).getCell(0), 產(chǎn)品名稱);setCellText(table.getRow(1).getCell(1), order.getProductName());setCellText(table.getRow(1).getCell(2), 產(chǎn)品型號);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());// 第四行生產(chǎn)周期setCellText(table.getRow(3).getCell(0), 生產(chǎn)周期);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);// 合并單元格可選簡化處理// 實際復(fù)雜表格建議用模板或 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);// 可選設(shè)置字體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. 構(gòu)建數(shù)據(jù)映射MapString, 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(ListXWPFParagraph paragraphs, MapString, 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, MapString, 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}導(dǎo)致無法匹配。這里不要用文本框或藝術(shù)字等Apache POI 在讀取 Word 文檔時會將文本按格式字體、顏色、加粗等拆分成多個 XWPFRun 對象。例如下面圖片編號未能正確顯示image-20251029163259173? 問題場景如果在 Word 中輸入 ${NO} 時中間不小心按了方向鍵、空格、Backspace或?qū)Σ糠肿址O(shè)置了格式比如只加粗了 N或從其他地方復(fù)制粘貼過來那么 Word 內(nèi)部可能存儲為Run1: ${NRun2: O}而替換邏輯是 逐 Run 處理for (XWPFRun run : para.getRuns()) {String text run.getText(0); // 只拿到 ${N 或 O}// 無法匹配完整 ${NO}}→ 結(jié)果${NO} 沒有被識別也就不會被替換而其他占位符如 ${SJBBH}可能是一次性輸入的所以在一個 Run 里能正常替換。解決方案在模板中一次性輸入完整占位符避免中途格式調(diào)整。不要中途按方向鍵、不要設(shè)置局部格式 技巧可以先輸入 ABC確認它在一個 Run 里比如全選后統(tǒng)一加粗再替換成 ${NO}。或使用更高級的跨 Run 合并替換算法實現(xiàn)復(fù)雜。當前邏輯只處理單個 Run無法處理被拆分的占位符。可以改用更健壯的方案方案 A合并段落所有文本整體替換簡單但會丟失格式不推薦會破壞原有樣式方案 B使用遞歸或緩沖區(qū)拼接 Run復(fù)雜但對大多數(shù)項目來說方法 1規(guī)范模板輸入是最高效、最可靠的。 調(diào)試技巧如果替換失敗可臨時打印 run.getText(0) 查看實際文本分段。次要可能原因排查? 1. 檢查 Java 實體類字段是否正確2. 檢查 Word 模板中是否真的是 ${NO}大小寫敏感檢查是否在表格 or 段落中??2、使用方式一返回的是zip文件而不是word 文件核心原因.docx 文件本質(zhì)上就是一個 ZIP 壓縮包Microsoft Office 2007 及以后的 .docx、.xlsx、.pptx 文件都采用 Open XML 格式。這種格式實際上是將 XML、圖片、樣式等文件打包成一個 ZIP 壓縮包只是擴展名改成了 .docx。當用代碼生成.docx但沒有正確設(shè)置 HTTP 響應(yīng)頭Content-Type 和 Content-Disposition瀏覽器無法識別這是 Word 文檔會根據(jù)文件內(nèi)容的“真實類型”ZIP來處理于是自動下載為 .zip 文件或提示“文件損壞”解決方案設(shè)置正確的響應(yīng)頭HttpHeaders headers new HttpHeaders();// 1. 設(shè)置 Content-TypeMIME 類型headers.setContentType(MediaType.parseMediaType(application/vnd.openxmlformats-officedocument.wordprocessingml.document));// 2. 設(shè)置 Content-Disposition告訴瀏覽器這是附件且文件名是 .docxheaders.setContentDispositionFormData(attachment, 生產(chǎn)任務(wù)單.docx);return new ResponseEntity(docBytes, headers, HttpStatus.OK);? 常見錯誤寫法會導(dǎo)致 ZIP 下載// 錯誤1Content-Type 寫成 application/zip 或 application/octet-streamheaders.setContentType(MediaType.APPLICATION_OCTET_STREAM); // ?// 錯誤2文件名沒有 .docx 后綴headers.setContentDispositionFormData(attachment, report); // ? 下載為 report.zip// 錯誤3文件名包含非法字符如 / : * ? |headers.setContentDispositionFormData(attachment, 生產(chǎn)/任務(wù)單.docx); // ? 可能被截斷或變 ZIP 額外檢查點確認生成的字節(jié)數(shù)組確實是合法 .docx將 docBytes 保存到本地文件Files.write(Paths.get(test.docx), docBytes);用 Word 能正常打開嗎如果打不開 → 說明生成邏輯有誤不是 ZIP 問題是文件損壞不要用 application/zip 或 application/octet-stream即使內(nèi)容是 ZIP 結(jié)構(gòu)也必須聲明為 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根本原因在設(shè)置 HTTP 響應(yīng)頭特別是 Content-Disposition 文件名時直接使用了包含中文字符如“生產(chǎn)任務(wù)單.docx”的字符串而 Tomcat 在處理 HTTP 響應(yīng)頭時默認使用 ISO-8859-1 編碼只支持 0–255 的字節(jié)范圍無法表示中文字符Unicode 超出 255于是拋出異常。? 正確解決方案對文件名進行 RFC 5987 / RFC 2231 兼容的編碼HTTP 協(xié)議規(guī)定響應(yīng)頭中的非 ASCII 字符必須進行編碼。推薦使用 filename* 語法帶編碼聲明。// 設(shè)置正確的 Content-Typeresponse.setContentType(application/vnd.openxmlformats-officedocument.wordprocessingml.document);response.setContentLength(docBytes.length);// ? 安全設(shè)置帶中文的文件名關(guān)鍵String filename 生產(chǎn)任務(wù)單_ 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);// 寫入響應(yīng)體response.getOutputStream().write(docBytes);response.getOutputStream().flush();六、接口驗證可以訪問接口http://127.0.0.1:8199/api/generate-word2?no27202SCRW250006這樣你的瀏覽器就會彈出下載頁面并且獲取一個填充數(shù)據(jù)的word 文檔image-20251029164051702