From f329f504040f3608478c687a99aa36a3cf5720b7 Mon Sep 17 00:00:00 2001
From: xyc <jc_xiong@hotmail.com>
Date: 星期六, 03 十月 2026 23:45:04 +0800
Subject: [PATCH] feat(observation): 新增观测站分日调查数据导入与明细查看

---
 traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayBulletinService.java |  140 +++++++++++++++++++++++++++++++++++-----------
 1 files changed, 105 insertions(+), 35 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
index 1c686ee..f99c55d 100644
--- 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
@@ -1,6 +1,7 @@
 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;
@@ -10,6 +11,7 @@
 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;
@@ -22,12 +24,14 @@
 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;
@@ -43,6 +47,8 @@
     private String bulletinTemplatePath;
 
     private final HolidayStatService statService;
+    @Resource
+    private HolidayConfigService configService;
     private static final ThreadLocal<Document> CURRENT_DOCUMENT = new ThreadLocal<>();
 
     public HolidayBulletinService(HolidayStatService statService) {
@@ -56,27 +62,30 @@
         }
         DailyMetrics previousDay = current.getDayIndex() > 1
                 ? statService.dailyMetrics(current.getYear(), current.getHolidayType(), current.getDayIndex() - 1) : 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 cumulativeTotalYi = formatNumber(currentCumulative.getTotalPersonFlow() == null
-                ? null : HolidayNumberUtils.divide(currentCumulative.getTotalPersonFlow(), BigDecimal.valueOf(10000)));
         String ringRatioClause = previousDay == null ? "" : "锛岀幆姣�" + ratioPhrase(
                 current.getTotalPersonFlow(), previousDay.getTotalPersonFlow());
-        String paragraph1 = String.format("%d鏈�%d鏃ワ紙鍋囨湡绗�%d澶╋級锛屽叏鐪佸叏绀句細璺ㄥ尯鍩熶汉鍛樻祦鍔ㄩ噺%s涓囦汉娆�%s锛屾瘮2025骞村悓鏈�%s銆�",
+        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()));
-        String paragraph2 = String.format("鍋囨湡浠ユ潵锛屽叏鐪佸叏绀句細璺ㄥ尯鍩熶汉鍛樻祦鍔ㄩ噺绱%s浜夸汉娆★紝姣�2025骞村悓鏈�%s銆�",
-                cumulativeTotalYi,
-                ratioPhrase(currentCumulative.getTotalPersonFlow(),
-                        previousYearCumulative == null ? null : previousYearCumulative.getTotalPersonFlow()));
+                ratioPhrase(current.getTotalPersonFlow(), previousYear == null ? null : previousYear.getTotalPersonFlow()),
+                ratioPhrase(current.getTotalPersonFlow(), twoYearsAgo == null ? null : twoYearsAgo.getTotalPersonFlow()));
+        String paragraph2 = cumulativeParagraph(currentCumulative, previousYearCumulative, twoYearsAgoCumulative);
 
         File template = resolveTemplateFile();
         if (!template.exists() || !template.isFile()) {
@@ -94,7 +103,8 @@
             try {
                 replaceParagraph(document, text -> text.startsWith("10鏈�") && text.contains("鍋囨湡绗�"), paragraph1);
                 replaceParagraph(document, text -> text.startsWith("鍋囨湡浠ユ潵"), paragraph2);
-                fillTable(document, current, previousDay, previousYear, currentCumulative, previousYearCumulative);
+                fillTable(document, current, previousDay, previousCalendarDay, previousYear, twoYearsAgo,
+                        currentCumulative, previousYearCumulative, twoYearsAgoCumulative);
                 byte[] replacedXml = serializeDocument(document);
                 return replaceZipEntry(templateBytes, "word/document.xml", replacedXml);
             } finally {
@@ -106,64 +116,91 @@
     }
 
     private void fillTable(Document document, DailyMetrics current, DailyMetrics previousDay,
-                           DailyMetrics previousYear, DailyMetrics currentCumulative,
-                           DailyMetrics previousYearCumulative) {
+                           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()),
                 ratio(current.getNonBusinessPersonFlow(), previousYear == null ? null : previousYear.getNonBusinessPersonFlow()),
-                "鈥�",
+                ratio(current.getNonBusinessPersonFlow(), twoYearsAgo == null ? null : twoYearsAgo.getNonBusinessPersonFlow()),
                 number(currentCumulative.getNonBusinessPersonFlow()),
                 ratio(currentCumulative.getNonBusinessPersonFlow(),
                         previousYearCumulative == null ? null : previousYearCumulative.getNonBusinessPersonFlow()),
-                "鈥�"
+                ratio(currentCumulative.getNonBusinessPersonFlow(),
+                        twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.getNonBusinessPersonFlow())
         });
         writeTableRow(table, 3, new String[]{
                 number(current.getRoad()),
                 ratio(current.getRoad(), previousDay == null ? null : previousDay.getRoad()),
                 ratio(current.getRoad(), previousYear == null ? null : previousYear.getRoad()),
-                "鈥�",
+                ratio(current.getRoad(), twoYearsAgo == null ? null : twoYearsAgo.getRoad()),
                 number(currentCumulative.getRoad()),
                 ratio(currentCumulative.getRoad(), previousYearCumulative == null ? null : previousYearCumulative.getRoad()),
-                "鈥�"
+                ratio(currentCumulative.getRoad(), twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.getRoad())
         });
         writeTableRow(table, 4, new String[]{
                 number(current.getRoadPersonFlow()),
                 ratio(current.getRoadPersonFlow(), previousDay == null ? null : previousDay.getRoadPersonFlow()),
                 ratio(current.getRoadPersonFlow(), previousYear == null ? null : previousYear.getRoadPersonFlow()),
-                "鈥�",
+                ratio(current.getRoadPersonFlow(), twoYearsAgo == null ? null : twoYearsAgo.getRoadPersonFlow()),
                 number(currentCumulative.getRoadPersonFlow()),
-                ratio(currentCumulative.getRoadPersonFlow(), previousYearCumulative == null ? null : previousYearCumulative.getRoadPersonFlow()),
-                "鈥�"
+                ratio(currentCumulative.getRoadPersonFlow(),
+                        previousYearCumulative == null ? null : previousYearCumulative.getRoadPersonFlow()),
+                ratio(currentCumulative.getRoadPersonFlow(),
+                        twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.getRoadPersonFlow())
         });
-        writeTableRow(table, 5, transportRow(current.getRail(), previousDay == null ? null : previousDay.getRail(),
-                previousYear == null ? null : previousYear.getRail(), currentCumulative.getRail(),
-                previousYearCumulative == null ? null : previousYearCumulative.getRail()));
-        writeTableRow(table, 6, transportRow(current.getCivilAviation(), previousDay == null ? null : previousDay.getCivilAviation(),
-                previousYear == null ? null : previousYear.getCivilAviation(), currentCumulative.getCivilAviation(),
-                previousYearCumulative == null ? null : previousYearCumulative.getCivilAviation()));
-        writeTableRow(table, 7, transportRow(current.getWater(), previousDay == null ? null : previousDay.getWater(),
-                previousYear == null ? null : previousYear.getWater(), currentCumulative.getWater(),
-                previousYearCumulative == null ? null : previousYearCumulative.getWater()));
-        writeTableRow(table, 8, transportRow(current.getTotalPersonFlow(), previousDay == null ? null : previousDay.getTotalPersonFlow(),
-                previousYear == null ? null : previousYear.getTotalPersonFlow(), currentCumulative.getTotalPersonFlow(),
-                previousYearCumulative == null ? null : previousYearCumulative.getTotalPersonFlow()));
+        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()));
+        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()));
+        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()));
+        writeTableRow(table, 8, transportRow(current.getTotalPersonFlow(),
+                previousDay == null ? null : previousDay.getTotalPersonFlow(),
+                previousYear == null ? null : previousYear.getTotalPersonFlow(),
+                twoYearsAgo == null ? null : twoYearsAgo.getTotalPersonFlow(),
+                currentCumulative.getTotalPersonFlow(),
+                previousYearCumulative == null ? null : previousYearCumulative.getTotalPersonFlow(),
+                twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.getTotalPersonFlow()));
     }
 
     private String[] transportRow(BigDecimal current, BigDecimal previousDay, BigDecimal previousYear,
-                                  BigDecimal currentCumulative, BigDecimal previousYearCumulative) {
+                                  BigDecimal twoYearsAgo, BigDecimal currentCumulative,
+                                  BigDecimal previousYearCumulative, BigDecimal twoYearsAgoCumulative) {
         return new String[]{
                 number(current),
                 ratio(current, previousDay),
                 ratio(current, previousYear),
-                "鈥�",
+                ratio(current, twoYearsAgo),
                 number(currentCumulative),
                 ratio(currentCumulative, previousYearCumulative),
-                "鈥�"
+                ratio(currentCumulative, twoYearsAgoCumulative)
         };
     }
 
@@ -175,7 +212,7 @@
         int logicalColumn = 0;
         for (Element cell : cells) {
             int span = gridSpan(cell);
-            if (logicalColumn >= 2 && logicalColumn < 9) {
+            if (logicalColumn >= 2) {
                 int valueIndex = logicalColumn - 2;
                 if (valueIndex < values.length) {
                     setCellText(cell, values[valueIndex]);
@@ -212,7 +249,10 @@
             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);
@@ -290,9 +330,39 @@
         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(total),
+                ratioPhrase(total, previousYearCumulative == null ? null : previousYearCumulative.getTotalPersonFlow()),
+                ratioPhrase(total, twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.getTotalPersonFlow()));
+    }
+
     private String ratio(BigDecimal current, BigDecimal base) {
         if (current == null || base == null || base.abs().compareTo(EPSILON) < 0) return "鈥�";
-        return ratioPhrase(current, base);
+        BigDecimal ratio = HolidayNumberUtils.divide(current, base);
+        if (ratio == null) return "鈥�";
+        BigDecimal percent = HolidayNumberUtils.multiply(ratio.subtract(BigDecimal.ONE), BigDecimal.valueOf(100));
+        if (percent == null) return "鈥�";
+        BigDecimal rounded = percent.setScale(1, RoundingMode.HALF_UP);
+        return rounded.signum() == 0 ? "0%" : rounded.toPlainString() + "%";
     }
 
     private String number(BigDecimal value) {

--
Gitblit v1.9.1