| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515 |
- 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<Integer> 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<Map<String, Object>> todayHourlyData = dataBoardMapper.selectHourlyChargePower(todayStart, todayEnd);
- Map<Integer, BigDecimal> todayMap = convertHourlyDataToMap(todayHourlyData);
- List<BigDecimal> 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<Map<String, Object>> compareHourlyData = dataBoardMapper.selectHourlyChargePower(compareDateStart, compareDateEnd);
- Map<Integer, BigDecimal> compareMap = convertHourlyDataToMap(compareHourlyData);
- List<BigDecimal> 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<String> labels = new ArrayList<>();
- List<BigDecimal> 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<Map<String, Object>> rawData = getDailyDataByType(dataType, startDateStr, endDateStr);
- // 转换数据
- Map<String, BigDecimal> dataMap = new LinkedHashMap<>();
- for (Map<String, Object> 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<Map<String, Object>> 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<Map<String, Object>> rawData = getDailyDataByType(dataType, startDateStr, endDateStr);
- BigDecimal total = BigDecimal.ZERO;
- for (Map<String, Object> 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<StationRankVO> hotStations = dataBoardMapper.selectHotStations(startDateStr, endDateStr, 5);
- // 设置排名
- for (int i = 0; i < hotStations.size(); i++) {
- hotStations.get(i).setRank(i + 1);
- }
- rankListVO.setHotStations(hotStations);
- // 计算波动充电站(基于充电度数波动)
- List<StationRankVO> 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<StationRankVO> calculateFluctuationStations(List<StationRankVO> hotStations,
- String prevStartDate, String prevEndDate,
- String currStartDate, String currEndDate) {
- List<StationRankVO> 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<Integer, BigDecimal> convertHourlyDataToMap(List<Map<String, Object>> hourlyData) {
- Map<Integer, BigDecimal> map = new HashMap<>();
- for (Map<String, Object> 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<ChurnUserExportDTO> 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<ChurnUserExportDTO> 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;
- }
- }
|