| New file |
| | |
| | | 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; |
| | | } |
| | | } |