From 9ee7acd9d16e59224ee5184c89374f06d6592ad3 Mon Sep 17 00:00:00 2001
From: xyc <jc_xiong@hotmail.com>
Date: 星期日, 04 十月 2026 17:47:34 +0800
Subject: [PATCH] feat(holiday): 新增节假日综合运输客流量走势图

---
 traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayTrendWordService.java |  535 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 535 insertions(+), 0 deletions(-)

diff --git a/traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayTrendWordService.java b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayTrendWordService.java
new file mode 100644
index 0000000..ccca074
--- /dev/null
+++ b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayTrendWordService.java
@@ -0,0 +1,535 @@
+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<DocumentBlock> 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<String, byte[]> media = new LinkedHashMap<>();
+            List<ImageReference> 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<Node> 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<String, byte[]> 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("璧板娍鍥炬枃妗g敓鎴愬け璐�: " + e.getMessage(), e);
+        }
+    }
+
+    private byte[] buildRelationships(byte[] templateBytes, List<ImageReference> imageReferences) throws Exception {
+        Document rels = parse(readEntry(templateBytes, "word/_rels/document.xml.rels"));
+        Element root = rels.getDocumentElement();
+        NodeList children = root.getChildNodes();
+        List<Node> 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<DocumentBlock> 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<Node> 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<String, byte[]> replacements) throws Exception {
+        ByteArrayOutputStream output = new ByteArrayOutputStream();
+        Map<String, Boolean> 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<String, byte[]> 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<Node> directChildren(Element parent) {
+        List<Node> 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<Element> 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<Element> directParagraphs(Element body) {
+            List<Element> 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<Element> paragraphs, String text) {
+            for (Element paragraph : paragraphs) {
+                if (text.equals(paragraphText(paragraph))) return paragraph;
+            }
+            return null;
+        }
+
+        private static Element imageAfter(List<Element> 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<Element> 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<Element> 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();
+        }
+    }
+}

--
Gitblit v1.9.1