package com.zsElectric.boot.system.service.impl; import cn.hutool.core.util.StrUtil; import com.zsElectric.boot.system.mapper.DataBoardMapper; import com.zsElectric.boot.system.model.dto.ChurnUserExportDTO; import com.zsElectric.boot.system.model.query.HistoryBusinessQuery; import com.zsElectric.boot.system.model.query.StationRankQuery; import com.zsElectric.boot.system.model.vo.*; import com.zsElectric.boot.system.service.DataBoardService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.YearMonth; import java.time.format.DateTimeFormatter; import java.util.*; import java.util.stream.Collectors; import java.util.stream.IntStream; /** * 数据看板服务实现类 * * @author zsElectric * @since 2026-03-09 */ @Slf4j @Service @RequiredArgsConstructor public class DataBoardServiceImpl implements DataBoardService { private final DataBoardMapper dataBoardMapper; private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); private static final DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); private static final DateTimeFormatter MM_DD_FORMATTER = DateTimeFormatter.ofPattern("MM-dd"); @Override public DataBoardRealTimeVO getRealTimeData() { DataBoardRealTimeVO realTimeVO = dataBoardMapper.selectRealTimeData(); if (realTimeVO == null) { realTimeVO = new DataBoardRealTimeVO(); } // 获取累计退款金额 BigDecimal totalRefundAmount = dataBoardMapper.selectTotalRefundAmount(); realTimeVO.setTotalRefundAmount(totalRefundAmount != null ? totalRefundAmount : BigDecimal.ZERO); // 获取累计首单金额 BigDecimal totalFirstOrderAmount = dataBoardMapper.selectTotalFirstOrderAmount(); realTimeVO.setTotalFirstOrderAmount(totalFirstOrderAmount != null ? totalFirstOrderAmount : BigDecimal.ZERO); // 确保所有字段非空 ensureRealTimeDataNotNull(realTimeVO); return realTimeVO; } @Override public DataBoardTodayVO getTodayData() { LocalDate today = LocalDate.now(); String todayStart = today.atStartOfDay().format(DATETIME_FORMATTER); String todayEnd = today.atTime(23, 59, 59).format(DATETIME_FORMATTER); DataBoardTodayVO todayVO = dataBoardMapper.selectTodayData(todayStart, todayEnd); if (todayVO == null) { todayVO = new DataBoardTodayVO(); } // 获取今日退款金额 BigDecimal todayRefundAmount = dataBoardMapper.selectTodayRefundAmount(todayStart, todayEnd); todayVO.setTodayRefundAmount(todayRefundAmount != null ? todayRefundAmount : BigDecimal.ZERO); // 获取今日首单金额 BigDecimal todayFirstOrderAmount = dataBoardMapper.selectTodayFirstOrderAmount(todayStart, todayEnd); todayVO.setTodayFirstOrderAmount(todayFirstOrderAmount != null ? todayFirstOrderAmount : BigDecimal.ZERO); // 确保所有字段非空 ensureTodayDataNotNull(todayVO); return todayVO; } @Override public ChargePowerTrendVO getChargePowerTrend(String compareDate) { ChargePowerTrendVO trendVO = new ChargePowerTrendVO(); // 生成0-23小时列表 List hours = IntStream.rangeClosed(0, 23).boxed().collect(Collectors.toList()); trendVO.setHours(hours); // 今日数据 LocalDate today = LocalDate.now(); String todayStart = today.atStartOfDay().format(DATETIME_FORMATTER); String todayEnd = today.atTime(23, 59, 59).format(DATETIME_FORMATTER); List> todayHourlyData = dataBoardMapper.selectHourlyChargePower(todayStart, todayEnd); Map todayMap = convertHourlyDataToMap(todayHourlyData); List todayData = hours.stream() .map(hour -> todayMap.getOrDefault(hour, BigDecimal.ZERO)) .collect(Collectors.toList()); trendVO.setTodayData(todayData); // 对比日数据 LocalDate compareDateParsed; if (StrUtil.isNotBlank(compareDate)) { compareDateParsed = LocalDate.parse(compareDate, DATE_FORMATTER); } else { // 默认对比昨天 compareDateParsed = today.minusDays(1); } String compareDateStart = compareDateParsed.atStartOfDay().format(DATETIME_FORMATTER); String compareDateEnd = compareDateParsed.atTime(23, 59, 59).format(DATETIME_FORMATTER); List> compareHourlyData = dataBoardMapper.selectHourlyChargePower(compareDateStart, compareDateEnd); Map compareMap = convertHourlyDataToMap(compareHourlyData); List compareData = hours.stream() .map(hour -> compareMap.getOrDefault(hour, BigDecimal.ZERO)) .collect(Collectors.toList()); trendVO.setCompareData(compareData); trendVO.setCompareDate(compareDateParsed.format(DATE_FORMATTER)); return trendVO; } @Override public HistoryBusinessDataVO getHistoryBusinessData(HistoryBusinessQuery query) { HistoryBusinessDataVO dataVO = new HistoryBusinessDataVO(); dataVO.setDataType(query.getDataType()); dataVO.setTimeDimension(query.getTimeDimension()); LocalDate today = LocalDate.now(); List labels = new ArrayList<>(); List values = new ArrayList<>(); String dataType = query.getDataType(); if (StrUtil.isBlank(dataType)) { dataType = "chargePower"; } if ("month".equalsIgnoreCase(query.getTimeDimension())) { // 月趋势:yearMonth 格式为 yyyy,查询该年每月汇总数据 String yearStr = query.getYearMonth(); if (yearStr.contains("-")) { throw new IllegalArgumentException("月趋势时yearMonth格式应为yyyy(如2026),当前传入:" + yearStr); } int year = Integer.parseInt(yearStr); int currentYear = today.getYear(); int endMonth = (year == currentYear) ? today.getMonthValue() : 12; for (int month = 1; month <= endMonth; month++) { YearMonth ym = YearMonth.of(year, month); LocalDate monthStart = ym.atDay(1); LocalDate monthEnd = ym.atEndOfMonth(); // 如果是当月,截止到今天 if (year == currentYear && month == today.getMonthValue()) { monthEnd = today; } String startDateStr = monthStart.format(DATE_FORMATTER); String endDateStr = monthEnd.format(DATE_FORMATTER); BigDecimal monthValue = getMonthlyDataByType(dataType, startDateStr, endDateStr); labels.add(String.format("%02d", month)); values.add(monthValue); } } else { // 日趋势:yearMonth 格式为 yyyy-MM,查询该月每日数据 String yearMonthStr = query.getYearMonth(); if (!yearMonthStr.contains("-")) { throw new IllegalArgumentException("日趋势时yearMonth格式应为yyyy-MM(如2026-03),当前传入:" + yearMonthStr); } YearMonth yearMonth = YearMonth.parse(yearMonthStr); LocalDate startDate = yearMonth.atDay(1); LocalDate endDate; // 如果是当月则到今天 if (yearMonth.equals(YearMonth.from(today))) { endDate = today; } else { endDate = yearMonth.atEndOfMonth(); } String startDateStr = startDate.format(DATE_FORMATTER); String endDateStr = endDate.format(DATE_FORMATTER); List> rawData = getDailyDataByType(dataType, startDateStr, endDateStr); // 转换数据 Map dataMap = new LinkedHashMap<>(); for (Map item : rawData) { String label = String.valueOf(item.get("dateLabel")); BigDecimal value = item.get("value") != null ? new BigDecimal(String.valueOf(item.get("value"))) : BigDecimal.ZERO; dataMap.put(label, value); } // 填充所有日期 LocalDate current = startDate; DateTimeFormatter labelFormatter = DateTimeFormatter.ofPattern("MM.dd"); while (!current.isAfter(endDate)) { String label = current.format(labelFormatter); labels.add(label); values.add(dataMap.getOrDefault(label, BigDecimal.ZERO)); current = current.plusDays(1); } } dataVO.setLabels(labels); dataVO.setValues(values); return dataVO; } /** * 根据数据类型获取每日数据 */ private List> getDailyDataByType(String dataType, String startDateStr, String endDateStr) { switch (dataType) { case "chargeAmount": return dataBoardMapper.selectDailyChargeAmount(startDateStr, endDateStr); case "validOrders": return dataBoardMapper.selectDailyValidOrders(startDateStr, endDateStr); case "registerUsers": return dataBoardMapper.selectDailyRegisterUsers(startDateStr, endDateStr); case "chargePower": default: return dataBoardMapper.selectDailyChargePower(startDateStr, endDateStr); } } /** * 根据数据类型获取月度汇总数据 */ private BigDecimal getMonthlyDataByType(String dataType, String startDateStr, String endDateStr) { List> rawData = getDailyDataByType(dataType, startDateStr, endDateStr); BigDecimal total = BigDecimal.ZERO; for (Map item : rawData) { BigDecimal value = item.get("value") != null ? new BigDecimal(String.valueOf(item.get("value"))) : BigDecimal.ZERO; total = total.add(value); } return total; } @Override public StationRankListVO getStationRankData(StationRankQuery query) { StationRankListVO rankListVO = new StationRankListVO(); // 计算时间范围 LocalDate startDate; LocalDate endDate = LocalDate.now(); String timeRange = query.getTimeRange(); if (StrUtil.isBlank(timeRange)) { timeRange = "week"; } switch (timeRange) { case "today": startDate = endDate; break; case "month": startDate = endDate.minusDays(29); break; case "custom": if (StrUtil.isNotBlank(query.getStartDate()) && StrUtil.isNotBlank(query.getEndDate())) { startDate = LocalDate.parse(query.getStartDate(), DATE_FORMATTER); endDate = LocalDate.parse(query.getEndDate(), DATE_FORMATTER); } else { startDate = endDate.minusDays(6); } break; case "week": default: startDate = endDate.minusDays(6); break; } String startDateStr = startDate.format(DATE_FORMATTER); String endDateStr = endDate.format(DATE_FORMATTER); // 计算前一个周期的日期范围(用于波动计算) long daysDiff = java.time.temporal.ChronoUnit.DAYS.between(startDate, endDate) + 1; LocalDate prevStartDate = startDate.minusDays(daysDiff); LocalDate prevEndDate = startDate.minusDays(1); String prevStartDateStr = prevStartDate.format(DATE_FORMATTER); String prevEndDateStr = prevEndDate.format(DATE_FORMATTER); // 获取热门充电站 List hotStations = dataBoardMapper.selectHotStations(startDateStr, endDateStr, 5); // 设置排名 for (int i = 0; i < hotStations.size(); i++) { hotStations.get(i).setRank(i + 1); } rankListVO.setHotStations(hotStations); // 计算波动充电站(基于充电度数波动) List fluctuationStations = calculateFluctuationStations( hotStations, prevStartDateStr, prevEndDateStr, startDateStr, endDateStr); rankListVO.setFluctuationStations(fluctuationStations); // 设置日期范围描述 rankListVO.setDateRange(startDateStr + " 至 " + endDateStr); return rankListVO; } @Override public UserChurnRateVO getUserChurnRate() { UserChurnRateVO churnRateVO = new UserChurnRateVO(); LocalDate today = LocalDate.now(); LocalDate yesterday = today.minusDays(1); // 计算近7天流失率(近7天=昨天之前7天,含昨天,即 today-7 到 today-1) // 早期用户:7天之前有充电的用户(today-14 到 today-8) // 流失判断:这些用户在近7天内(today-7 到 today-1)没有充电 BigDecimal sevenDayRate = calculateChurnRate( today.minusDays(14), today.minusDays(8), today.minusDays(7), yesterday); churnRateVO.setSevenDayChurnRate(sevenDayRate); // 计算近1个月流失率(近1个月=昨天之前30天,含昨天,即 today-30 到 today-1) // 早期用户:30天之前有充电的用户(today-60 到 today-31) // 流失判断:这些用户在近30天内(today-30 到 today-1)没有充电 BigDecimal oneMonthRate = calculateChurnRate( today.minusDays(60), today.minusDays(31), today.minusDays(30), yesterday); churnRateVO.setOneMonthChurnRate(oneMonthRate); // 计算近3个月流失率(近3个月=昨天之前90天,含昨天,即 today-90 到 today-1) // 早期用户:90天之前有充电的用户(today-180 到 today-91) // 流失判断:这些用户在近90天内(today-90 到 today-1)没有充电 BigDecimal threeMonthRate = calculateChurnRate( today.minusDays(180), today.minusDays(91), today.minusDays(90), yesterday); churnRateVO.setThreeMonthChurnRate(threeMonthRate); return churnRateVO; } /** * 计算流失率 */ private BigDecimal calculateChurnRate(LocalDate earlyStart, LocalDate earlyEnd, LocalDate recentStart, LocalDate recentEnd) { String earlyStartStr = earlyStart.format(DATE_FORMATTER); String earlyEndStr = earlyEnd.format(DATE_FORMATTER); String recentStartStr = recentStart.format(DATE_FORMATTER); String recentEndStr = recentEnd.format(DATE_FORMATTER); Long activeCount = dataBoardMapper.selectActiveUserCount(earlyStartStr, earlyEndStr); if (activeCount == null || activeCount == 0) { return BigDecimal.ZERO; } Long churnCount = dataBoardMapper.selectChurnUserCount(earlyStartStr, earlyEndStr, recentStartStr, recentEndStr); if (churnCount == null) { churnCount = 0L; } return BigDecimal.valueOf(churnCount) .multiply(BigDecimal.valueOf(100)) .divide(BigDecimal.valueOf(activeCount), 2, RoundingMode.HALF_UP); } /** * 计算波动充电站 */ private List calculateFluctuationStations(List hotStations, String prevStartDate, String prevEndDate, String currStartDate, String currEndDate) { List fluctuationStations = new ArrayList<>(); for (StationRankVO station : hotStations) { if (station.getStationId() == null) { continue; } StationRankVO fluctuationStation = new StationRankVO(); fluctuationStation.setRank(station.getRank()); fluctuationStation.setStationId(station.getStationId()); fluctuationStation.setStationName(station.getStationName()); fluctuationStation.setChargePower(station.getChargePower()); // 获取前一周期的充电度数 BigDecimal prevPower = dataBoardMapper.selectStationChargePower( String.valueOf(station.getStationId()), prevStartDate, prevEndDate); if (prevPower == null) { prevPower = BigDecimal.ZERO; } BigDecimal currPower = station.getChargePower(); if (currPower == null) { currPower = BigDecimal.ZERO; } // 计算波动百分比 if (prevPower.compareTo(BigDecimal.ZERO) == 0) { if (currPower.compareTo(BigDecimal.ZERO) > 0) { fluctuationStation.setFluctuation(BigDecimal.valueOf(100)); fluctuationStation.setFluctuationDirection("up"); } else { fluctuationStation.setFluctuation(BigDecimal.ZERO); fluctuationStation.setFluctuationDirection("up"); } } else { BigDecimal diff = currPower.subtract(prevPower); BigDecimal fluctuation = diff.multiply(BigDecimal.valueOf(100)) .divide(prevPower, 2, RoundingMode.HALF_UP).abs(); fluctuationStation.setFluctuation(fluctuation); fluctuationStation.setFluctuationDirection(diff.compareTo(BigDecimal.ZERO) >= 0 ? "up" : "down"); } fluctuationStations.add(fluctuationStation); } // 按波动幅度排序 fluctuationStations.sort((a, b) -> b.getFluctuation().compareTo(a.getFluctuation())); // 重新设置排名 for (int i = 0; i < fluctuationStations.size(); i++) { fluctuationStations.get(i).setRank(i + 1); } return fluctuationStations; } /** * 将小时数据转换为Map */ private Map convertHourlyDataToMap(List> hourlyData) { Map map = new HashMap<>(); for (Map item : hourlyData) { Integer hour = ((Number) item.get("hour")).intValue(); BigDecimal power = item.get("chargePower") != null ? new BigDecimal(String.valueOf(item.get("chargePower"))) : BigDecimal.ZERO; map.put(hour, power); } return map; } /** * 确保实时数据所有字段非空 */ private void ensureRealTimeDataNotNull(DataBoardRealTimeVO vo) { if (vo.getTotalChargePower() == null) vo.setTotalChargePower(BigDecimal.ZERO); if (vo.getTotalChargeAmount() == null) vo.setTotalChargeAmount(BigDecimal.ZERO); if (vo.getTotalDiscountAmount() == null) vo.setTotalDiscountAmount(BigDecimal.ZERO); if (vo.getTotalServiceFeeAmount() == null) vo.setTotalServiceFeeAmount(BigDecimal.ZERO); if (vo.getTotalRefundAmount() == null) vo.setTotalRefundAmount(BigDecimal.ZERO); if (vo.getTotalActualPayAmount() == null) vo.setTotalActualPayAmount(BigDecimal.ZERO); if (vo.getTotalFirstOrderAmount() == null) vo.setTotalFirstOrderAmount(BigDecimal.ZERO); if (vo.getTotalCouponAmount() == null) vo.setTotalCouponAmount(BigDecimal.ZERO); if (vo.getTotalFirmDiscountAmount() == null) vo.setTotalFirmDiscountAmount(BigDecimal.ZERO); if (vo.getTotalCommissionAmount() == null) vo.setTotalCommissionAmount(BigDecimal.ZERO); } /** * 确保今日数据所有字段非空 */ private void ensureTodayDataNotNull(DataBoardTodayVO vo) { if (vo.getTodayChargePower() == null) vo.setTodayChargePower(BigDecimal.ZERO); if (vo.getTodayChargeAmount() == null) vo.setTodayChargeAmount(BigDecimal.ZERO); if (vo.getTodayDiscountAmount() == null) vo.setTodayDiscountAmount(BigDecimal.ZERO); if (vo.getTodayServiceFeeAmount() == null) vo.setTodayServiceFeeAmount(BigDecimal.ZERO); if (vo.getTodayRefundAmount() == null) vo.setTodayRefundAmount(BigDecimal.ZERO); if (vo.getTodayActualPayAmount() == null) vo.setTodayActualPayAmount(BigDecimal.ZERO); if (vo.getTodayFirstOrderAmount() == null) vo.setTodayFirstOrderAmount(BigDecimal.ZERO); if (vo.getTodayCouponAmount() == null) vo.setTodayCouponAmount(BigDecimal.ZERO); if (vo.getTodayFirmDiscountAmount() == null) vo.setTodayFirmDiscountAmount(BigDecimal.ZERO); if (vo.getTodayCommissionAmount() == null) vo.setTodayCommissionAmount(BigDecimal.ZERO); } @Override public List getChurnUserExportList(String periodType) { LocalDate today = LocalDate.now(); LocalDate yesterday = today.minusDays(1); LocalDate earlyStart; LocalDate earlyEnd; LocalDate recentStart; LocalDate recentEnd = yesterday; // 根据时段类型计算日期范围 switch (periodType) { case "7d": // 近7天流失用户 earlyStart = today.minusDays(14); earlyEnd = today.minusDays(8); recentStart = today.minusDays(7); break; case "3m": // 近3个月流失用户 earlyStart = today.minusDays(180); earlyEnd = today.minusDays(91); recentStart = today.minusDays(90); break; case "1m": default: // 近1个月流失用户(默认) earlyStart = today.minusDays(60); earlyEnd = today.minusDays(31); recentStart = today.minusDays(30); break; } List list = dataBoardMapper.selectChurnUserList( earlyStart.format(DATE_FORMATTER), earlyEnd.format(DATE_FORMATTER), recentStart.format(DATE_FORMATTER), recentEnd.format(DATE_FORMATTER) ); // 设置序号 for (int i = 0; i < list.size(); i++) { list.get(i).setRowNum(i + 1); } return list; } }