From bee9e18c3af284ff432a4472f1d409f5a6dc09a7 Mon Sep 17 00:00:00 2001
From: zhizhijie <zhizhijie@users.noreply.gitee.com>
Date: 星期二, 06 十月 2026 16:59:15 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/main'
---
traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayBulletinService.java | 554 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 554 insertions(+), 0 deletions(-)
diff --git a/traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayBulletinService.java b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayBulletinService.java
new file mode 100644
index 0000000..77f85f9
--- /dev/null
+++ b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayBulletinService.java
@@ -0,0 +1,554 @@
+package com.trafficaudit.holiday.service;
+
+import com.trafficaudit.holiday.HolidayNumberUtils;
+import com.trafficaudit.holiday.entity.HolidayCalendar;
+import com.trafficaudit.holiday.service.HolidayStatService.DailyMetrics;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+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.annotation.Resource;
+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.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.nio.file.Files;
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+import java.util.zip.ZipOutputStream;
+
+@Slf4j
+@Service
+public class HolidayBulletinService {
+
+ private static final String WORD_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
+ private static final BigDecimal EPSILON = new BigDecimal("0.0000001");
+ private static final int EXCEL_ONE_DECIMAL_PERCENT = 1;
+ private static final int EXCEL_TWO_DECIMAL_PERCENT = 2;
+ private static final int EXCEL_SEVEN_DECIMAL_YI = 7;
+ private static final BigDecimal TEN_THOUSAND = new BigDecimal("10000");
+
+ @Value("${holiday.bulletin-template:docs/涓撻」缁熻/閲嶈鑺傚亣鏃�/婀栧寳鐪佸浗搴嗗亣鏈熺患鍚堣繍杈撳娴佸揩鎶ユā鏉�.docx}")
+ private String bulletinTemplatePath;
+
+ private final HolidayStatService statService;
+ @Resource
+ private HolidayConfigService configService;
+ private static final ThreadLocal<Document> CURRENT_DOCUMENT = new ThreadLocal<>();
+
+ public HolidayBulletinService(HolidayStatService statService) {
+ this.statService = statService;
+ }
+
+ public byte[] generate(LocalDate date) {
+ DailyMetrics current = statService.findMetricsByDate(date);
+ if (current == null) {
+ throw new IllegalArgumentException("鎵�閫夋棩鏈熶笉鍦ㄥ凡閰嶇疆鐨勮妭鍋囨棩鑼冨洿鍐�");
+ }
+ DailyMetrics previousDay = current.getDayIndex() > 1
+ ? statService.ringRatioBaseMetrics(current.getYear(), current.getHolidayType(), current.getDayIndex()) : null;
+ HolidayCalendar previousDayConfig = configService.findPreviousDay(current.getYear(), current.getHolidayType());
+ LocalDate previousDayDate = previousDayConfig == null || previousDayConfig.getHolidayDate() == null
+ ? current.getDate().minusDays(1) : previousDayConfig.getHolidayDate();
+ DailyMetrics previousCalendarDay = previousDay != null
+ ? previousDay : statService.dailyMetricsByDate(previousDayDate);
+ DailyMetrics previousYear = statService.dailyMetrics(current.getYear() - 1, current.getHolidayType(), current.getDayIndex());
+ DailyMetrics twoYearsAgo = statService.dailyMetrics(current.getYear() - 2, current.getHolidayType(), current.getDayIndex());
+ DailyMetrics currentCumulative = statService.cumulativeMetrics(current.getYear(), current.getHolidayType(), current.getDayIndex());
+ DailyMetrics previousYearCumulative = statService.cumulativeMetrics(current.getYear() - 1, current.getHolidayType(), current.getDayIndex());
+ DailyMetrics twoYearsAgoCumulative = statService.cumulativeMetrics(current.getYear() - 2, current.getHolidayType(), current.getDayIndex());
+
+ if (current.getTotalPersonFlow() == null) {
+ throw new IllegalArgumentException("鎵�閫夋棩鏈熺殑鍏ㄧぞ浼氳法鍖哄煙浜哄憳娴佸姩閲忔病鏈夎冻澶熸暟鎹紝鏃犳硶鐢熸垚蹇姤");
+ }
+
+ String currentTotal = formatNumber(current.getTotalPersonFlow());
+ String ringRatioClause = previousDay == null ? "" : "锛岀幆姣�" + ratioPhrase(
+ current.getTotalPersonFlow(), previousDay.getTotalPersonFlow(), EXCEL_TWO_DECIMAL_PERCENT);
+ String paragraph1 = String.format("%d鏈�%d鏃ワ紙鍋囨湡绗�%d澶╋級锛屽叏鐪佸叏绀句細璺ㄥ尯鍩熶汉鍛樻祦鍔ㄩ噺%s涓囦汉娆�%s锛屾瘮2025骞村悓鏈�%s锛屾瘮2024骞村悓鏈�%s銆�",
+ date.getMonthValue(), date.getDayOfMonth(), current.getDayIndex(), currentTotal,
+ ringRatioClause,
+ ratioPhrase(current.getTotalPersonFlow(), previousYear == null ? null : previousYear.getTotalPersonFlow(), EXCEL_ONE_DECIMAL_PERCENT),
+ ratioPhrase(current.getTotalPersonFlow(), twoYearsAgo == null ? null : twoYearsAgo.getTotalPersonFlow(), EXCEL_ONE_DECIMAL_PERCENT));
+ String paragraph2 = cumulativeParagraph(currentCumulative, previousYearCumulative, twoYearsAgoCumulative);
+
+ File template = resolveTemplateFile();
+ if (!template.exists() || !template.isFile()) {
+ throw new IllegalArgumentException("蹇姤妯℃澘鏂囦欢涓嶅瓨鍦�: " + template.getAbsolutePath());
+ }
+
+ try {
+ byte[] templateBytes;
+ try (InputStream in = new FileInputStream(template)) {
+ templateBytes = readAll(in);
+ }
+ byte[] documentXml = readZipEntry(templateBytes, "word/document.xml");
+ Document document = parseDocument(documentXml);
+ CURRENT_DOCUMENT.set(document);
+ try {
+ replaceParagraph(document, text -> text.startsWith("10鏈�") && text.contains("鍋囨湡绗�"), paragraph1);
+ replaceParagraph(document, text -> text.startsWith("鍋囨湡浠ユ潵"), paragraph2);
+ fillTable(document, current, previousDay, previousCalendarDay, previousYear, twoYearsAgo,
+ currentCumulative, previousYearCumulative, twoYearsAgoCumulative);
+ byte[] replacedXml = serializeDocument(document);
+ return replaceZipEntry(templateBytes, "word/document.xml", replacedXml);
+ } finally {
+ CURRENT_DOCUMENT.remove();
+ }
+ } catch (Exception e) {
+ throw new IllegalArgumentException("蹇姤鐢熸垚澶辫触: " + e.getMessage(), e);
+ }
+ }
+
+ private void fillTable(Document document, DailyMetrics current, DailyMetrics previousDay,
+ DailyMetrics previousCalendarDay, DailyMetrics previousYear, DailyMetrics twoYearsAgo,
+ DailyMetrics currentCumulative, DailyMetrics previousYearCumulative,
+ DailyMetrics twoYearsAgoCumulative) {
+ DailyMetrics firstDayRailBase = previousDay == null ? previousCalendarDay : previousDay;
+ DailyMetrics firstDayCivilAviationBase = previousDay == null ? previousCalendarDay : previousDay;
+ Element table = firstElement(document, "tbl");
+ if (table == null) {
+ throw new IllegalArgumentException("蹇姤妯℃澘涓壘涓嶅埌鏁版嵁琛�");
+ }
+ DailyMetrics previousYearCivilCumulative = comparableCumulative(
+ current, previousYear, DailyMetrics::getCivilAviation);
+ DailyMetrics twoYearsAgoCivilCumulative = comparableCumulative(
+ current, twoYearsAgo, DailyMetrics::getCivilAviation);
+ writeTableRow(table, 2, new String[]{
+ number(current.getNonBusinessPersonFlow()),
+ ratio(current.getNonBusinessPersonFlow(), previousDay == null ? null : previousDay.getNonBusinessPersonFlow(),
+ EXCEL_TWO_DECIMAL_PERCENT),
+ ratio(current.getNonBusinessPersonFlow(), previousYear == null ? null : previousYear.getNonBusinessPersonFlow(),
+ EXCEL_ONE_DECIMAL_PERCENT),
+ ratio(current.getNonBusinessPersonFlow(), twoYearsAgo == null ? null : twoYearsAgo.getNonBusinessPersonFlow(),
+ EXCEL_ONE_DECIMAL_PERCENT),
+ numberCumulative(currentCumulative.getNonBusinessPersonFlow()),
+ ratio(currentCumulative.getNonBusinessPersonFlow(),
+ previousYearCumulative == null ? null : previousYearCumulative.getNonBusinessPersonFlow(),
+ EXCEL_TWO_DECIMAL_PERCENT),
+ ratio(currentCumulative.getNonBusinessPersonFlow(),
+ twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.getNonBusinessPersonFlow(),
+ EXCEL_TWO_DECIMAL_PERCENT)
+ });
+ writeTableRow(table, 3, new String[]{
+ number(current.getRoad()),
+ ratio(current.getRoad(), previousDay == null ? null : previousDay.getRoad(), EXCEL_ONE_DECIMAL_PERCENT),
+ ratio(current.getRoad(), previousYear == null ? null : previousYear.getRoad(), EXCEL_ONE_DECIMAL_PERCENT),
+ ratio(current.getRoad(), twoYearsAgo == null ? null : twoYearsAgo.getRoad(), EXCEL_TWO_DECIMAL_PERCENT),
+ number(currentCumulative.getRoad()),
+ ratio(currentCumulative.getRoad(), previousYearCumulative == null ? null : previousYearCumulative.getRoad(),
+ EXCEL_ONE_DECIMAL_PERCENT),
+ ratio(currentCumulative.getRoad(), twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.getRoad(),
+ EXCEL_ONE_DECIMAL_PERCENT)
+ });
+ writeTableRow(table, 4, new String[]{
+ number(current.getRoadPersonFlow()),
+ ratio(current.getRoadPersonFlow(), previousDay == null ? null : previousDay.getRoadPersonFlow(),
+ EXCEL_TWO_DECIMAL_PERCENT),
+ ratio(current.getRoadPersonFlow(), previousYear == null ? null : previousYear.getRoadPersonFlow(),
+ EXCEL_ONE_DECIMAL_PERCENT),
+ ratio(current.getRoadPersonFlow(), twoYearsAgo == null ? null : twoYearsAgo.getRoadPersonFlow(),
+ EXCEL_ONE_DECIMAL_PERCENT),
+ numberCumulative(currentCumulative.getRoadPersonFlow()),
+ ratio(currentCumulative.getRoadPersonFlow(),
+ previousYearCumulative == null ? null : previousYearCumulative.getRoadPersonFlow(),
+ EXCEL_TWO_DECIMAL_PERCENT),
+ ratio(currentCumulative.getRoadPersonFlow(),
+ twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.getRoadPersonFlow(),
+ EXCEL_TWO_DECIMAL_PERCENT)
+ });
+ writeTableRow(table, 5, transportRow(current.getRail(),
+ firstDayRailBase == null ? null : firstDayRailBase.getRail(),
+ previousYear == null ? null : previousYear.getRail(),
+ twoYearsAgo == null ? null : twoYearsAgo.getRail(),
+ currentCumulative.getRail(),
+ previousYearCumulative == null ? null : previousYearCumulative.getRail(),
+ twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.getRail(),
+ EXCEL_ONE_DECIMAL_PERCENT));
+ writeTableRow(table, 6, transportRow(current.getCivilAviation(),
+ firstDayCivilAviationBase == null ? null : firstDayCivilAviationBase.getCivilAviation(),
+ previousYear == null ? null : previousYear.getCivilAviation(),
+ twoYearsAgo == null ? null : twoYearsAgo.getCivilAviation(),
+ currentCumulative.getCivilAviation(),
+ previousYearCivilCumulative == null ? null : previousYearCivilCumulative.getCivilAviation(),
+ twoYearsAgoCivilCumulative == null ? null : twoYearsAgoCivilCumulative.getCivilAviation(),
+ EXCEL_ONE_DECIMAL_PERCENT));
+ writeTableRow(table, 7, transportRow(current.getWater(),
+ previousDay == null ? null : previousDay.getWater(),
+ previousYear == null ? null : previousYear.getWater(),
+ twoYearsAgo == null ? null : twoYearsAgo.getWater(),
+ currentCumulative.getWater(),
+ previousYearCumulative == null ? null : previousYearCumulative.getWater(),
+ twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.getWater(),
+ EXCEL_TWO_DECIMAL_PERCENT));
+ writeTableRow(table, 8, new String[]{
+ number(current.getTotalPersonFlow()),
+ ratio(current.getTotalPersonFlow(), previousDay == null ? null : previousDay.getTotalPersonFlow(),
+ EXCEL_TWO_DECIMAL_PERCENT),
+ ratio(current.getTotalPersonFlow(), previousYear == null ? null : previousYear.getTotalPersonFlow(),
+ EXCEL_ONE_DECIMAL_PERCENT),
+ ratio(current.getTotalPersonFlow(), twoYearsAgo == null ? null : twoYearsAgo.getTotalPersonFlow(),
+ EXCEL_ONE_DECIMAL_PERCENT),
+ numberCumulative(currentCumulative.getTotalPersonFlow()),
+ ratio(currentCumulative.getTotalPersonFlow(),
+ previousYearCumulative == null ? null : previousYearCumulative.getTotalPersonFlow(),
+ EXCEL_TWO_DECIMAL_PERCENT),
+ ratio(currentCumulative.getTotalPersonFlow(),
+ twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.getTotalPersonFlow(),
+ EXCEL_TWO_DECIMAL_PERCENT)
+ });
+ }
+
+ private String[] transportRow(BigDecimal current, BigDecimal previousDay, BigDecimal previousYear,
+ BigDecimal twoYearsAgo, BigDecimal currentCumulative,
+ BigDecimal previousYearCumulative, BigDecimal twoYearsAgoCumulative,
+ int ringRatioExcelScale) {
+ return new String[]{
+ number(current),
+ ratio(current, previousDay, ringRatioExcelScale),
+ ratio(current, previousYear, EXCEL_ONE_DECIMAL_PERCENT),
+ ratio(current, twoYearsAgo, EXCEL_ONE_DECIMAL_PERCENT),
+ number(currentCumulative),
+ ratio(currentCumulative, previousYearCumulative, EXCEL_ONE_DECIMAL_PERCENT),
+ ratio(currentCumulative, twoYearsAgoCumulative, EXCEL_ONE_DECIMAL_PERCENT)
+ };
+ }
+
+ private void writeTableRow(Element table, int rowIndex, String[] values) {
+ List<Element> rows = childElements(table, "tr");
+ if (rowIndex >= rows.size()) return;
+ Element row = rows.get(rowIndex);
+ List<Element> cells = cellRefs(row);
+ int logicalColumn = 0;
+ for (Element cell : cells) {
+ int span = gridSpan(cell);
+ if (logicalColumn >= 2) {
+ int valueIndex = logicalColumn - 2;
+ if (valueIndex < values.length) {
+ setCellText(cell, values[valueIndex]);
+ }
+ }
+ logicalColumn += span;
+ }
+ }
+
+ private List<Element> cellRefs(Element row) {
+ List<Element> cells = new ArrayList<>();
+ for (Element child : childElements(row, "tc")) cells.add(child);
+ return cells;
+ }
+
+ private int gridSpan(Element cell) {
+ Element tcPr = firstChildElement(cell, "tcPr");
+ if (tcPr == null) return 1;
+ Element span = firstChildElement(tcPr, "gridSpan");
+ if (span == null) return 1;
+ String value = span.getAttributeNS(WORD_NS, "val");
+ try {
+ return Integer.parseInt(value);
+ } catch (Exception e) {
+ return 1;
+ }
+ }
+
+ private void setCellText(Element cell, String text) {
+ Element tcPr = firstChildElement(cell, "tcPr");
+ Element templateParagraph = firstChildElement(cell, "p");
+ Element paragraph = createElement("p");
+ if (templateParagraph != null) {
+ Element pPr = firstChildElement(templateParagraph, "pPr");
+ if (pPr != null) paragraph.appendChild(pPr.cloneNode(true));
+ }
+ Element templateRun = templateParagraph == null ? null : firstChildElement(templateParagraph, "r");
+ Element rPr = templateRun == null ? null : firstChildElement(templateRun, "rPr");
+ Element run = createElement("r");
+ if (rPr != null) run.appendChild(rPr.cloneNode(true));
+ Element t = createElement("t");
+ t.setAttributeNS("http://www.w3.org/XML/1998/namespace", "xml:space", "preserve");
+ t.setTextContent(text == null ? "" : text);
+ run.appendChild(t);
+ paragraph.appendChild(run);
+
+ List<Node> removals = new ArrayList<>();
+ Node child = cell.getFirstChild();
+ while (child != null) {
+ Node next = child.getNextSibling();
+ if (child instanceof Element && "p".equals(localName(child))) {
+ removals.add(child);
+ }
+ child = next;
+ }
+ for (Node node : removals) cell.removeChild(node);
+ if (tcPr != null && tcPr.getNextSibling() != null) {
+ cell.insertBefore(paragraph, tcPr.getNextSibling());
+ } else if (tcPr != null) {
+ cell.appendChild(paragraph);
+ } else {
+ cell.insertBefore(paragraph, cell.getFirstChild());
+ }
+ }
+
+ private void replaceParagraph(Document document, ParagraphMatcher matcher, String replacement) {
+ NodeList paragraphs = document.getElementsByTagNameNS(WORD_NS, "p");
+ for (int i = 0; i < paragraphs.getLength(); i++) {
+ Element paragraph = (Element) paragraphs.item(i);
+ String text = paragraphText(paragraph);
+ if (matcher.matches(text)) {
+ setParagraphText(paragraph, replacement);
+ return;
+ }
+ }
+ throw new IllegalArgumentException("蹇姤妯℃澘缂哄皯鍙浛鎹㈡钀�: " + replacement);
+ }
+
+ private void setParagraphText(Element paragraph, String text) {
+ Element pPr = firstChildElement(paragraph, "pPr");
+ Element firstRun = firstChildElement(paragraph, "r");
+ Element rPr = firstRun == null ? null : firstChildElement(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 = createElement("r");
+ if (rPr != null) run.appendChild(rPr.cloneNode(true));
+ Element t = createElement("t");
+ t.setAttributeNS("http://www.w3.org/XML/1998/namespace", "xml:space", "preserve");
+ t.setTextContent(text);
+ run.appendChild(t);
+ paragraph.appendChild(run);
+ }
+
+ private String paragraphText(Element paragraph) {
+ StringBuilder builder = new StringBuilder();
+ NodeList texts = paragraph.getElementsByTagNameNS(WORD_NS, "t");
+ for (int i = 0; i < texts.getLength(); i++) {
+ builder.append(texts.item(i).getTextContent());
+ }
+ return builder.toString();
+ }
+
+ private String ratioPhrase(BigDecimal current, BigDecimal base, int excelPercentScale) {
+ BigDecimal percent = bulletinPercent(current, base, excelPercentScale);
+ if (percent == null) return "鏃犲彲姣旀暟鎹�";
+ if (percent.signum() == 0) return "鎸佸钩";
+ return (percent.signum() > 0 ? "澧為暱" : "涓嬮檷") + HolidayNumberUtils.format1(percent.abs()) + "%";
+ }
+
+ private DailyMetrics comparableCumulative(DailyMetrics current, DailyMetrics comparisonYear,
+ Function<DailyMetrics, BigDecimal> selector) {
+ if (current == null || comparisonYear == null) return null;
+ int coveredDay = 0;
+ for (int day = current.getDayIndex(); day >= 1; day--) {
+ DailyMetrics metrics = statService.dailyMetrics(current.getYear(), current.getHolidayType(), day);
+ if (metrics != null && selector.apply(metrics) != null) {
+ coveredDay = day;
+ break;
+ }
+ }
+ if (coveredDay == 0) return null;
+ return statService.cumulativeMetrics(comparisonYear.getYear(), current.getHolidayType(), coveredDay);
+ }
+
+ private String cumulativeParagraph(DailyMetrics currentCumulative,
+ DailyMetrics previousYearCumulative,
+ DailyMetrics twoYearsAgoCumulative) {
+ BigDecimal total = currentCumulative == null ? null : currentCumulative.getTotalPersonFlow();
+ return String.format("鍋囨湡浠ユ潵锛屽叏鐪佸叏绀句細璺ㄥ尯鍩熶汉鍛樻祦鍔ㄩ噺绱%s涓囦汉娆★紝姣�2025骞村悓鏈�%s锛屾瘮2024骞村悓鏈�%s銆�",
+ formatNumber(displayedCumulativeWan(total)),
+ ratioPhrase(total, previousYearCumulative == null ? null : previousYearCumulative.getTotalPersonFlow(),
+ EXCEL_TWO_DECIMAL_PERCENT),
+ ratioPhrase(total, twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.getTotalPersonFlow(),
+ EXCEL_TWO_DECIMAL_PERCENT));
+ }
+
+ private String ratio(BigDecimal current, BigDecimal base, int excelPercentScale) {
+ BigDecimal percent = bulletinPercent(current, base, excelPercentScale);
+ if (percent == null) return "鈥�";
+ return percent.signum() == 0 ? "0%" : percent.toPlainString() + "%";
+ }
+
+ /**
+ * Mirrors the source Excel percentage cell format, then applies the bulletin's one-decimal rule.
+ * Excel stores full precision but displays 0.0% or 0.00%; reviewers compare against that display.
+ */
+ private BigDecimal bulletinPercent(BigDecimal current, BigDecimal base, int excelPercentScale) {
+ if (current == null || base == null || base.abs().compareTo(EPSILON) < 0) return null;
+ BigDecimal ratio = HolidayNumberUtils.divide(current, base);
+ if (ratio == null) return null;
+ BigDecimal percent = HolidayNumberUtils.multiply(ratio.subtract(BigDecimal.ONE), BigDecimal.valueOf(100));
+ if (percent == null) return null;
+ BigDecimal displayed = percent.setScale(excelPercentScale, RoundingMode.HALF_UP);
+ return displayed.setScale(1, RoundingMode.HALF_UP);
+ }
+
+ private String number(BigDecimal value) {
+ return value == null ? "鈥�" : formatNumber(value);
+ }
+
+ private String numberCumulative(BigDecimal value) {
+ return value == null ? "鈥�" : formatNumber(displayedCumulativeWan(value));
+ }
+
+ /**
+ * Source cumulative cells U5/X5/AA5 are stored in 100-million units and displayed with seven decimals.
+ * Mirror that displayed precision before the bulletin converts the value to 10-thousand units.
+ */
+ private BigDecimal displayedCumulativeWan(BigDecimal cumulativeWan) {
+ if (cumulativeWan == null) return null;
+ BigDecimal yi = cumulativeWan.divide(TEN_THOUSAND, EXCEL_SEVEN_DECIMAL_YI, RoundingMode.HALF_UP);
+ return HolidayNumberUtils.normalize(yi.multiply(TEN_THOUSAND));
+ }
+
+ private String formatNumber(BigDecimal value) {
+ if (value == null) return "";
+ return HolidayNumberUtils.format2(value);
+ }
+
+ private File resolveTemplateFile() {
+ List<File> candidates = new ArrayList<>();
+ File configured = new File(bulletinTemplatePath);
+ candidates.add(configured);
+ if (!configured.isAbsolute()) {
+ candidates.add(new File(System.getProperty("user.dir"), bulletinTemplatePath));
+ candidates.add(new File(System.getProperty("user.dir"), "../" + bulletinTemplatePath));
+ }
+ for (File candidate : candidates) {
+ try {
+ if (candidate.exists() && candidate.isFile()) {
+ return candidate.getCanonicalFile();
+ }
+ } catch (Exception ignored) {
+ // Keep checking.
+ }
+ }
+ return configured;
+ }
+
+ private byte[] readZipEntry(byte[] zipBytes, String entryName) throws Exception {
+ try (ZipInputStream zin = new ZipInputStream(new ByteArrayInputStream(zipBytes))) {
+ ZipEntry entry;
+ while ((entry = zin.getNextEntry()) != null) {
+ if (entryName.equals(entry.getName())) {
+ return readAll(zin);
+ }
+ }
+ }
+ throw new IllegalArgumentException("妯℃澘涓己灏� " + entryName);
+ }
+
+ private byte[] replaceZipEntry(byte[] zipBytes, String entryName, byte[] replacement) throws Exception {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ try (ZipInputStream zin = new ZipInputStream(new ByteArrayInputStream(zipBytes));
+ ZipOutputStream zout = new ZipOutputStream(out)) {
+ ZipEntry entry;
+ byte[] buffer = new byte[8192];
+ while ((entry = zin.getNextEntry()) != null) {
+ ZipEntry target = new ZipEntry(entry.getName());
+ zout.putNextEntry(target);
+ if (entryName.equals(entry.getName())) {
+ zout.write(replacement);
+ } else {
+ int length;
+ while ((length = zin.read(buffer)) > 0) {
+ zout.write(buffer, 0, length);
+ }
+ }
+ zout.closeEntry();
+ }
+ }
+ return out.toByteArray();
+ }
+
+ private Document parseDocument(byte[] xml) throws Exception {
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ factory.setNamespaceAware(true);
+ return factory.newDocumentBuilder().parse(new ByteArrayInputStream(xml));
+ }
+
+ private byte[] serializeDocument(Document document) throws Exception {
+ Transformer transformer = TransformerFactory.newInstance().newTransformer();
+ transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
+ transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ transformer.transform(new DOMSource(document), new StreamResult(out));
+ return out.toByteArray();
+ }
+
+ private Element firstElement(Document document, String localName) {
+ NodeList list = document.getElementsByTagNameNS(WORD_NS, localName);
+ return list.getLength() == 0 ? null : (Element) list.item(0);
+ }
+
+ private Element createElement(String localName) {
+ Document document = CURRENT_DOCUMENT.get();
+ if (document == null) {
+ throw new IllegalStateException("Word document is not initialized");
+ }
+ return document.createElementNS(WORD_NS, "w:" + localName);
+ }
+
+ private List<Element> childElements(Element parent, String localName) {
+ List<Element> result = new ArrayList<>();
+ Node child = parent.getFirstChild();
+ while (child != null) {
+ if (child instanceof Element && localName.equals(localName(child))) {
+ result.add((Element) child);
+ }
+ child = child.getNextSibling();
+ }
+ return result;
+ }
+
+ private Element firstChildElement(Element parent, String localName) {
+ Node child = parent.getFirstChild();
+ while (child != null) {
+ if (child instanceof Element && localName.equals(localName(child))) {
+ return (Element) child;
+ }
+ child = child.getNextSibling();
+ }
+ return null;
+ }
+
+ private String localName(Node node) {
+ return node.getLocalName() == null ? node.getNodeName().replaceFirst("^.*:", "") : node.getLocalName();
+ }
+
+ private byte[] readAll(InputStream in) throws Exception {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ byte[] buffer = new byte[8192];
+ int length;
+ while ((length = in.read(buffer)) > 0) {
+ out.write(buffer, 0, length);
+ }
+ return out.toByteArray();
+ }
+
+ private interface ParagraphMatcher {
+ boolean matches(String text);
+ }
+}
--
Gitblit v1.9.1