package com.trafficaudit.holiday.service; import org.springframework.stereotype.Service; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import java.util.zip.ZipOutputStream; @Service public class HolidayTrendWordService { private static final String WORD_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"; private static final String DRAWING_NS = "http://schemas.openxmlformats.org/drawingml/2006/main"; private static final String WORD_DRAWING_NS = "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"; private static final String REL_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"; private static final String PACKAGE_REL_NS = "http://schemas.openxmlformats.org/package/2006/relationships"; public byte[] generate(byte[] templateBytes, int year, List blocks) { try { byte[] documentXml = readEntry(templateBytes, "word/document.xml"); Document document = parse(documentXml); Element body = firstElement(document.getDocumentElement(), "body"); if (body == null) throw new IllegalArgumentException("走势图模板缺少正文"); TemplateParts parts = TemplateParts.from(document, body); Map media = new LinkedHashMap<>(); List imageReferences = new ArrayList<>(); int imageIndex = 1; for (DocumentBlock block : blocks) { if (!(block instanceof ChartBlock)) continue; String imageName = "image" + imageIndex++ + ".png"; imageReferences.add(new ImageReference("rId" + (100 + imageIndex - 2), "media/" + imageName, ((ChartBlock) block).getPng())); media.put("word/media/" + imageName, ((ChartBlock) block).getPng()); } Element sectPr = directChild(body, "sectPr"); List removals = directChildren(body); for (Node node : removals) { if (node != sectPr) body.removeChild(node); } appendClone(body, parts.titleLine, year + "年“十一”假期全省综合运输客流量", sectPr); appendClone(body, parts.subtitleLine, null, sectPr); appendClone(body, parts.titleBlank, null, sectPr); int chartCursor = 0; int chartsOnPage = 0; for (int blockIndex = 0; blockIndex < blocks.size(); blockIndex++) { DocumentBlock block = blocks.get(blockIndex); if (block instanceof PageBreakBlock) { appendPageBreak(body, sectPr); chartsOnPage = 0; } else if (block instanceof SectionBlock) { appendClone(body, parts.sectionTemplate, ((SectionBlock) block).getText(), sectPr, true); } else if (block instanceof ChartBlock) { ChartBlock chart = (ChartBlock) block; Element titleTemplate = chart.isCompact() ? parts.compactTitleTemplate : parts.lineTitleTemplate; Element imageTemplate = chart.isCompact() ? parts.compactImageTemplate : parts.lineImageTemplate; appendClone(body, titleTemplate, chart.getTitle(), sectPr); Element imageParagraph = clone(imageTemplate); ImageReference imageReference = imageReferences.get(chartCursor++); bindImage(imageParagraph, imageReference, chart.getTitle(), chartCursor, ((ChartBlock) block).getImageSize()); body.insertBefore(imageParagraph, sectPr); appendClone(body, parts.chartBlank, null, sectPr); chartsOnPage++; boolean firstChartPageBreak = chartCursor == 1 && needsAutomaticPageBreak(blocks, blockIndex + 1); if (firstChartPageBreak || chartsOnPage >= 2 && needsAutomaticPageBreak(blocks, blockIndex + 1)) { appendPageBreak(body, sectPr); chartsOnPage = 0; } } } byte[] updatedDocument = serialize(document); byte[] updatedRels = buildRelationships(templateBytes, imageReferences); Map replacements = new HashMap<>(); replacements.put("word/document.xml", updatedDocument); replacements.put("word/_rels/document.xml.rels", updatedRels); replacements.putAll(media); return rewriteZip(templateBytes, replacements); } catch (Exception e) { throw new IllegalArgumentException("走势图文档生成失败: " + e.getMessage(), e); } } private byte[] buildRelationships(byte[] templateBytes, List imageReferences) throws Exception { Document rels = parse(readEntry(templateBytes, "word/_rels/document.xml.rels")); Element root = rels.getDocumentElement(); NodeList children = root.getChildNodes(); List removals = new ArrayList<>(); for (int i = 0; i < children.getLength(); i++) { Node node = children.item(i); if (node instanceof Element) { Element element = (Element) node; if ("Relationship".equals(element.getLocalName()) && element.getAttribute("Type").endsWith("/image")) { removals.add(node); } } } for (Node node : removals) root.removeChild(node); for (ImageReference reference : imageReferences) { Element relationship = rels.createElementNS(PACKAGE_REL_NS, "Relationship"); relationship.setAttribute("Id", reference.getRelationshipId()); relationship.setAttribute("Type", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image"); relationship.setAttribute("Target", reference.getTarget()); root.appendChild(relationship); } return serialize(rels); } private void bindImage(Element paragraph, ImageReference reference, String title, int index, ImageSize imageSize) { Element blip = firstElement(paragraph, "blip"); if (blip == null) throw new IllegalArgumentException("走势图模板图片节点缺失"); blip.setAttributeNS(REL_NS, "r:embed", reference.getRelationshipId()); Element srcRect = firstElement(paragraph, "srcRect"); if (srcRect != null) srcRect.getParentNode().removeChild(srcRect); resizeDrawing(paragraph, imageSize); Element docPr = firstElement(paragraph, "docPr"); if (docPr != null) { docPr.setAttribute("id", String.valueOf(1000 + index)); docPr.setAttribute("name", title); docPr.setAttribute("descr", title); } } private void resizeDrawing(Element paragraph, ImageSize imageSize) { if (imageSize == null) return; NodeList extents = paragraph.getElementsByTagNameNS(WORD_DRAWING_NS, "extent"); for (int i = 0; i < extents.getLength(); i++) { Element extent = (Element) extents.item(i); extent.setAttribute("cx", String.valueOf(imageSize.getWidth())); extent.setAttribute("cy", String.valueOf(imageSize.getHeight())); } NodeList transforms = paragraph.getElementsByTagNameNS(DRAWING_NS, "xfrm"); for (int i = 0; i < transforms.getLength(); i++) { Element transform = (Element) transforms.item(i); Element extent = firstElement(transform, "ext"); if (extent == null) continue; extent.setAttribute("cx", String.valueOf(imageSize.getWidth())); extent.setAttribute("cy", String.valueOf(imageSize.getHeight())); } } private void appendClone(Element body, Element template, String text, Element before) { appendClone(body, template, text, before, false); } private void appendClone(Element body, Element template, String text, Element before, boolean removeNumbering) { if (template == null) return; Element copy = clone(template); if (removeNumbering) removeNumbering(copy); if (text != null) setParagraphText(copy, text); body.insertBefore(copy, before); } private void removeNumbering(Element paragraph) { NodeList numbered = paragraph.getElementsByTagNameNS(WORD_NS, "numPr"); while (numbered.getLength() > 0) { Node node = numbered.item(0); node.getParentNode().removeChild(node); } } private boolean needsAutomaticPageBreak(List blocks, int nextIndex) { if (nextIndex >= blocks.size()) return false; return !(blocks.get(nextIndex) instanceof PageBreakBlock); } private void appendPageBreak(Element body, Element before) { Document document = body.getOwnerDocument(); Element paragraph = document.createElementNS(WORD_NS, "w:p"); Element run = document.createElementNS(WORD_NS, "w:r"); Element br = document.createElementNS(WORD_NS, "w:br"); br.setAttributeNS(WORD_NS, "w:type", "page"); run.appendChild(br); paragraph.appendChild(run); body.insertBefore(paragraph, before); } private void setParagraphText(Element paragraph, String text) { Element pPr = firstDirectChild(paragraph, "pPr"); Element firstRun = firstDirectChild(paragraph, "r"); Element rPr = firstRun == null ? null : firstDirectChild(firstRun, "rPr"); List removals = new ArrayList<>(); Node child = paragraph.getFirstChild(); while (child != null) { Node next = child.getNextSibling(); if (child != pPr) removals.add(child); child = next; } for (Node node : removals) paragraph.removeChild(node); Element run = paragraph.getOwnerDocument().createElementNS(WORD_NS, "w:r"); if (rPr != null) run.appendChild(rPr.cloneNode(true)); Element t = paragraph.getOwnerDocument().createElementNS(WORD_NS, "w:t"); t.setAttributeNS("http://www.w3.org/XML/1998/namespace", "xml:space", "preserve"); t.setTextContent(text); run.appendChild(t); paragraph.appendChild(run); } private Element clone(Element source) { return (Element) source.cloneNode(true); } private byte[] rewriteZip(byte[] source, Map replacements) throws Exception { ByteArrayOutputStream output = new ByteArrayOutputStream(); Map seen = new HashMap<>(); try (ZipInputStream input = new ZipInputStream(new ByteArrayInputStream(source)); ZipOutputStream zipOutput = new ZipOutputStream(output)) { ZipEntry entry; byte[] buffer = new byte[8192]; while ((entry = input.getNextEntry()) != null) { seen.put(entry.getName(), Boolean.TRUE); byte[] replacement = replacements.get(entry.getName()); if (isGeneratedChartImage(entry.getName()) && replacement == null) continue; zipOutput.putNextEntry(new ZipEntry(entry.getName())); if (replacement != null) { zipOutput.write(replacement); } else { int length; while ((length = input.read(buffer)) > 0) { zipOutput.write(buffer, 0, length); } } zipOutput.closeEntry(); } for (Map.Entry replacement : replacements.entrySet()) { if (Boolean.TRUE.equals(seen.get(replacement.getKey()))) continue; zipOutput.putNextEntry(new ZipEntry(replacement.getKey())); zipOutput.write(replacement.getValue()); zipOutput.closeEntry(); } } return output.toByteArray(); } private boolean isGeneratedChartImage(String entryName) { return entryName != null && entryName.matches("word/media/image[0-9]+\\.png"); } private byte[] readEntry(byte[] zipBytes, String entryName) throws Exception { try (ZipInputStream input = new ZipInputStream(new ByteArrayInputStream(zipBytes))) { ZipEntry entry; while ((entry = input.getNextEntry()) != null) { if (entryName.equals(entry.getName())) return readAll(input); } } throw new IllegalArgumentException("模板中缺少 " + entryName); } private Document parse(byte[] xml) throws Exception { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setNamespaceAware(true); return factory.newDocumentBuilder().parse(new ByteArrayInputStream(xml)); } private byte[] serialize(Document document) throws Exception { Transformer transformer = TransformerFactory.newInstance().newTransformer(); transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no"); ByteArrayOutputStream output = new ByteArrayOutputStream(); transformer.transform(new DOMSource(document), new StreamResult(output)); return output.toByteArray(); } private byte[] readAll(InputStream input) throws Exception { ByteArrayOutputStream output = new ByteArrayOutputStream(); byte[] buffer = new byte[8192]; int length; while ((length = input.read(buffer)) > 0) { output.write(buffer, 0, length); } return output.toByteArray(); } private Element firstElement(Element parent, String localName) { NodeList nodes = parent.getElementsByTagNameNS("*", localName); if (nodes.getLength() > 0) return (Element) nodes.item(0); if (DRAWING_NS.equals(parent.getNamespaceURI()) && parent.getLocalName() != null && localName.equals(parent.getLocalName())) return parent; NodeList children = parent.getChildNodes(); for (int i = 0; i < children.getLength(); i++) { Node node = children.item(i); if (node instanceof Element) { Element found = firstElement((Element) node, localName); if (found != null) return found; } } return null; } private Element firstDirectChild(Element parent, String localName) { Node node = parent.getFirstChild(); while (node != null) { if (node instanceof Element && localName.equals(localName(node))) return (Element) node; node = node.getNextSibling(); } return null; } private Element directChild(Element parent, String localName) { return firstDirectChild(parent, localName); } private List directChildren(Element parent) { List result = new ArrayList<>(); Node node = parent.getFirstChild(); while (node != null) { result.add(node); node = node.getNextSibling(); } return result; } private String localName(Node node) { return node.getLocalName() == null ? node.getNodeName().replaceFirst("^.*:", "") : node.getLocalName(); } public static class DocumentBlock { } public static class SectionBlock extends DocumentBlock { private final String text; public SectionBlock(String text) { this.text = text; } public String getText() { return text; } } public static class PageBreakBlock extends DocumentBlock { } public static class ChartBlock extends DocumentBlock { private final String title; private final byte[] png; private final boolean compact; private final ImageSize imageSize; public ChartBlock(String title, byte[] png) { this(title, png, false); } public ChartBlock(String title, byte[] png, boolean compact) { this(title, png, compact, null); } public ChartBlock(String title, byte[] png, boolean compact, ImageSize imageSize) { this.title = title; this.png = png; this.compact = compact; this.imageSize = imageSize; } public ChartBlock(String title, byte[] png, ImageSize imageSize) { this(title, png, false, imageSize); } public String getTitle() { return title; } public byte[] getPng() { return png; } public boolean isCompact() { return compact; } public ImageSize getImageSize() { return imageSize; } } public static class ImageSize { private final long width; private final long height; public ImageSize(long width, long height) { this.width = width; this.height = height; } public long getWidth() { return width; } public long getHeight() { return height; } } private static class ImageReference { private final String relationshipId; private final String target; private final byte[] image; private ImageReference(String relationshipId, String target, byte[] image) { this.relationshipId = relationshipId; this.target = target; this.image = image; } private String getRelationshipId() { return relationshipId; } private String getTarget() { return target; } private byte[] getImage() { return image; } } private static class TemplateParts { private Element titleLine; private Element subtitleLine; private Element titleBlank; private Element sectionTemplate; private Element sectionBlank; private Element lineTitleTemplate; private Element lineImageTemplate; private Element compactTitleTemplate; private Element compactImageTemplate; private Element chartBlank; private static TemplateParts from(Document document, Element body) { TemplateParts parts = new TemplateParts(); List paragraphs = directParagraphs(body); parts.titleLine = byText(paragraphs, "2026年“十一”假期全省综合运输客流量"); parts.subtitleLine = byText(paragraphs, "走 势 图"); parts.sectionTemplate = byText(paragraphs, "总体情况"); parts.lineTitleTemplate = byText(paragraphs, "2026年“十一”假期全省跨区域人员流动量情况"); parts.lineImageTemplate = imageAfter(paragraphs, parts.lineTitleTemplate); parts.compactTitleTemplate = byText(paragraphs, "“十一”假期各交通运输方式客运量占比情况"); parts.compactImageTemplate = imageAfter(paragraphs, parts.compactTitleTemplate); parts.titleBlank = nextParagraph(paragraphs, parts.subtitleLine); parts.sectionBlank = nextParagraph(paragraphs, parts.sectionTemplate); parts.chartBlank = afterParagraph(paragraphs, parts.lineImageTemplate); if (parts.titleLine == null || parts.subtitleLine == null || parts.sectionTemplate == null || parts.lineTitleTemplate == null || parts.lineImageTemplate == null || parts.compactTitleTemplate == null || parts.compactImageTemplate == null) { throw new IllegalArgumentException("走势图模板结构不完整"); } return parts; } private static List directParagraphs(Element body) { List result = new ArrayList<>(); Node node = body.getFirstChild(); while (node != null) { if (node instanceof Element && "p".equals(((Element) node).getLocalName())) { result.add((Element) node); } node = node.getNextSibling(); } return result; } private static Element byText(List paragraphs, String text) { for (Element paragraph : paragraphs) { if (text.equals(paragraphText(paragraph))) return paragraph; } return null; } private static Element imageAfter(List paragraphs, Element anchor) { int index = paragraphs.indexOf(anchor); if (index < 0) return null; for (int i = index + 1; i < paragraphs.size(); i++) { if (firstDescendant(paragraphs.get(i), "blip") != null) return paragraphs.get(i); } return null; } private static Element nextParagraph(List paragraphs, Element anchor) { int index = paragraphs.indexOf(anchor); return index >= 0 && index + 1 < paragraphs.size() ? paragraphs.get(index + 1) : null; } private static Element afterParagraph(List paragraphs, Element anchor) { int index = paragraphs.indexOf(anchor); return index >= 0 && index + 1 < paragraphs.size() ? paragraphs.get(index + 1) : null; } private static Element firstDescendant(Element parent, String localName) { NodeList nodes = parent.getElementsByTagNameNS("*", localName); return nodes.getLength() == 0 ? null : (Element) nodes.item(0); } private static String paragraphText(Element paragraph) { StringBuilder builder = new StringBuilder(); NodeList nodes = paragraph.getElementsByTagNameNS(WORD_NS, "t"); for (int i = 0; i < nodes.getLength(); i++) { builder.append(nodes.item(i).getTextContent()); } return builder.toString(); } } }