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

diff --git a/traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayStatService.java b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayStatService.java
new file mode 100644
index 0000000..2309466
--- /dev/null
+++ b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayStatService.java
@@ -0,0 +1,369 @@
+package com.trafficaudit.holiday.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.trafficaudit.dataimport.entity.ImportBatch;
+import com.trafficaudit.dataimport.mapper.ImportBatchMapper;
+import com.trafficaudit.holiday.HolidayConstants;
+import com.trafficaudit.holiday.HolidayNumberUtils;
+import com.trafficaudit.holiday.entity.HolidayCalendar;
+import com.trafficaudit.holiday.entity.HolidayPassengerFlow;
+import com.trafficaudit.holiday.entity.HolidayVehicleFlow;
+import com.trafficaudit.holiday.entity.HolidayVehicleAdjustment;
+import com.trafficaudit.holiday.mapper.HolidayPassengerFlowMapper;
+import com.trafficaudit.holiday.mapper.HolidayVehicleAdjustmentMapper;
+import com.trafficaudit.holiday.mapper.HolidayVehicleFlowMapper;
+import lombok.Data;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.annotation.Resource;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+@Service
+public class HolidayStatService {
+
+    @Resource
+    private HolidayConfigService configService;
+    @Resource
+    private HolidayWorkbookService workbookService;
+    @Resource
+    private HolidayPassengerFlowMapper passengerFlowMapper;
+    @Resource
+    private HolidayVehicleFlowMapper vehicleFlowMapper;
+    @Resource
+    private HolidayVehicleAdjustmentMapper vehicleAdjustmentMapper;
+    @Resource
+    private ImportBatchMapper importBatchMapper;
+
+    public Map<String, Object> overview(int year, String holidayType) {
+        List<HolidayCalendar> calendar = configService.findHolidayDays(year, holidayType);
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("year", year);
+        result.put("holidayType", holidayType);
+        result.put("holidayTypeLabel", HolidayConstants.HOLIDAY_TYPE_LABELS.get(holidayType));
+        result.put("days", calendar);
+        result.put("rows", buildOverviewRows(year, holidayType, calendar));
+        result.put("templateFileName", workbookService.referenceFileName());
+        return result;
+    }
+
+    @Transactional
+    public Map<String, Object> importWorkbook(MultipartFile file, int year, String holidayType, boolean allowHistoricalOverwrite) {
+        HolidayWorkbookService.ImportSummary summary = workbookService.importWorkbook(file, year, holidayType, allowHistoricalOverwrite);
+        if (summary.isConfirmationRequired()) {
+            return importResult(summary);
+        }
+        ImportBatch batch = new ImportBatch();
+        batch.setFileName(file == null ? null : file.getOriginalFilename());
+        batch.setImportType("HOLIDAY_FLOW");
+        batch.setReportPeriod(year + "-" + holidayType);
+        batch.setTotalRows(summary.getSuccessRows() + summary.getFailRows());
+        batch.setSuccessRows(summary.getSuccessRows());
+        batch.setFailRows(summary.getFailRows());
+        batch.setErrorDetail(joinErrors(summary.getErrors()));
+        batch.setCreatedAt(LocalDateTime.now());
+        importBatchMapper.insert(batch);
+
+        return importResult(summary);
+    }
+
+    private Map<String, Object> importResult(HolidayWorkbookService.ImportSummary summary) {
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("passengerRows", summary.getPassengerRows());
+        result.put("vehicleRows", summary.getVehicleRows());
+        result.put("skippedRows", summary.getSkippedRows());
+        result.put("successRows", summary.getSuccessRows());
+        result.put("failRows", summary.getFailRows());
+        result.put("errors", summary.getErrors());
+        result.put("confirmationRequired", summary.isConfirmationRequired());
+        result.put("historicalChanges", summary.getHistoricalChanges());
+        return result;
+    }
+
+    public byte[] exportWorkbook(int year, String holidayType) {
+        return workbookService.exportWorkbook(year, holidayType);
+    }
+
+    public List<Map<String, Object>> buildOverviewRows(int year, String holidayType, List<HolidayCalendar> calendar) {
+        Map<LocalDate, Map<String, BigDecimal>> passengerByDate = loadPassengerByDate(year);
+        Map<LocalDate, Map<String, BigDecimal>> vehicleByDate = loadVehicleByDate(year);
+        Map<LocalDate, BigDecimal> comparisonBaseByDate = loadComparisonBaseByDate(year);
+        BigDecimal coefficient = configService.coefficientOf(year, HolidayConstants.CATEGORY_HOLIDAY);
+        Map<Integer, HolidayCalendar> calendarByDay = new LinkedHashMap<>();
+        for (HolidayCalendar day : calendar) {
+            if (day.getDayIndex() != null) calendarByDay.put(day.getDayIndex(), day);
+        }
+        List<Map<String, Object>> rows = new ArrayList<>();
+        for (HolidayCalendar day : calendar) {
+            DailyMetrics current = buildDailyMetrics(year, holidayType, day.getDayIndex(), day.getHolidayDate(),
+                    passengerByDate, vehicleByDate, comparisonBaseByDate, coefficient);
+            DailyMetrics ringRatioBase = ringRatioBaseFromMaps(year, holidayType, day.getDayIndex(), calendarByDay,
+                    passengerByDate, vehicleByDate, comparisonBaseByDate, coefficient);
+            BigDecimal nationalProvincialRingRatio = ratio(current.getNationalProvincial(),
+                    ringRatioBase == null ? null : ringRatioBase.getNationalProvincial());
+            Map<String, Object> row = new LinkedHashMap<>();
+            row.put("dayIndex", day.getDayIndex());
+            row.put("date", day.getHolidayDate());
+            row.put("rail", current.getRail());
+            row.put("civilAviation", current.getCivilAviation());
+            row.put("road", current.getRoad());
+            row.put("water", current.getWater());
+            row.put("expressway", current.getExpressway());
+            row.put("nationalProvincial", current.getNationalProvincial());
+            row.put("comparisonBase", current.getComparisonBase());
+            row.put("nationalProvincialRingRatio", nationalProvincialRingRatio);
+            row.put("coefficient", current.getCoefficient());
+            row.put("nonBusinessPersonFlow", current.getNonBusinessPersonFlow());
+            row.put("totalPersonFlow", current.getTotalPersonFlow());
+            rows.add(row);
+        }
+        return rows;
+    }
+
+    public DailyMetrics dailyMetrics(int year, String holidayType, int dayIndex) {
+        List<HolidayCalendar> calendar = configService.findHolidayDays(year, holidayType);
+        HolidayCalendar target = null;
+        for (HolidayCalendar day : calendar) {
+            if (day.getDayIndex() != null && day.getDayIndex() == dayIndex) {
+                target = day;
+                break;
+            }
+        }
+        if (target == null) return null;
+        return dailyMetrics(year, holidayType, dayIndex, target.getHolidayDate());
+    }
+
+    public DailyMetrics dailyMetricsByDate(LocalDate date) {
+        if (date == null) return null;
+        return dailyMetrics(date.getYear(), null, 0, date);
+    }
+
+    private DailyMetrics dailyMetrics(int year, String holidayType, int dayIndex, LocalDate date) {
+        Map<LocalDate, Map<String, BigDecimal>> passengerByDate = loadPassengerByDate(year);
+        Map<LocalDate, Map<String, BigDecimal>> vehicleByDate = loadVehicleByDate(year);
+        Map<LocalDate, BigDecimal> comparisonBaseByDate = loadComparisonBaseByDate(year);
+        BigDecimal coefficient = configService.coefficientOf(year, HolidayConstants.CATEGORY_HOLIDAY);
+        return buildDailyMetrics(year, holidayType, dayIndex, date, passengerByDate, vehicleByDate,
+                comparisonBaseByDate, coefficient);
+    }
+
+    public DailyMetrics ringRatioBaseMetrics(int year, String holidayType, int dayIndex) {
+        if (dayIndex <= 1) return null;
+        List<HolidayCalendar> calendar = configService.findHolidayDays(year, holidayType);
+        Map<Integer, HolidayCalendar> calendarByDay = new LinkedHashMap<>();
+        for (HolidayCalendar day : calendar) {
+            if (day.getDayIndex() != null) calendarByDay.put(day.getDayIndex(), day);
+        }
+        Map<LocalDate, Map<String, BigDecimal>> passengerByDate = loadPassengerByDate(year);
+        Map<LocalDate, Map<String, BigDecimal>> vehicleByDate = loadVehicleByDate(year);
+        Map<LocalDate, BigDecimal> comparisonBaseByDate = loadComparisonBaseByDate(year);
+        BigDecimal coefficient = configService.coefficientOf(year, HolidayConstants.CATEGORY_HOLIDAY);
+        return ringRatioBaseFromMaps(year, holidayType, dayIndex, calendarByDay, passengerByDate,
+                vehicleByDate, comparisonBaseByDate, coefficient);
+    }
+
+    private DailyMetrics buildDailyMetrics(int year, String holidayType, int dayIndex, LocalDate date,
+                                           Map<LocalDate, Map<String, BigDecimal>> passengerByDate,
+                                           Map<LocalDate, Map<String, BigDecimal>> vehicleByDate,
+                                           Map<LocalDate, BigDecimal> comparisonBaseByDate,
+                                           BigDecimal coefficient) {
+        Map<String, BigDecimal> passenger = passengerByDate.getOrDefault(date, Collections.emptyMap());
+        Map<String, BigDecimal> vehicle = vehicleByDate.getOrDefault(date, Collections.emptyMap());
+        BigDecimal expressway = vehicle.get(HolidayConstants.ROAD_EXPRESSWAY);
+        BigDecimal nationalProvincial = vehicle.get(HolidayConstants.ROAD_NATIONAL_PROVINCIAL);
+        BigDecimal comparisonBase = comparisonBaseByDate.get(date);
+        BigDecimal nonBusiness = nonBusinessPersonFlow(expressway, nationalProvincial, coefficient);
+
+        DailyMetrics metrics = new DailyMetrics();
+        metrics.setYear(year);
+        metrics.setHolidayType(holidayType);
+        metrics.setDayIndex(dayIndex);
+        metrics.setDate(date);
+        metrics.setRail(passenger.get(HolidayConstants.MODE_RAIL));
+        metrics.setCivilAviation(passenger.get(HolidayConstants.MODE_CIVIL_AVIATION));
+        metrics.setRoad(passenger.get(HolidayConstants.MODE_ROAD));
+        metrics.setWater(passenger.get(HolidayConstants.MODE_WATER));
+        metrics.setExpressway(expressway);
+        metrics.setNationalProvincial(nationalProvincial);
+        metrics.setComparisonBase(comparisonBase);
+        metrics.setCoefficient(coefficient);
+        metrics.setNonBusinessPersonFlow(nonBusiness);
+        recalculateDerived(metrics);
+        return metrics;
+    }
+
+    private DailyMetrics ringRatioBaseFromMaps(int year, String holidayType, int dayIndex,
+                                               Map<Integer, HolidayCalendar> calendarByDay,
+                                               Map<LocalDate, Map<String, BigDecimal>> passengerByDate,
+                                               Map<LocalDate, Map<String, BigDecimal>> vehicleByDate,
+                                               Map<LocalDate, BigDecimal> comparisonBaseByDate,
+                                               BigDecimal coefficient) {
+        if (dayIndex <= 1) return null;
+        HolidayCalendar previous = calendarByDay.get(dayIndex - 1);
+        if (previous == null || previous.getHolidayDate() == null) return null;
+        DailyMetrics base = buildDailyMetrics(year, holidayType, previous.getDayIndex(), previous.getHolidayDate(),
+                passengerByDate, vehicleByDate, comparisonBaseByDate, coefficient);
+        BigDecimal comparisonBase = comparisonBaseByDate.get(previous.getHolidayDate());
+        if (comparisonBase != null) {
+            base.setNationalProvincial(comparisonBase);
+            recalculateDerived(base);
+        }
+        return base;
+    }
+
+    private void recalculateDerived(DailyMetrics metrics) {
+        BigDecimal nonBusiness = nonBusinessPersonFlow(metrics.getExpressway(), metrics.getNationalProvincial(),
+                metrics.getCoefficient());
+        metrics.setNonBusinessPersonFlow(nonBusiness);
+        metrics.setRoadPersonFlow(sumNullable(metrics.getRoad(), nonBusiness));
+        metrics.setTotalPersonFlow(totalPersonFlow(nonBusiness, metrics.getRoad(), metrics.getRail(),
+                metrics.getCivilAviation(), metrics.getWater()));
+    }
+
+    private BigDecimal nonBusinessPersonFlow(BigDecimal expressway, BigDecimal nationalProvincial,
+                                            BigDecimal coefficient) {
+        if (coefficient == null || (expressway == null && nationalProvincial == null)) return null;
+        return HolidayNumberUtils.multiply(
+                HolidayNumberUtils.sumNullable(expressway, nationalProvincial), coefficient);
+    }
+
+    public DailyMetrics cumulativeMetrics(int year, String holidayType, int dayIndex) {
+        DailyMetrics cumulative = new DailyMetrics();
+        cumulative.setYear(year);
+        cumulative.setHolidayType(holidayType);
+        cumulative.setDayIndex(dayIndex);
+        for (int day = 1; day <= dayIndex; day++) {
+            DailyMetrics item = dailyMetrics(year, holidayType, day);
+            if (item == null) continue;
+            cumulative.setRail(sumNullable(cumulative.getRail(), item.getRail()));
+            cumulative.setCivilAviation(sumNullable(cumulative.getCivilAviation(), item.getCivilAviation()));
+            cumulative.setRoad(sumNullable(cumulative.getRoad(), item.getRoad()));
+            cumulative.setWater(sumNullable(cumulative.getWater(), item.getWater()));
+            cumulative.setExpressway(sumNullable(cumulative.getExpressway(), item.getExpressway()));
+            cumulative.setNationalProvincial(sumNullable(cumulative.getNationalProvincial(), item.getNationalProvincial()));
+            cumulative.setNonBusinessPersonFlow(sumNullable(cumulative.getNonBusinessPersonFlow(), item.getNonBusinessPersonFlow()));
+            cumulative.setRoadPersonFlow(sumNullable(cumulative.getRoadPersonFlow(), item.getRoadPersonFlow()));
+            cumulative.setTotalPersonFlow(sumNullable(cumulative.getTotalPersonFlow(), item.getTotalPersonFlow()));
+        }
+        return cumulative;
+    }
+
+
+    public DailyMetrics findMetricsByDate(LocalDate date) {
+        HolidayCalendar calendar = configService.findHolidayByDate(date);
+        if (calendar == null) return null;
+        return dailyMetrics(calendar.getYear(), calendar.getHolidayType(), calendar.getDayIndex());
+    }
+
+    private Map<LocalDate, Map<String, BigDecimal>> loadPassengerByDate(int year) {
+        Map<LocalDate, Map<String, BigDecimal>> result = new LinkedHashMap<>();
+        List<HolidayPassengerFlow> rows = passengerFlowMapper.selectList(
+                new LambdaQueryWrapper<HolidayPassengerFlow>().eq(HolidayPassengerFlow::getYear, year));
+        for (HolidayPassengerFlow row : rows) {
+            result.computeIfAbsent(row.getFlowDate(), key -> new LinkedHashMap<>())
+                    .put(row.getTransportMode(), row.getPassengerCount());
+        }
+        return result;
+    }
+
+    private Map<LocalDate, Map<String, BigDecimal>> loadVehicleByDate(int year) {
+        Map<LocalDate, Map<String, BigDecimal>> result = new LinkedHashMap<>();
+        List<HolidayVehicleFlow> rows = vehicleFlowMapper.selectList(
+                new LambdaQueryWrapper<HolidayVehicleFlow>().eq(HolidayVehicleFlow::getYear, year));
+        for (HolidayVehicleFlow row : rows) {
+            result.computeIfAbsent(row.getFlowDate(), key -> new LinkedHashMap<>())
+                    .put(row.getRoadCategory(), row.getVehicleCount());
+        }
+        Map<LocalDate, BigDecimal> adjustments = loadNationalProvincialAdjustmentsByDate(year);
+        for (Map.Entry<LocalDate, BigDecimal> entry : adjustments.entrySet()) {
+            result.computeIfAbsent(entry.getKey(), key -> new LinkedHashMap<>())
+                    .put(HolidayConstants.ROAD_NATIONAL_PROVINCIAL, entry.getValue());
+        }
+        return result;
+    }
+
+    private Map<LocalDate, BigDecimal> loadNationalProvincialAdjustmentsByDate(int year) {
+        Map<LocalDate, BigDecimal> result = new LinkedHashMap<>();
+        List<HolidayVehicleAdjustment> rows = vehicleAdjustmentMapper.selectList(
+                new LambdaQueryWrapper<HolidayVehicleAdjustment>().eq(HolidayVehicleAdjustment::getYear, year));
+        for (HolidayVehicleAdjustment row : rows) {
+            if (row.getFlowDate() != null && row.getAdjustedValue() != null) {
+                result.put(row.getFlowDate(), row.getAdjustedValue());
+            }
+        }
+        return result;
+    }
+
+    private Map<LocalDate, BigDecimal> loadComparisonBaseByDate(int year) {
+        Map<LocalDate, BigDecimal> result = new LinkedHashMap<>();
+        List<HolidayVehicleFlow> rows = vehicleFlowMapper.selectList(
+                new LambdaQueryWrapper<HolidayVehicleFlow>()
+                        .eq(HolidayVehicleFlow::getYear, year)
+                        .eq(HolidayVehicleFlow::getRoadCategory, HolidayConstants.ROAD_NATIONAL_PROVINCIAL));
+        for (HolidayVehicleFlow row : rows) {
+            if (row.getComparisonBase() != null) result.put(row.getFlowDate(), row.getComparisonBase());
+        }
+        return result;
+    }
+
+    private BigDecimal totalPersonFlow(BigDecimal nonBusinessPersonFlow, BigDecimal road, BigDecimal rail,
+                                       BigDecimal civilAviation, BigDecimal water) {
+        BigDecimal total = nonBusinessPersonFlow;
+        total = sumNullable(total, road);
+        total = sumNullable(total, rail);
+        total = sumNullable(total, civilAviation);
+        total = sumNullable(total, water);
+        return total;
+    }
+
+
+    private BigDecimal ratio(BigDecimal current, BigDecimal base) {
+        BigDecimal quotient = HolidayNumberUtils.divide(current, base);
+        return quotient == null ? null : HolidayNumberUtils.normalize(quotient.subtract(BigDecimal.ONE));
+    }
+
+    private BigDecimal sumNullable(BigDecimal left, BigDecimal right) {
+        return HolidayNumberUtils.sumNullable(left, right);
+    }
+
+    private String joinErrors(List<String> errors) {
+        if (errors == null || errors.isEmpty()) return null;
+        StringBuilder builder = new StringBuilder();
+        int limit = Math.min(errors.size(), 200);
+        for (int i = 0; i < limit; i++) {
+            if (i > 0) builder.append('\n');
+            builder.append(errors.get(i));
+        }
+        if (errors.size() > limit) {
+            builder.append("\n... ").append(errors.size() - limit).append(" more");
+        }
+        return builder.toString();
+    }
+
+    @Data
+    public static class DailyMetrics {
+        private int year;
+        private String holidayType;
+        private int dayIndex;
+        private LocalDate date;
+        private BigDecimal rail;
+        private BigDecimal civilAviation;
+        private BigDecimal road;
+        private BigDecimal water;
+        private BigDecimal expressway;
+        private BigDecimal nationalProvincial;
+        private BigDecimal comparisonBase;
+        private BigDecimal coefficient;
+        private BigDecimal nonBusinessPersonFlow;
+        private BigDecimal roadPersonFlow;
+        private BigDecimal totalPersonFlow;
+    }
+}

--
Gitblit v1.9.1