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 years = Arrays.asList(year, year - 1, year - 2); Map 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 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, 3, 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 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 series( List years, Map data, Function> selector) { List result = new ArrayList<>(); for (Integer year : years) { YearData item = data.get(year); result.add(series(year + "年", selector.apply(item))); } return result; } private List yoySeries( String label, List years, Map data, Function> selector) { List result = new ArrayList<>(); YearData selected = data.get(years.get(0)); List 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 dailyShareSeries(YearData data) { List 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 share(List values, YearData data) { List 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 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 roadStructureSeries( List years, Map data) { List road = new ArrayList<>(); List 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 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 rows = observationStationFlowMapper.selectDailyOperations(start, end); Map byDate = new LinkedHashMap<>(); for (ObservationDailyOperation row : rows) { byDate.put(row.getObsDate(), row); } List speed = new ArrayList<>(); List 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 values) { HolidayTrendChartService.ChartSeries result = new HolidayTrendChartService.ChartSeries(); result.setName(name); result.setValues(values == null ? nulls() : values); return result; } private List values(List metrics, Function selector) { List result = new ArrayList<>(); for (DailyMetrics item : metrics) { result.add(item == null ? null : selector.apply(item)); } return result; } private List combine(List metrics, Function left, Function right) { List result = new ArrayList<>(); for (DailyMetrics item : metrics) { result.add(item == null ? null : HolidayNumberUtils.sumNullable(left.apply(item), right.apply(item))); } return result; } private List cumulative(List values) { List 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 percentageChanges(List current, List previous) { List 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 values) { BigDecimal total = null; for (BigDecimal value : values) { if (value != null) total = HolidayNumberUtils.sumNullable(total, value); } return total; } private BigDecimal valueAt(List values, int index) { return values == null || index >= values.size() ? null : values.get(index); } private List nulls() { List result = new ArrayList<>(); for (int i = 0; i < HolidayTrendChartService.DAY_LABELS.size(); i++) result.add(null); return result; } private File resolveTemplateFile() { List 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 rail; private List civilAviation; private List road; private List water; private List expressway; private List nationalProvincial; private List selfDrivingPersonFlow; private List selfDrivingVehicleFlow; private List totalVehicleFlow; private List totalDaily; private List totalCumulative; private List railCumulative; private List civilAviationCumulative; private List roadCumulative; private List waterCumulative; private List selfDrivingPersonFlowCumulative; private boolean hasAnyData() { return hasValue(totalDaily) || hasValue(rail) || hasValue(road) || hasValue(expressway) || hasValue(nationalProvincial); } private boolean hasValue(List values) { if (values == null) return false; for (BigDecimal value : values) { if (value != null) return true; } return false; } private List modalShare() { List 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 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 speed; private final List congestion; private ObservationData(List speed, List congestion) { this.speed = speed; this.congestion = congestion; } } }