建设学校网站的意义,搜索引擎营销漏斗模型,wordpress登陆后可见,开封 网站建设 网络推广文介绍一种高效、灵活的解决方案#xff1a;通过预定义 Word 模板中的 ${KEY} 占位符#xff0c;结合后端数据自动填充生成最终文档。该方法实现逻辑清晰、模板可由非技术人员维护#xff0c;显著提升开发效率与系统可扩展性。以下是代码实现步骤和逻辑。二、添加依赖:Apach…文介绍一种高效、灵活的解决方案通过预定义 Word 模板中的 ${KEY} 占位符结合后端数据自动填充生成最终文档。该方法实现逻辑清晰、模板可由非技术人员维护显著提升开发效率与系统可扩展性。以下是代码实现步骤和逻辑。二、添加依赖: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 模板打开需要生产的数据模板在对应位置填写占位符类似下图占位符格式为${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);// 设置正确的 Content-Typeresponse.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:ListProductionOrder getProductDataList this.list(// new LambdaQueryWrapperProductionOrder()// .eq(ProductionOrder::getNo, no));ProductionOrder order new ProductionOrder();// 改用模板生成byte[] docBytes productionOrderService.generateFromTemplate(order);// 设置正确的 Content-Typeresponse.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层核心实现代码 注这里就省去了接口层需要可以自己加直接放置的核心方法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(生产任务单申请表);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. 构建数据映射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. 输出为字节数组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、占位符被拆分问题未能正确显示数值Word 会因格式变化将 ${NO} 拆成多个 Run如 ${N O}导致无法匹配。这里不要用文本框或艺术字等Apache POI 在读取 Word 文档时会将文本按格式字体、颜色、加粗等拆分成多个 XWPFRun 对象。例如下面图片编号未能正确显示image-20251029163259173❌ 问题场景如果在 Word 中输入 ${NO} 时中间不小心按了方向键、空格、Backspace或对部分字符设置了格式比如只加粗了 N或从其他地方复制粘贴过来那么 Word 内部可能存储为Run1: ${NRun2: O}而替换逻辑是 逐 Run 处理for (XWPFRun run : para.getRuns()) {String text run.getText(0); // 只拿到 ${N 或 O}// 无法匹配完整 ${NO}}→ 结果${NO} 没有被识别也就不会被替换而其他占位符如 ${SJBBH}可能是一次性输入的所以在一个 Run 里能正常替换。解决方案在模板中一次性输入完整占位符避免中途格式调整。不要中途按方向键、不要设置局部格式 技巧可以先输入 ABC确认它在一个 Run 里比如全选后统一加粗再替换成 ${NO}。或使用更高级的跨 Run 合并替换算法实现复杂。当前逻辑只处理单个 Run无法处理被拆分的占位符。可以改用更健壮的方案方案 A合并段落所有文本整体替换简单但会丢失格式不推荐会破坏原有样式方案 B使用递归或缓冲区拼接 Run复杂但对大多数项目来说方法 1规范模板输入是最高效、最可靠的。 调试技巧如果替换失败可临时打印 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 文档会根据文件内容的“真实类型”ZIP来处理于是自动下载为 .zip 文件或提示“文件损坏”解决方案设置正确的响应头HttpHeaders headers new HttpHeaders();// 1. 设置 Content-TypeMIME 类型headers.setContentType(MediaType.parseMediaType(application/vnd.openxmlformats-officedocument.wordprocessingml.document));// 2. 设置 Content-Disposition告诉浏览器这是附件且文件名是 .docxheaders.setContentDispositionFormData(attachment, 生产任务单.docx);return new ResponseEntity(docBytes, headers, HttpStatus.OK);❌ 常见错误写法会导致 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, 生产/任务单.docx); // ❌ 可能被截断或变 ZIP 额外检查点确认生成的字节数组确实是合法 .docx将 docBytes 保存到本地文件Files.write(Paths.get(test.docx), docBytes);用 Word 能正常打开吗如果打不开 → 说明生成逻辑有误不是 ZIP 问题是文件损坏不要用 application/zip 或 application/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 的字节范围无法表示中文字符Unicode 超出 255于是抛出异常。✅ 正确解决方案对文件名进行 RFC 5987 / RFC 2231 兼容的编码HTTP 协议规定响应头中的非 ASCII 字符必须进行编码。推荐使用 filename\* 语法带编码声明。// 设置正确的 Content-Typeresponse.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?no27202SCRW250006这样你的浏览器就会弹出下载页面并且获取一个填充数据的word 文档image-20251029164051702