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.mapper.HolidayPassengerFlowMapper; 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 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); List> rows = new ArrayList<>(); BigDecimal previousNationalProvincial = null; for (HolidayCalendar day : calendar) { Map passenger = passengerByDate.getOrDefault(day.getHolidayDate(), Collections.emptyMap()); Map vehicle = vehicleByDate.getOrDefault(day.getHolidayDate(), Collections.emptyMap()); BigDecimal expressway = vehicle.get(HolidayConstants.ROAD_EXPRESSWAY); BigDecimal nationalProvincial = vehicle.get(HolidayConstants.ROAD_NATIONAL_PROVINCIAL); BigDecimal comparisonBase = comparisonBaseByDate.get(day.getHolidayDate()); BigDecimal nationalProvincialRingRatio = nationalProvincialRingRatio( year, nationalProvincial, previousNationalProvincial, comparisonBase); BigDecimal nonBusinessPersonFlow = null; if (coefficient != null && (expressway != null || nationalProvincial != null)) { nonBusinessPersonFlow = HolidayNumberUtils.multiply( HolidayNumberUtils.sumNullable(expressway, nationalProvincial), coefficient); } Map row = new LinkedHashMap<>(); row.put("dayIndex", day.getDayIndex()); row.put("date", day.getHolidayDate()); row.put("rail", passenger.get(HolidayConstants.MODE_RAIL)); row.put("civilAviation", passenger.get(HolidayConstants.MODE_CIVIL_AVIATION)); row.put("road", passenger.get(HolidayConstants.MODE_ROAD)); row.put("water", passenger.get(HolidayConstants.MODE_WATER)); row.put("expressway", expressway); row.put("nationalProvincial", nationalProvincial); row.put("comparisonBase", comparisonBase); row.put("nationalProvincialRingRatio", nationalProvincialRingRatio); row.put("coefficient", coefficient); row.put("nonBusinessPersonFlow", nonBusinessPersonFlow); row.put("totalPersonFlow", totalPersonFlow(passenger, nonBusinessPersonFlow)); rows.add(row); previousNationalProvincial = nationalProvincial; } 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; Map> passengerByDate = loadPassengerByDate(year); Map> vehicleByDate = loadVehicleByDate(year); Map comparisonBaseByDate = loadComparisonBaseByDate(year); Map passenger = passengerByDate.getOrDefault(target.getHolidayDate(), Collections.emptyMap()); Map vehicle = vehicleByDate.getOrDefault(target.getHolidayDate(), Collections.emptyMap()); BigDecimal coefficient = configService.coefficientOf(year, HolidayConstants.CATEGORY_HOLIDAY); BigDecimal expressway = vehicle.get(HolidayConstants.ROAD_EXPRESSWAY); BigDecimal nationalProvincial = vehicle.get(HolidayConstants.ROAD_NATIONAL_PROVINCIAL); BigDecimal comparisonBase = comparisonBaseByDate.get(target.getHolidayDate()); BigDecimal nonBusiness = null; if (coefficient != null && (expressway != null || nationalProvincial != null)) { nonBusiness = HolidayNumberUtils.multiply( HolidayNumberUtils.sumNullable(expressway, nationalProvincial), coefficient); } DailyMetrics metrics = new DailyMetrics(); metrics.setYear(year); metrics.setHolidayType(holidayType); metrics.setDayIndex(dayIndex); metrics.setDate(target.getHolidayDate()); 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); metrics.setRoadPersonFlow(sumNullable(metrics.getRoad(), nonBusiness)); metrics.setTotalPersonFlow(totalPersonFlow(passenger, nonBusiness)); return metrics; } 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()); } 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(Map passenger, BigDecimal nonBusinessPersonFlow) { BigDecimal total = nonBusinessPersonFlow; total = sumNullable(total, passenger.get(HolidayConstants.MODE_ROAD)); total = sumNullable(total, passenger.get(HolidayConstants.MODE_RAIL)); total = sumNullable(total, passenger.get(HolidayConstants.MODE_CIVIL_AVIATION)); total = sumNullable(total, passenger.get(HolidayConstants.MODE_WATER)); return total; } private BigDecimal nationalProvincialRingRatio(int year, BigDecimal current, BigDecimal previous, BigDecimal comparisonBase) { if (current == null) return null; if (HolidayConstants.supportsComparisonBase(year) && comparisonBase != null) { return ratio(current, comparisonBase); } return previous == null ? null : ratio(current, previous); } 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; } }