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<HolidayCalendar> holidayDays = configService.findHolidayDays(year, holidayType);
|
if (holidayDays.isEmpty()) {
|
throw new IllegalArgumentException("所选年份和节假日尚未配置日期,请先在系统配置中维护节假日");
|
}
|
List<String> dayLabels = dayLabels(holidayDays);
|
int dayCount = holidayDays.size();
|
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, dayCount));
|
}
|
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, dayLabels, 2, 1100, 715),
|
lineImageSize(5268595L, 3424555L)));
|
blocks.add(new HolidayTrendWordService.ChartBlock(
|
"“" + holidayLabel + "”假期累计跨区域人员流动量走势",
|
chartService.lineChart("万人次",
|
series(years, yearData, item -> item.totalCumulative), false, dayLabels, 2, 1100, 590),
|
lineImageSize(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, dayLabels, 2, 1100, 522),
|
lineImageSize(5232400L, 2484120L)));
|
blocks.add(new HolidayTrendWordService.ChartBlock(
|
"“" + holidayLabel + "”假期铁路客运量情况",
|
chartService.lineChart("万人次",
|
series(years, yearData, item -> item.rail), false, dayLabels, 2, 1100, 445),
|
lineImageSize(5264785L, 2129155L)));
|
blocks.add(new HolidayTrendWordService.ChartBlock(
|
"“" + holidayLabel + "”假期水路客运量情况",
|
chartService.lineChart("万人次",
|
series(years, yearData, item -> item.water), false, dayLabels, 2, 1100, 433),
|
lineImageSize(5274310L, 2075815L)));
|
blocks.add(new HolidayTrendWordService.ChartBlock(
|
"“" + holidayLabel + "”假期民航客运量情况",
|
chartService.lineChart("万人次",
|
series(years, yearData, item -> item.civilAviation), false, dayLabels, 2, 1100, 521),
|
lineImageSize(5271135L, 2496185L)));
|
blocks.add(new HolidayTrendWordService.ChartBlock(
|
"“" + holidayLabel + "”假期道路客运量情况",
|
chartService.lineChart("万人次",
|
series(years, yearData, item -> item.road), false, dayLabels, 2, 1100, 590),
|
lineImageSize(5268595L, 2821305L)));
|
|
blocks.add(new HolidayTrendWordService.SectionBlock("三、车流量走势"));
|
blocks.add(new HolidayTrendWordService.ChartBlock(
|
"“" + holidayLabel + "”假期高速公路车流量情况",
|
chartService.lineChart("万辆次",
|
series(years, yearData, item -> item.expressway), false, dayLabels, 2, 1100, 590),
|
lineImageSize(5268595L, 2821305L)));
|
blocks.add(new HolidayTrendWordService.ChartBlock(
|
"“" + holidayLabel + "”假期普通国省道车流量情况",
|
chartService.lineChart("万辆次",
|
series(years, yearData, item -> item.nationalProvincial), false, dayLabels, 2, 1100, 590),
|
lineImageSize(5268595L, 2821305L)));
|
blocks.add(new HolidayTrendWordService.ChartBlock(
|
"“" + holidayLabel + "”假期全省车流总量情况",
|
chartService.lineChart("万辆次",
|
series(years, yearData, item -> item.totalVehicleFlow), false, dayLabels, 2, 1100, 590),
|
lineImageSize(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("占比",
|
dayLabels,
|
dailyShareSeries(selected, dayCount), true, 1000, 607), true,
|
new HolidayTrendWordService.ImageSize(5106670L, 3097530L)));
|
|
blocks.add(new HolidayTrendWordService.SectionBlock("附录二 普通国省道观测站路网运行"));
|
ObservationData observation = observationData(holidayDays);
|
blocks.add(new HolidayTrendWordService.ChartBlock(
|
prefix + "普通国省道观测站平均机动车速度",
|
chartService.lineChart("公里/小时",
|
Collections.singletonList(series("平均速度", observation.speed)), false, dayLabels, 2, 1100, 590),
|
lineImageSize(5268595L, 2821305L)));
|
blocks.add(new HolidayTrendWordService.ChartBlock(
|
prefix + "普通国省道观测站平均拥挤度",
|
chartService.lineChart("拥挤度",
|
Collections.singletonList(series("平均拥挤度", observation.congestion)), false, dayLabels, 2, 1100, 590),
|
lineImageSize(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, int dayCount) {
|
List<DailyMetrics> metrics = new ArrayList<>();
|
for (int day = 1; day <= dayCount; 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, int dayCount) {
|
List<HolidayTrendChartService.ChartSeries> result = new ArrayList<>();
|
result.add(series("自驾", share(data.selfDrivingPersonFlow, data, dayCount)));
|
result.add(series("道路", share(data.road, data, dayCount)));
|
result.add(series("铁路", share(data.rail, data, dayCount)));
|
result.add(series("民航", share(data.civilAviation, data, dayCount)));
|
result.add(series("水路", share(data.water, data, dayCount)));
|
return result;
|
}
|
|
private List<BigDecimal> share(List<BigDecimal> values, YearData data, int dayCount) {
|
List<BigDecimal> result = new ArrayList<>();
|
for (int i = 0; i < dayCount; 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(List<HolidayCalendar> days) {
|
if (days.isEmpty()) {
|
return new ObservationData(nulls(0), nulls(0));
|
}
|
LocalDate start = days.get(0).getHolidayDate();
|
LocalDate end = days.get(days.size() - 1).getHolidayDate();
|
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 (HolidayCalendar day : days) {
|
ObservationDailyOperation row = byDate.get(day.getHolidayDate());
|
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<>();
|
int size = Math.max(current == null ? 0 : current.size(), previous == null ? 0 : previous.size());
|
for (int i = 0; i < 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() {
|
return nulls(HolidayTrendChartService.DAY_LABELS.size());
|
}
|
|
private List<BigDecimal> nulls(int size) {
|
List<BigDecimal> result = new ArrayList<>();
|
for (int i = 0; i < size; i++) result.add(null);
|
return result;
|
}
|
|
static List<String> dayLabels(List<HolidayCalendar> days) {
|
List<String> labels = new ArrayList<>();
|
if (days == null) return labels;
|
for (HolidayCalendar day : days) {
|
if (day == null || day.getHolidayDate() == null) continue;
|
labels.add(day.getHolidayDate().getMonthValue() + "月"
|
+ day.getHolidayDate().getDayOfMonth() + "日");
|
}
|
return labels;
|
}
|
|
private HolidayTrendWordService.ImageSize lineImageSize(long width, long height) {
|
return new HolidayTrendWordService.ImageSize(
|
width, HolidayTrendChartService.scaleLineChartHeight(height));
|
}
|
|
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;
|
}
|
}
|
}
|