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 overview(int year, String holidayType) { List calendar = configService.findHolidayDays(year, holidayType); Map 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 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 importResult(HolidayWorkbookService.ImportSummary summary) { Map 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> buildOverviewRows(int year, String holidayType, List calendar) { Map> passengerByDate = loadPassengerByDate(year); Map> vehicleByDate = loadVehicleByDate(year); Map comparisonBaseByDate = loadComparisonBaseByDate(year); BigDecimal coefficient = configService.coefficientOf(year, HolidayConstants.CATEGORY_HOLIDAY); Map calendarByDay = new LinkedHashMap<>(); for (HolidayCalendar day : calendar) { if (day.getDayIndex() != null) calendarByDay.put(day.getDayIndex(), day); } List> 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 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 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> passengerByDate = loadPassengerByDate(year); Map> vehicleByDate = loadVehicleByDate(year); Map 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 calendar = configService.findHolidayDays(year, holidayType); Map calendarByDay = new LinkedHashMap<>(); for (HolidayCalendar day : calendar) { if (day.getDayIndex() != null) calendarByDay.put(day.getDayIndex(), day); } Map> passengerByDate = loadPassengerByDate(year); Map> vehicleByDate = loadVehicleByDate(year); Map 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> passengerByDate, Map> vehicleByDate, Map comparisonBaseByDate, BigDecimal coefficient) { Map passenger = passengerByDate.getOrDefault(date, Collections.emptyMap()); Map 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 calendarByDay, Map> passengerByDate, Map> vehicleByDate, Map 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> loadPassengerByDate(int year) { Map> result = new LinkedHashMap<>(); List rows = passengerFlowMapper.selectList( new LambdaQueryWrapper().eq(HolidayPassengerFlow::getYear, year)); for (HolidayPassengerFlow row : rows) { result.computeIfAbsent(row.getFlowDate(), key -> new LinkedHashMap<>()) .put(row.getTransportMode(), row.getPassengerCount()); } return result; } private Map> loadVehicleByDate(int year) { Map> result = new LinkedHashMap<>(); List rows = vehicleFlowMapper.selectList( new LambdaQueryWrapper().eq(HolidayVehicleFlow::getYear, year)); for (HolidayVehicleFlow row : rows) { result.computeIfAbsent(row.getFlowDate(), key -> new LinkedHashMap<>()) .put(row.getRoadCategory(), row.getVehicleCount()); } Map adjustments = loadNationalProvincialAdjustmentsByDate(year); for (Map.Entry entry : adjustments.entrySet()) { result.computeIfAbsent(entry.getKey(), key -> new LinkedHashMap<>()) .put(HolidayConstants.ROAD_NATIONAL_PROVINCIAL, entry.getValue()); } return result; } private Map loadNationalProvincialAdjustmentsByDate(int year) { Map result = new LinkedHashMap<>(); List rows = vehicleAdjustmentMapper.selectList( new LambdaQueryWrapper().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 loadComparisonBaseByDate(int year) { Map result = new LinkedHashMap<>(); List rows = vehicleFlowMapper.selectList( new LambdaQueryWrapper() .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 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; } }