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/HolidayTrendService.java |  445 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 445 insertions(+), 0 deletions(-)

diff --git a/traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayTrendService.java b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayTrendService.java
new file mode 100644
index 0000000..0e35898
--- /dev/null
+++ b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayTrendService.java
@@ -0,0 +1,445 @@
+package com.trafficaudit.holiday.service;
+
+import com.trafficaudit.holiday.HolidayConstants;
+import com.trafficaudit.holiday.HolidayNumberUtils;
+import com.trafficaudit.holiday.entity.HolidayCalendar;
+import com.trafficaudit.holiday.service.HolidayStatService.DailyMetrics;
+import com.trafficaudit.observation.dto.ObservationDailyOperation;
+import com.trafficaudit.observation.mapper.ObservationStationFlowMapper;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+
+@Service
+public class HolidayTrendService {
+
+    private static final BigDecimal HUNDRED = new BigDecimal("100");
+
+    @Value("${holiday.trend-template:docs/涓撻」缁熻/閲嶈鑺傚亣鏃�/2026骞村崄涓�鍋囨湡鍏ㄧ渷缁煎悎杩愯緭瀹㈡祦閲忚蛋鍔垮浘.docx}")
+    private String trendTemplatePath;
+
+    @Resource
+    private HolidayStatService statService;
+    @Resource
+    private HolidayConfigService configService;
+    @Resource
+    private HolidayTrendChartService chartService;
+    @Resource
+    private HolidayTrendWordService wordService;
+    @Resource
+    private ObservationStationFlowMapper observationStationFlowMapper;
+
+    public byte[] generate(int year, String holidayType) {
+        String holidayLabel = HolidayConstants.HOLIDAY_TYPE_LABELS.get(holidayType);
+        if (holidayLabel == null) {
+            throw new IllegalArgumentException("涓嶆敮鎸佺殑鑺傚亣鏃ョ被鍨�");
+        }
+        List<Integer> years = Arrays.asList(year, year - 1, year - 2);
+        Map<Integer, YearData> yearData = new LinkedHashMap<>();
+        for (Integer item : years) {
+            yearData.put(item, buildYearData(item, holidayType));
+        }
+        YearData selected = yearData.get(year);
+        if (selected == null || !selected.hasAnyData()) {
+            throw new IllegalArgumentException("鎵�閫夊勾浠芥病鏈夎冻澶熺殑璧板娍鍥炬暟鎹�");
+        }
+
+        String prefix = "鈥�" + holidayLabel + "鈥濆亣鏈�";
+        List<HolidayTrendWordService.DocumentBlock> blocks = new ArrayList<>();
+        blocks.add(new HolidayTrendWordService.SectionBlock("涓�銆佹�讳綋鎯呭喌"));
+        blocks.add(new HolidayTrendWordService.ChartBlock(
+                prefix + "鍏ㄧ渷璺ㄥ尯鍩熶汉鍛樻祦鍔ㄩ噺鎯呭喌",
+                chartService.lineChart("涓囦汉娆�",
+                        series(years, yearData, item -> item.totalDaily), false, 1100, 715),
+                new HolidayTrendWordService.ImageSize(5268595L, 3424555L)));
+        blocks.add(new HolidayTrendWordService.ChartBlock(
+                "鈥�" + holidayLabel + "鈥濆亣鏈熺疮璁¤法鍖哄煙浜哄憳娴佸姩閲忚蛋鍔�",
+                chartService.lineChart("涓囦汉娆�",
+                        series(years, yearData, item -> item.totalCumulative), false, 1100, 590),
+                new HolidayTrendWordService.ImageSize(5268595L, 2821305L)));
+        blocks.add(new HolidayTrendWordService.ChartBlock(
+                "鈥�" + holidayLabel + "鈥濆亣鏈熷悇浜ら�氳繍杈撴柟寮忓杩愰噺鍗犳瘮鎯呭喌",
+                chartService.pieChart("浜烘", selected.modalShare(), 1000, 607), true,
+                new HolidayTrendWordService.ImageSize(5106670L, 3097530L)));
+
+        blocks.add(new HolidayTrendWordService.SectionBlock("浜屻�佸悇绉嶈繍杈撴柟寮忓娴侀噺璧板娍"));
+        blocks.add(new HolidayTrendWordService.ChartBlock(
+                "鈥�" + holidayLabel + "鈥濆亣鏈熻嚜椹惧嚭琛岄噺鎯呭喌",
+                chartService.lineChart("涓囦汉娆�",
+                        series(years, yearData, item -> item.selfDrivingPersonFlow), false, 1100, 522),
+                new HolidayTrendWordService.ImageSize(5232400L, 2484120L)));
+        blocks.add(new HolidayTrendWordService.ChartBlock(
+                "鈥�" + holidayLabel + "鈥濆亣鏈熼搧璺杩愰噺鎯呭喌",
+                chartService.lineChart("涓囦汉娆�",
+                        series(years, yearData, item -> item.rail), false, 1100, 445),
+                new HolidayTrendWordService.ImageSize(5264785L, 2129155L)));
+        blocks.add(new HolidayTrendWordService.ChartBlock(
+                "鈥�" + holidayLabel + "鈥濆亣鏈熸按璺杩愰噺鎯呭喌",
+                chartService.lineChart("涓囦汉娆�",
+                        series(years, yearData, item -> item.water), false, 1100, 433),
+                new HolidayTrendWordService.ImageSize(5274310L, 2075815L)));
+        blocks.add(new HolidayTrendWordService.ChartBlock(
+                "鈥�" + holidayLabel + "鈥濆亣鏈熸皯鑸杩愰噺鎯呭喌",
+                chartService.lineChart("涓囦汉娆�",
+                        series(years, yearData, item -> item.civilAviation), false, 1100, 521),
+                new HolidayTrendWordService.ImageSize(5271135L, 2496185L)));
+        blocks.add(new HolidayTrendWordService.ChartBlock(
+                "鈥�" + holidayLabel + "鈥濆亣鏈熼亾璺杩愰噺鎯呭喌",
+                chartService.lineChart("涓囦汉娆�",
+                        series(years, yearData, item -> item.road), false, 1100, 590),
+                new HolidayTrendWordService.ImageSize(5268595L, 2821305L)));
+
+        blocks.add(new HolidayTrendWordService.SectionBlock("涓夈�佽溅娴侀噺璧板娍"));
+        blocks.add(new HolidayTrendWordService.ChartBlock(
+                "鈥�" + holidayLabel + "鈥濆亣鏈熼珮閫熷叕璺溅娴侀噺鎯呭喌",
+                chartService.lineChart("涓囪締娆�",
+                        series(years, yearData, item -> item.expressway), false, 1100, 590),
+                new HolidayTrendWordService.ImageSize(5268595L, 2821305L)));
+        blocks.add(new HolidayTrendWordService.ChartBlock(
+                "鈥�" + holidayLabel + "鈥濆亣鏈熸櫘閫氬浗鐪侀亾杞︽祦閲忔儏鍐�",
+                chartService.lineChart("涓囪締娆�",
+                        series(years, yearData, item -> item.nationalProvincial), false, 1100, 590),
+                new HolidayTrendWordService.ImageSize(5268595L, 2821305L)));
+        blocks.add(new HolidayTrendWordService.ChartBlock(
+                "鈥�" + holidayLabel + "鈥濆亣鏈熷叏鐪佽溅娴佹�婚噺鎯呭喌",
+                chartService.lineChart("涓囪締娆�",
+                        series(years, yearData, item -> item.totalVehicleFlow), false, 1100, 590),
+                new HolidayTrendWordService.ImageSize(5268595L, 2821305L)));
+        blocks.add(new HolidayTrendWordService.ChartBlock(
+                "鈥�" + holidayLabel + "鈥濆亣鏈熷叕璺嚭琛岀粨鏋�",
+                chartService.stackedBarChart("涓囦汉娆�",
+                        Arrays.asList((year - 2) + "骞�", (year - 1) + "骞�", year + "骞�"),
+                        roadStructureSeries(years, yearData), false, 1000, 607), true,
+                new HolidayTrendWordService.ImageSize(5106670L, 3097530L)));
+
+        blocks.add(new HolidayTrendWordService.SectionBlock("闄勫綍涓� 姣忔棩瀹㈡祦鏋勬垚鍙樺寲"));
+        blocks.add(new HolidayTrendWordService.ChartBlock(
+                "鈥�" + holidayLabel + "鈥濆亣鏈熸瘡鏃ュ娴佹瀯鎴愬彉鍖�",
+                chartService.stackedBarChart("鍗犳瘮",
+                        HolidayTrendChartService.DAY_LABELS,
+                        dailyShareSeries(selected), true, 1000, 607), true,
+                new HolidayTrendWordService.ImageSize(5106670L, 3097530L)));
+
+        blocks.add(new HolidayTrendWordService.SectionBlock("闄勫綍浜� 鏅�氬浗鐪侀亾瑙傛祴绔欒矾缃戣繍琛�"));
+        ObservationData observation = observationData(year, holidayType);
+        blocks.add(new HolidayTrendWordService.ChartBlock(
+                prefix + "鏅�氬浗鐪侀亾瑙傛祴绔欏钩鍧囨満鍔ㄨ溅閫熷害",
+                chartService.lineChart("鍏噷/灏忔椂",
+                        Collections.singletonList(series("骞冲潎閫熷害", observation.speed)), false, 1100, 590),
+                new HolidayTrendWordService.ImageSize(5268595L, 2821305L)));
+        blocks.add(new HolidayTrendWordService.ChartBlock(
+                prefix + "鏅�氬浗鐪侀亾瑙傛祴绔欏钩鍧囨嫢鎸ゅ害",
+                chartService.lineChart("鎷ユ尋搴�",
+                        Collections.singletonList(series("骞冲潎鎷ユ尋搴�", observation.congestion)), false, 1100, 590),
+                new HolidayTrendWordService.ImageSize(5268595L, 2821305L)));
+
+        File template = resolveTemplateFile();
+        if (!template.exists() || !template.isFile()) {
+            throw new IllegalArgumentException("璧板娍鍥炬ā鏉挎枃浠朵笉瀛樺湪: " + template.getAbsolutePath());
+        }
+        try (InputStream input = new FileInputStream(template)) {
+            return wordService.generate(readAll(input), year, blocks);
+        } catch (Exception e) {
+            throw new IllegalArgumentException("璧板娍鍥剧敓鎴愬け璐�: " + e.getMessage(), e);
+        }
+    }
+
+    private YearData buildYearData(int year, String holidayType) {
+        List<DailyMetrics> metrics = new ArrayList<>();
+        for (int day = 1; day <= HolidayTrendChartService.DAY_LABELS.size(); day++) {
+            metrics.add(statService.dailyMetrics(year, holidayType, day));
+        }
+        YearData data = new YearData();
+        data.year = year;
+        data.rail = values(metrics, DailyMetrics::getRail);
+        data.civilAviation = values(metrics, DailyMetrics::getCivilAviation);
+        data.road = values(metrics, DailyMetrics::getRoad);
+        data.water = values(metrics, DailyMetrics::getWater);
+        data.expressway = values(metrics, DailyMetrics::getExpressway);
+        data.nationalProvincial = values(metrics, DailyMetrics::getNationalProvincial);
+        data.selfDrivingPersonFlow = values(metrics, DailyMetrics::getNonBusinessPersonFlow);
+        data.selfDrivingVehicleFlow = combine(metrics,
+                DailyMetrics::getExpressway, DailyMetrics::getNationalProvincial);
+        data.totalVehicleFlow = combine(metrics,
+                DailyMetrics::getExpressway, DailyMetrics::getNationalProvincial);
+        data.totalDaily = values(metrics, DailyMetrics::getTotalPersonFlow);
+        data.totalCumulative = cumulative(data.totalDaily);
+        data.railCumulative = cumulative(data.rail);
+        data.civilAviationCumulative = cumulative(data.civilAviation);
+        data.roadCumulative = cumulative(data.road);
+        data.waterCumulative = cumulative(data.water);
+        data.selfDrivingPersonFlowCumulative = cumulative(data.selfDrivingPersonFlow);
+        return data;
+    }
+
+    private List<HolidayTrendChartService.ChartSeries> series(
+            List<Integer> years, Map<Integer, YearData> data,
+            Function<YearData, List<BigDecimal>> selector) {
+        List<HolidayTrendChartService.ChartSeries> result = new ArrayList<>();
+        for (Integer year : years) {
+            YearData item = data.get(year);
+            result.add(series(year + "骞�", selector.apply(item)));
+        }
+        return result;
+    }
+
+    private List<HolidayTrendChartService.ChartSeries> yoySeries(
+            String label, List<Integer> years, Map<Integer, YearData> data,
+            Function<YearData, List<BigDecimal>> selector) {
+        List<HolidayTrendChartService.ChartSeries> result = new ArrayList<>();
+        YearData selected = data.get(years.get(0));
+        List<BigDecimal> selectedValues = selector.apply(selected);
+        for (int i = 1; i < years.size(); i++) {
+            YearData comparison = data.get(years.get(i));
+            result.add(series(label + "鍚屾瘮" + comparison.year,
+                    percentageChanges(selectedValues, selector.apply(comparison))));
+        }
+        return result;
+    }
+
+    private List<HolidayTrendChartService.ChartSeries> dailyShareSeries(YearData data) {
+        List<HolidayTrendChartService.ChartSeries> result = new ArrayList<>();
+        result.add(series("鑷┚", share(data.selfDrivingPersonFlow, data)));
+        result.add(series("閬撹矾", share(data.road, data)));
+        result.add(series("閾佽矾", share(data.rail, data)));
+        result.add(series("姘戣埅", share(data.civilAviation, data)));
+        result.add(series("姘磋矾", share(data.water, data)));
+        return result;
+    }
+
+    private List<BigDecimal> share(List<BigDecimal> values, YearData data) {
+        List<BigDecimal> result = new ArrayList<>();
+        for (int i = 0; i < HolidayTrendChartService.DAY_LABELS.size(); i++) {
+            BigDecimal total = dayTotal(data, i);
+            BigDecimal value = valueAt(values, i);
+            result.add(total == null || total.signum() == 0
+                    ? null : HolidayNumberUtils.divide(value == null ? BigDecimal.ZERO : value, total));
+        }
+        return result;
+    }
+
+    private BigDecimal dayTotal(YearData data, int index) {
+        BigDecimal total = BigDecimal.ZERO;
+        boolean hasValue = false;
+        for (List<BigDecimal> values : Arrays.asList(
+                data.selfDrivingPersonFlow, data.road, data.rail,
+                data.civilAviation, data.water)) {
+            BigDecimal value = valueAt(values, index);
+            if (value != null) {
+                total = total.add(value);
+                hasValue = true;
+            }
+        }
+        return hasValue ? HolidayNumberUtils.normalize(total) : null;
+    }
+
+    private List<HolidayTrendChartService.ChartSeries> roadStructureSeries(
+            List<Integer> years, Map<Integer, YearData> data) {
+        List<BigDecimal> road = new ArrayList<>();
+        List<BigDecimal> selfDriving = new ArrayList<>();
+        for (int i = years.size() - 1; i >= 0; i--) {
+            YearData item = data.get(years.get(i));
+            road.add(sum(item.road));
+            selfDriving.add(sum(item.selfDrivingPersonFlow));
+        }
+        return Arrays.asList(
+                series("鑷┚鍑鸿閲�", selfDriving),
+                series("閬撹矾瀹㈣繍閲�", road));
+    }
+
+    private ObservationData observationData(int year, String holidayType) {
+        List<HolidayCalendar> days = configService.findHolidayDays(year, holidayType);
+        if (days.isEmpty()) {
+            return new ObservationData(nulls(), nulls());
+        }
+        LocalDate start = days.get(0).getHolidayDate();
+        LocalDate end = start.plusDays(HolidayTrendChartService.DAY_LABELS.size() - 1L);
+        List<ObservationDailyOperation> rows = observationStationFlowMapper.selectDailyOperations(start, end);
+        Map<LocalDate, ObservationDailyOperation> byDate = new LinkedHashMap<>();
+        for (ObservationDailyOperation row : rows) {
+            byDate.put(row.getObsDate(), row);
+        }
+        List<BigDecimal> speed = new ArrayList<>();
+        List<BigDecimal> congestion = new ArrayList<>();
+        for (int i = 0; i < HolidayTrendChartService.DAY_LABELS.size(); i++) {
+            ObservationDailyOperation row = byDate.get(start.plusDays(i));
+            speed.add(row == null ? null : row.getAverageSpeed());
+            congestion.add(row == null ? null : row.getAverageCongestion());
+        }
+        return new ObservationData(speed, congestion);
+    }
+
+    private HolidayTrendChartService.ChartSeries series(String name, List<BigDecimal> values) {
+        HolidayTrendChartService.ChartSeries result = new HolidayTrendChartService.ChartSeries();
+        result.setName(name);
+        result.setValues(values == null ? nulls() : values);
+        return result;
+    }
+
+    private List<BigDecimal> values(List<DailyMetrics> metrics, Function<DailyMetrics, BigDecimal> selector) {
+        List<BigDecimal> result = new ArrayList<>();
+        for (DailyMetrics item : metrics) {
+            result.add(item == null ? null : selector.apply(item));
+        }
+        return result;
+    }
+
+    private List<BigDecimal> combine(List<DailyMetrics> metrics,
+                                     Function<DailyMetrics, BigDecimal> left,
+                                     Function<DailyMetrics, BigDecimal> right) {
+        List<BigDecimal> result = new ArrayList<>();
+        for (DailyMetrics item : metrics) {
+            result.add(item == null ? null : HolidayNumberUtils.sumNullable(left.apply(item), right.apply(item)));
+        }
+        return result;
+    }
+
+    private List<BigDecimal> cumulative(List<BigDecimal> values) {
+        List<BigDecimal> result = new ArrayList<>();
+        BigDecimal total = null;
+        for (BigDecimal value : values) {
+            if (value != null) total = HolidayNumberUtils.sumNullable(total, value);
+            result.add(value == null ? null : total);
+        }
+        return result;
+    }
+
+    private List<BigDecimal> percentageChanges(List<BigDecimal> current, List<BigDecimal> previous) {
+        List<BigDecimal> result = new ArrayList<>();
+        for (int i = 0; i < HolidayTrendChartService.DAY_LABELS.size(); i++) {
+            BigDecimal currentValue = valueAt(current, i);
+            BigDecimal previousValue = valueAt(previous, i);
+            BigDecimal ratio = HolidayNumberUtils.divide(currentValue, previousValue);
+            result.add(ratio == null ? null : HolidayNumberUtils.multiply(
+                    ratio.subtract(BigDecimal.ONE), HUNDRED));
+        }
+        return result;
+    }
+
+    private BigDecimal sum(List<BigDecimal> values) {
+        BigDecimal total = null;
+        for (BigDecimal value : values) {
+            if (value != null) total = HolidayNumberUtils.sumNullable(total, value);
+        }
+        return total;
+    }
+
+    private BigDecimal valueAt(List<BigDecimal> values, int index) {
+        return values == null || index >= values.size() ? null : values.get(index);
+    }
+
+    private List<BigDecimal> nulls() {
+        List<BigDecimal> result = new ArrayList<>();
+        for (int i = 0; i < HolidayTrendChartService.DAY_LABELS.size(); i++) result.add(null);
+        return result;
+    }
+
+    private File resolveTemplateFile() {
+        List<File> candidates = new ArrayList<>();
+        File configured = new File(trendTemplatePath);
+        candidates.add(configured);
+        if (!configured.isAbsolute()) {
+            candidates.add(new File(System.getProperty("user.dir"), trendTemplatePath));
+            candidates.add(new File(System.getProperty("user.dir"), "../" + trendTemplatePath));
+        }
+        for (File candidate : candidates) {
+            try {
+                if (candidate.exists() && candidate.isFile()) return candidate.getCanonicalFile();
+            } catch (Exception ignored) {
+                // Keep checking.
+            }
+        }
+        return configured;
+    }
+
+    private byte[] readAll(InputStream input) throws Exception {
+        java.io.ByteArrayOutputStream output = new java.io.ByteArrayOutputStream();
+        byte[] buffer = new byte[8192];
+        int length;
+        while ((length = input.read(buffer)) > 0) {
+            output.write(buffer, 0, length);
+        }
+        return output.toByteArray();
+    }
+
+    private static class YearData {
+        private int year;
+        private List<BigDecimal> rail;
+        private List<BigDecimal> civilAviation;
+        private List<BigDecimal> road;
+        private List<BigDecimal> water;
+        private List<BigDecimal> expressway;
+        private List<BigDecimal> nationalProvincial;
+        private List<BigDecimal> selfDrivingPersonFlow;
+        private List<BigDecimal> selfDrivingVehicleFlow;
+        private List<BigDecimal> totalVehicleFlow;
+        private List<BigDecimal> totalDaily;
+        private List<BigDecimal> totalCumulative;
+        private List<BigDecimal> railCumulative;
+        private List<BigDecimal> civilAviationCumulative;
+        private List<BigDecimal> roadCumulative;
+        private List<BigDecimal> waterCumulative;
+        private List<BigDecimal> selfDrivingPersonFlowCumulative;
+
+        private boolean hasAnyData() {
+            return hasValue(totalDaily) || hasValue(rail) || hasValue(road)
+                    || hasValue(expressway) || hasValue(nationalProvincial);
+        }
+
+        private boolean hasValue(List<BigDecimal> values) {
+            if (values == null) return false;
+            for (BigDecimal value : values) {
+                if (value != null) return true;
+            }
+            return false;
+        }
+
+        private List<HolidayTrendChartService.ChartCategory> modalShare() {
+            List<HolidayTrendChartService.ChartCategory> result = new ArrayList<>();
+            result.add(category("鑷┚", sum(selfDrivingPersonFlow)));
+            result.add(category("閬撹矾", sum(road)));
+            result.add(category("閾佽矾", sum(rail)));
+            result.add(category("姘戣埅", sum(civilAviation)));
+            result.add(category("姘磋矾", sum(water)));
+            return result;
+        }
+
+        private BigDecimal sum(List<BigDecimal> values) {
+            BigDecimal total = null;
+            for (BigDecimal value : values) {
+                if (value != null) total = HolidayNumberUtils.sumNullable(total, value);
+            }
+            return total;
+        }
+
+        private HolidayTrendChartService.ChartCategory category(String name, BigDecimal value) {
+            HolidayTrendChartService.ChartCategory category = new HolidayTrendChartService.ChartCategory();
+            category.setName(name);
+            category.setValue(value == null ? BigDecimal.ZERO : value);
+            return category;
+        }
+    }
+
+    private static class ObservationData {
+        private final List<BigDecimal> speed;
+        private final List<BigDecimal> congestion;
+
+        private ObservationData(List<BigDecimal> speed, List<BigDecimal> congestion) {
+            this.speed = speed;
+            this.congestion = congestion;
+        }
+    }
+}

--
Gitblit v1.9.1