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