|
@@ -1,9 +1,15 @@
|
|
|
package com.pig4cloud.pig.statistics.service.impl;
|
|
|
|
|
|
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
|
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
|
import com.pig4cloud.pig.statistics.api.dto.user.*;
|
|
|
+import com.pig4cloud.pig.statistics.api.entity.MktStatUserAnalysis;
|
|
|
+import com.pig4cloud.pig.statistics.api.entity.MktStatUserRetention;
|
|
|
import com.pig4cloud.pig.statistics.api.vo.user.*;
|
|
|
+import com.pig4cloud.pig.statistics.mapper.MktStatUserAnalysisMapper;
|
|
|
+import com.pig4cloud.pig.statistics.mapper.MktStatUserRetentionMapper;
|
|
|
import com.pig4cloud.pig.statistics.service.UserAnalyseService;
|
|
|
import lombok.AllArgsConstructor;
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
@@ -16,11 +22,13 @@ import java.time.LocalDate;
|
|
|
import java.time.LocalDateTime;
|
|
|
import java.time.LocalTime;
|
|
|
import java.time.format.DateTimeFormatter;
|
|
|
+import java.time.format.DateTimeParseException;
|
|
|
import java.time.temporal.ChronoUnit;
|
|
|
-import java.util.ArrayList;
|
|
|
-import java.util.Arrays;
|
|
|
-import java.util.List;
|
|
|
-import java.util.Random;
|
|
|
+import java.time.temporal.TemporalAdjusters;
|
|
|
+import java.util.*;
|
|
|
+import java.util.stream.Collectors;
|
|
|
+
|
|
|
+import static com.baomidou.mybatisplus.core.toolkit.Wrappers.lambdaQuery;
|
|
|
|
|
|
/**
|
|
|
* @author: lwh
|
|
@@ -32,103 +40,47 @@ import java.util.Random;
|
|
|
@AllArgsConstructor
|
|
|
public class UserAnalyseServiceImpl implements UserAnalyseService {
|
|
|
|
|
|
+ private final MktStatUserAnalysisMapper userAnalysisMapper;
|
|
|
+
|
|
|
|
|
|
+ private final MktStatUserRetentionMapper userRetentionMapper;
|
|
|
/**
|
|
|
* 获取新增趋势
|
|
|
* @param reqDto 请求参数
|
|
|
* @return GetNewUserTrendVO
|
|
|
*/
|
|
|
@Override
|
|
|
- public GetNewUserTrendVO getNewUserTrend(GetNewUserTrendDTO reqDto) {
|
|
|
+ public GetNewUserTrendVO getNewUserTrend(UserAnalyseQueryBaseDTO reqDto) {
|
|
|
GetNewUserTrendVO result = new GetNewUserTrendVO();
|
|
|
- List<String> dates = new ArrayList<>();
|
|
|
- String timeUnit = reqDto.getTimeUnit();
|
|
|
- LocalDate fromDate = reqDto.getFromDate();
|
|
|
- LocalDate toDate = reqDto.getToDate();
|
|
|
-
|
|
|
- // 生成日期列表
|
|
|
- switch (timeUnit) {
|
|
|
- case "hour":
|
|
|
- DateTimeFormatter hourFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:00");
|
|
|
- LocalDateTime hourStart = fromDate.atStartOfDay();
|
|
|
- LocalDateTime hourEnd = toDate.atTime(23, 0);
|
|
|
- while (!hourStart.isAfter(hourEnd)) {
|
|
|
- dates.add(hourStart.format(hourFormatter));
|
|
|
- hourStart = hourStart.plusHours(1);
|
|
|
- }
|
|
|
- break;
|
|
|
- case "day":
|
|
|
- DateTimeFormatter dayFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
|
|
- LocalDate dayStart = fromDate;
|
|
|
- while (!dayStart.isAfter(toDate)) {
|
|
|
- dates.add(dayStart.format(dayFormatter));
|
|
|
- dayStart = dayStart.plusDays(1);
|
|
|
- }
|
|
|
- break;
|
|
|
- case "week":
|
|
|
- DateTimeFormatter weekFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
|
|
- // 计算起始日期后的第一个周日
|
|
|
- LocalDate currentWeek = fromDate;
|
|
|
- DayOfWeek dayOfWeek = currentWeek.getDayOfWeek();
|
|
|
- int daysToSunday = 7 - dayOfWeek.getValue(); // 周日的DayOfWeek值为7
|
|
|
- LocalDate firstSunday = currentWeek.plusDays(daysToSunday);
|
|
|
-
|
|
|
- while (!firstSunday.isAfter(toDate)) {
|
|
|
- dates.add(firstSunday.format(weekFormatter));
|
|
|
- firstSunday = firstSunday.plusWeeks(1);
|
|
|
- }
|
|
|
- break;
|
|
|
- case "month":
|
|
|
- DateTimeFormatter monthFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
|
|
- // 计算范围内的每月1号
|
|
|
- LocalDate currentMonthFirstDay = LocalDate.of(fromDate.getYear(), fromDate.getMonth(), 1);
|
|
|
- // 如果当月1号在起始日期之前,则取下个月1号
|
|
|
- if (currentMonthFirstDay.isBefore(fromDate)) {
|
|
|
- currentMonthFirstDay = currentMonthFirstDay.plusMonths(1);
|
|
|
- }
|
|
|
-
|
|
|
- while (!currentMonthFirstDay.isAfter(toDate)) {
|
|
|
- dates.add(currentMonthFirstDay.format(monthFormatter));
|
|
|
- currentMonthFirstDay = currentMonthFirstDay.plusMonths(1);
|
|
|
- }
|
|
|
- break;
|
|
|
- default:
|
|
|
- // 默认按天处理
|
|
|
- DateTimeFormatter defaultFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
|
|
- LocalDate defaultStart = fromDate;
|
|
|
- while (!defaultStart.isAfter(toDate)) {
|
|
|
- dates.add(defaultStart.format(defaultFormatter));
|
|
|
- defaultStart = defaultStart.plusDays(1);
|
|
|
- }
|
|
|
- }
|
|
|
+ // 1. 生成时间轴
|
|
|
+ List<String> dates = generateTimeAxis(reqDto.getFromDate(), reqDto.getToDate(), reqDto.getTimeUnit());
|
|
|
+ result.setDates(dates);
|
|
|
|
|
|
- // 生成items数据
|
|
|
- List<TrendBaseVO> items = new ArrayList<>();
|
|
|
+ // 2. 处理版本列表(为空时按"All"处理)
|
|
|
List<String> versions = reqDto.getVersion();
|
|
|
- // 若未指定版本,使用默认版本列表
|
|
|
- if (versions == null || versions.isEmpty()) {
|
|
|
- versions = Arrays.asList("1.0", "5.1");
|
|
|
- }
|
|
|
-
|
|
|
- Random random = new Random();
|
|
|
- for (String version : versions) {
|
|
|
- TrendBaseVO trend = new TrendBaseVO();
|
|
|
- trend.setName("新增用户");
|
|
|
- trend.setVersion(version);
|
|
|
- trend.setKey("newUser");
|
|
|
-
|
|
|
- // 生成与日期数量匹配的随机数据
|
|
|
- List<Integer> data = new ArrayList<>(dates.size());
|
|
|
- for (int i = 0; i < dates.size(); i++) {
|
|
|
- // 生成100-1000之间的随机数,模拟用户数量
|
|
|
- data.add(random.nextInt(901) + 100);
|
|
|
- }
|
|
|
- trend.setData(data);
|
|
|
- items.add(trend);
|
|
|
- }
|
|
|
-
|
|
|
- result.setDates(dates);
|
|
|
- result.setItems(items);
|
|
|
+ List<String> processVersions = (versions == null || versions.isEmpty())
|
|
|
+ ? Collections.singletonList("All")
|
|
|
+ : versions.stream().filter(v -> v != null && !v.isEmpty()).toList();
|
|
|
+
|
|
|
+ // 3. 处理每个版本的数据
|
|
|
+ List<TrendBaseVO> voList = processVersions.stream()
|
|
|
+ .map(version -> {
|
|
|
+ TrendBaseVO trend = new TrendBaseVO();
|
|
|
+ trend.setName("新增用户");
|
|
|
+ trend.setVersion(version);
|
|
|
+ trend.setKey("newUser");
|
|
|
+
|
|
|
+ // 计算每个时间点的新增用户数
|
|
|
+ List<Integer> counts = dates.stream()
|
|
|
+ .map(date -> calculateNewUserCount(date, version, reqDto, "newUser"))
|
|
|
+ .collect(Collectors.toList());
|
|
|
+
|
|
|
+ trend.setData(counts);
|
|
|
+ return trend;
|
|
|
+ })
|
|
|
+ .collect(Collectors.toList());
|
|
|
+
|
|
|
+ result.setItems(voList);
|
|
|
return result;
|
|
|
}
|
|
|
|
|
@@ -139,107 +91,69 @@ public class UserAnalyseServiceImpl implements UserAnalyseService {
|
|
|
*/
|
|
|
@Override
|
|
|
public Page<PageNewUserTrendDetailVO> pageNewUserTrendDetail(PageNewUserTrendDetailDTO reqDto) {
|
|
|
- // 创建分页对象
|
|
|
+ // 1. 初始化分页对象并计算总条数
|
|
|
Page<PageNewUserTrendDetailVO> page = new Page<>(reqDto.getCurrent(), reqDto.getSize());
|
|
|
- List<PageNewUserTrendDetailVO> result = new ArrayList<>();
|
|
|
- List<String> timeLabels = new ArrayList<>();
|
|
|
- String timeUnit = reqDto.getTimeUnit();
|
|
|
- LocalDate startDate = reqDto.getFromDate();
|
|
|
- LocalDate endDate = reqDto.getToDate();
|
|
|
- Random random = new Random();
|
|
|
+ page.setTotal(calculateTotalCount(reqDto.getFromDate(), reqDto.getToDate(), reqDto.getTimeUnit()));
|
|
|
|
|
|
- // 1. 根据时间单位生成对应的时间标签列表
|
|
|
- switch (timeUnit) {
|
|
|
- case "hour":
|
|
|
- // 按小时生成:2025-08-05 23:00
|
|
|
- LocalDateTime startHour = startDate.atStartOfDay();
|
|
|
- LocalDateTime endHour = endDate.atTime(23, 0);
|
|
|
- LocalDateTime currentHour = startHour;
|
|
|
- DateTimeFormatter hourFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
|
|
|
-
|
|
|
- while (!currentHour.isAfter(endHour)) {
|
|
|
- timeLabels.add(currentHour.format(hourFormatter));
|
|
|
- currentHour = currentHour.plusHours(1);
|
|
|
- }
|
|
|
- break;
|
|
|
-
|
|
|
- case "day":
|
|
|
- // 按天生成:2025-08-03
|
|
|
- LocalDate currentDay = startDate;
|
|
|
- DateTimeFormatter dayFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
|
|
-
|
|
|
- while (!currentDay.isAfter(endDate)) {
|
|
|
- timeLabels.add(currentDay.format(dayFormatter));
|
|
|
- currentDay = currentDay.plusDays(1);
|
|
|
- }
|
|
|
- break;
|
|
|
-
|
|
|
- case "week":
|
|
|
- // 按周生成:2025/07/27~2025/08/02
|
|
|
- LocalDate currentWeekStart = startDate;
|
|
|
- DateTimeFormatter weekFormatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");
|
|
|
-
|
|
|
- while (!currentWeekStart.isAfter(endDate)) {
|
|
|
- LocalDate currentWeekEnd = currentWeekStart.plusDays(6);
|
|
|
- if (currentWeekEnd.isAfter(endDate)) {
|
|
|
- currentWeekEnd = endDate;
|
|
|
- }
|
|
|
- timeLabels.add(currentWeekStart.format(weekFormatter) + "~" + currentWeekEnd.format(weekFormatter));
|
|
|
- currentWeekStart = currentWeekStart.plusWeeks(1);
|
|
|
- }
|
|
|
- break;
|
|
|
-
|
|
|
- case "month":
|
|
|
- // 按月生成:2025/07/01~2025/07/31
|
|
|
- LocalDate currentMonthStart = startDate;
|
|
|
- DateTimeFormatter monthFormatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");
|
|
|
-
|
|
|
- while (!currentMonthStart.isAfter(endDate)) {
|
|
|
- LocalDate currentMonthEnd = currentMonthStart.plusMonths(1).minusDays(1);
|
|
|
- if (currentMonthEnd.isAfter(endDate)) {
|
|
|
- currentMonthEnd = endDate;
|
|
|
- }
|
|
|
- timeLabels.add(currentMonthStart.withDayOfMonth(1).format(monthFormatter) + "~" + currentMonthEnd.format(monthFormatter));
|
|
|
- currentMonthStart = currentMonthStart.plusMonths(1);
|
|
|
- }
|
|
|
- break;
|
|
|
-
|
|
|
- default:
|
|
|
- throw new IllegalArgumentException("不支持的时间单位: " + timeUnit);
|
|
|
+ // 2. 生成当前页时间轴
|
|
|
+ List<String> dates = generatePageTimeAxis(reqDto.getFromDate(), reqDto.getToDate(),
|
|
|
+ reqDto.getTimeUnit(), reqDto.getCurrent(), reqDto.getSize());
|
|
|
+ if (dates.isEmpty()) {
|
|
|
+ return page; // 无数据直接返回
|
|
|
}
|
|
|
|
|
|
- // 2. 计算总记录数并设置到分页对象
|
|
|
- long total = timeLabels.size();
|
|
|
- page.setTotal(total);
|
|
|
-
|
|
|
- // 3. 计算分页范围
|
|
|
- long startIndex = (reqDto.getCurrent() - 1) * reqDto.getSize();
|
|
|
- long endIndex = Math.min(startIndex + reqDto.getSize(), total);
|
|
|
-
|
|
|
- // 4. 生成当前页的模拟数据
|
|
|
- if (startIndex < total) {
|
|
|
- double baseRate = 0.05; // 基础占比基数(5%)
|
|
|
-
|
|
|
- for (long i = startIndex; i < endIndex; i++) {
|
|
|
- PageNewUserTrendDetailVO vo = new PageNewUserTrendDetailVO();
|
|
|
- vo.setDate(timeLabels.get((int) i));
|
|
|
-
|
|
|
- // 生成新增用户数(随时间增长并带有随机波动)
|
|
|
- int baseUsers = 100 + (int) (i * 5);
|
|
|
- int randomUsers = random.nextInt(60) - 20; // 随机波动(-20至39)
|
|
|
- vo.setNewUser(Math.max(10, baseUsers + randomUsers));
|
|
|
-
|
|
|
- // 生成新增用户占比(保留两位小数)
|
|
|
- double rateFluctuation = (random.nextDouble() - 0.5) * 0.03;
|
|
|
- double userRate = baseRate + rateFluctuation;
|
|
|
- vo.setNewUserRate(Math.round(userRate * 100) / 100.0);
|
|
|
-
|
|
|
- result.add(vo);
|
|
|
- }
|
|
|
+ // 3. 定义变量
|
|
|
+ DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");
|
|
|
+ List<PageNewUserTrendDetailVO> detailVOS = new ArrayList<>(dates.size());
|
|
|
+ String timeUnit = reqDto.getTimeUnit();
|
|
|
+ boolean isRangeType = "week".equals(timeUnit) || "month".equals(timeUnit);
|
|
|
+
|
|
|
+ for (String date : dates) {
|
|
|
+ // 4. 获取时间范围
|
|
|
+ LocalDateTime startTime = getStartTime(date, timeUnit);
|
|
|
+ LocalDateTime endTime = getEndTime(date, timeUnit);
|
|
|
+
|
|
|
+ // 5. 构建查询条件
|
|
|
+ LambdaQueryWrapper<MktStatUserAnalysis> queryWrapper = lambdaQuery(MktStatUserAnalysis.class)
|
|
|
+ .in(reqDto.getVersion() != null && !reqDto.getVersion().isEmpty(),
|
|
|
+ MktStatUserAnalysis::getVersion, reqDto.getVersion())
|
|
|
+ .in(reqDto.getChannel() != null && !reqDto.getChannel().isEmpty(),
|
|
|
+ MktStatUserAnalysis::getChannel, reqDto.getChannel())
|
|
|
+ .ge(MktStatUserAnalysis::getStatDate, startTime)
|
|
|
+ .lt(MktStatUserAnalysis::getStatDate, endTime)
|
|
|
+ .select(MktStatUserAnalysis::getNewUser, MktStatUserAnalysis::getActiveUser);
|
|
|
+
|
|
|
+ // 6. 计算汇总数据
|
|
|
+ List<MktStatUserAnalysis> userAnalyses = userAnalysisMapper.selectList(queryWrapper);
|
|
|
+ BigDecimal[] totals = userAnalyses.stream()
|
|
|
+ .reduce(new BigDecimal[]{BigDecimal.ZERO, BigDecimal.ZERO},
|
|
|
+ (acc, analysis) -> {
|
|
|
+ acc[0] = acc[0].add(BigDecimal.valueOf(analysis.getNewUser() == null ? 0 : analysis.getNewUser()));
|
|
|
+ acc[1] = acc[1].add(BigDecimal.valueOf(analysis.getActiveUser() == null ? 0 : analysis.getActiveUser()));
|
|
|
+ return acc;
|
|
|
+ }, (a, b) -> a); // 并行流合并
|
|
|
+ BigDecimal totalNewUser = totals[0];
|
|
|
+ BigDecimal totalActiveUser = totals[1];
|
|
|
+
|
|
|
+ // 7. 构建返回对象
|
|
|
+ PageNewUserTrendDetailVO detailVO = new PageNewUserTrendDetailVO();
|
|
|
+ // 处理日期显示
|
|
|
+ String displayDate = isRangeType && startTime != null && endTime != null
|
|
|
+ ? startTime.toLocalDate().format(dateFormatter) + "~" + endTime.minusDays(1).toLocalDate().format(dateFormatter)
|
|
|
+ : date;
|
|
|
+ detailVO.setDate(displayDate);
|
|
|
+ detailVO.setNewUser(totalNewUser.intValue());
|
|
|
+
|
|
|
+ // 计算新用户占比
|
|
|
+ BigDecimal newUserRate = totalNewUser.compareTo(BigDecimal.ZERO) == 0 || totalActiveUser.compareTo(BigDecimal.ZERO) == 0
|
|
|
+ ? BigDecimal.ZERO
|
|
|
+ : totalNewUser.divide(totalActiveUser, 4, RoundingMode.HALF_UP);
|
|
|
+ detailVO.setNewUserRate(newUserRate);
|
|
|
+
|
|
|
+ detailVOS.add(detailVO);
|
|
|
}
|
|
|
|
|
|
- // 5. 设置分页数据并返回
|
|
|
- page.setRecords(result);
|
|
|
+ page.setRecords(detailVOS);
|
|
|
return page;
|
|
|
}
|
|
|
|
|
@@ -250,28 +164,40 @@ public class UserAnalyseServiceImpl implements UserAnalyseService {
|
|
|
*/
|
|
|
@Override
|
|
|
public GetNewUserRetentionVO getNewUserRetention(GetNewUserRetentionDTO reqDto) {
|
|
|
- GetNewUserRetentionVO vo = new GetNewUserRetentionVO();
|
|
|
- List<String> dates = new ArrayList<>();
|
|
|
- List<Double> counts = new ArrayList<>();
|
|
|
-
|
|
|
- // 计算日期范围并生成模拟数据
|
|
|
- LocalDate start = reqDto.getFromDate();
|
|
|
- LocalDate end = reqDto.getToDate();
|
|
|
- long days = ChronoUnit.DAYS.between(start, end) + 1;
|
|
|
-
|
|
|
- Random random = new Random();
|
|
|
- for (int i = 0; i < days; i++) {
|
|
|
- dates.add(start.plusDays(i).toString());
|
|
|
- // 生成5%-30%之间的留存率,保留两位小数
|
|
|
- double rate = 5 + random.nextDouble() * 25;
|
|
|
- // 使用BigDecimal处理小数位数
|
|
|
- BigDecimal bd = new BigDecimal(rate).setScale(2, RoundingMode.HALF_UP);
|
|
|
- counts.add(bd.doubleValue());
|
|
|
+ String timeUnit = "day";
|
|
|
+ GetNewUserRetentionVO result = new GetNewUserRetentionVO();
|
|
|
+ List<String> dates = generateTimeAxis(reqDto.getFromDate(), reqDto.getToDate(), timeUnit);
|
|
|
+ ArrayList<BigDecimal> retentions = new ArrayList<>();
|
|
|
+ for (String date : dates) {
|
|
|
+ LocalDateTime startTime = getStartTime(date, timeUnit);
|
|
|
+ LocalDateTime endTime = getEndTime(date, timeUnit);
|
|
|
+ LambdaQueryWrapper<MktStatUserRetention> queryWrapper = Wrappers.<MktStatUserRetention>lambdaQuery()
|
|
|
+ .in(reqDto.getVersion() != null && !reqDto.getVersion().isEmpty(), MktStatUserRetention::getVersion, reqDto.getVersion())
|
|
|
+ .in(reqDto.getChannel() != null && !reqDto.getChannel().isEmpty(), MktStatUserRetention::getChannel, reqDto.getChannel())
|
|
|
+ .ge(MktStatUserRetention::getStatDate, startTime)
|
|
|
+ .lt(MktStatUserRetention::getStatDate, endTime)
|
|
|
+ .select(MktStatUserRetention::getRetention,MktStatUserRetention::getNewUser);
|
|
|
+ List<MktStatUserRetention> userRetentions = userRetentionMapper.selectList(queryWrapper);
|
|
|
+ // 计算留存率
|
|
|
+ BigDecimal totalRetentionUsers = BigDecimal.ZERO;
|
|
|
+ int totalUsers = 0;
|
|
|
+ for (MktStatUserRetention userRetention : userRetentions) {
|
|
|
+ totalUsers+=userRetention.getNewUser();
|
|
|
+ // 计算当前版本留存用户数(总用户数 × 留存率),用BigDecimal保证精度
|
|
|
+ BigDecimal retentionUsers = BigDecimal.valueOf(userRetention.getNewUser())
|
|
|
+ .multiply(userRetention.getRetention());
|
|
|
+ totalRetentionUsers = totalRetentionUsers.add(retentionUsers);
|
|
|
+ }
|
|
|
+ if (totalUsers == 0){
|
|
|
+ retentions.add(BigDecimal.ZERO);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ BigDecimal retention = totalRetentionUsers.divide(BigDecimal.valueOf(totalUsers), 4, RoundingMode.HALF_UP);
|
|
|
+ retentions.add(retention);
|
|
|
}
|
|
|
-
|
|
|
- vo.setDates(dates);
|
|
|
- vo.setRetention(counts);
|
|
|
- return vo;
|
|
|
+ result.setDates(dates);
|
|
|
+ result.setRetentions(retentions);
|
|
|
+ return result;
|
|
|
}
|
|
|
|
|
|
/**
|
|
@@ -281,54 +207,65 @@ public class UserAnalyseServiceImpl implements UserAnalyseService {
|
|
|
*/
|
|
|
@Override
|
|
|
public Page<PageRetentionDetailVO> pageNewUserRetentionDetail(PageRetentionDetailDTO reqDto) {
|
|
|
- // 创建分页对象
|
|
|
+ // 1. 初始化分页对象并设置总条数
|
|
|
+ final String timeUnit = "day";
|
|
|
Page<PageRetentionDetailVO> page = new Page<>(reqDto.getCurrent(), reqDto.getSize());
|
|
|
- List<PageRetentionDetailVO> result = new ArrayList<>();
|
|
|
+ page.setTotal(calculateTotalCount(reqDto.getFromDate(), reqDto.getToDate(), timeUnit));
|
|
|
|
|
|
- // 1. 生成日期列表(根据时间范围)
|
|
|
- List<String> dates = new ArrayList<>();
|
|
|
- LocalDate currentDate = reqDto.getFromDate();
|
|
|
- LocalDate endDate = reqDto.getToDate();
|
|
|
-
|
|
|
- // 格式化日期的工具
|
|
|
- DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
|
|
-
|
|
|
- // 生成从开始日期到结束日期的所有日期
|
|
|
- while (!currentDate.isAfter(endDate)) {
|
|
|
- dates.add(currentDate.format(dateFormatter));
|
|
|
- currentDate = currentDate.plusDays(1);
|
|
|
+ // 2. 生成当前页时间轴,无数据直接返回
|
|
|
+ List<String> dates = generatePageTimeAxis(
|
|
|
+ reqDto.getFromDate(), reqDto.getToDate(), timeUnit,
|
|
|
+ reqDto.getCurrent(), reqDto.getSize()
|
|
|
+ );
|
|
|
+ if (dates.isEmpty()) {
|
|
|
+ return page;
|
|
|
}
|
|
|
|
|
|
- // 2. 设置总记录数
|
|
|
- long total = dates.size();
|
|
|
- page.setTotal(total);
|
|
|
-
|
|
|
- // 3. 计算分页范围
|
|
|
- long startIndex = (reqDto.getCurrent() - 1) * reqDto.getSize();
|
|
|
- long endIndex = Math.min(startIndex + reqDto.getSize(), total);
|
|
|
-
|
|
|
- // 4. 生成当前页的模拟数据
|
|
|
- if (startIndex < total) {
|
|
|
- Random random = new Random();
|
|
|
-
|
|
|
- for (long i = startIndex; i < endIndex; i++) {
|
|
|
- PageRetentionDetailVO vo = new PageRetentionDetailVO();
|
|
|
-
|
|
|
- // 设置日期
|
|
|
- vo.setDate(dates.get((int) i));
|
|
|
-
|
|
|
- // 生成留存率(5%-30%之间的随机数,保留两位小数)
|
|
|
- double baseRate = 5 + random.nextDouble() * 25; // 5% ~ 30%
|
|
|
- // 使用四舍五入保留两位小数
|
|
|
- double retention = Math.round(baseRate * 100) / 100.0;
|
|
|
- vo.setRetention(retention);
|
|
|
-
|
|
|
- result.add(vo);
|
|
|
+ // 3. 提取版本和渠道参数
|
|
|
+ List<String> versions = reqDto.getVersion();
|
|
|
+ List<String> channels = reqDto.getChannel();
|
|
|
+ List<PageRetentionDetailVO> retentions = new ArrayList<>(dates.size());
|
|
|
+
|
|
|
+ // 4. 循环处理每个日期
|
|
|
+ for (String date : dates) {
|
|
|
+ // 获取时间范围
|
|
|
+ LocalDateTime startTime = getStartTime(date, timeUnit);
|
|
|
+ LocalDateTime endTime = getEndTime(date, timeUnit);
|
|
|
+
|
|
|
+ // 构建查询条件
|
|
|
+ LambdaQueryWrapper<MktStatUserRetention> queryWrapper = lambdaQuery(MktStatUserRetention.class)
|
|
|
+ .in(versions != null && !versions.isEmpty(), MktStatUserRetention::getVersion, versions)
|
|
|
+ .in(channels != null && !channels.isEmpty(), MktStatUserRetention::getChannel, channels)
|
|
|
+ .ge(MktStatUserRetention::getStatDate, startTime)
|
|
|
+ .lt(MktStatUserRetention::getStatDate, endTime)
|
|
|
+ .select(MktStatUserRetention::getRetention, MktStatUserRetention::getNewUser);
|
|
|
+
|
|
|
+ // 查询数据
|
|
|
+ List<MktStatUserRetention> userRetentions = userRetentionMapper.selectList(queryWrapper);
|
|
|
+
|
|
|
+ // 5. 计算总用户数和总留存用户数
|
|
|
+ int totalUsers = 0;
|
|
|
+ BigDecimal totalRetentionUsers = BigDecimal.ZERO;
|
|
|
+ for (MktStatUserRetention retention : userRetentions) {
|
|
|
+ int newUser = retention.getNewUser();
|
|
|
+ totalUsers += newUser;
|
|
|
+ // 直接累加留存用户数
|
|
|
+ totalRetentionUsers = totalRetentionUsers.add(
|
|
|
+ BigDecimal.valueOf(newUser).multiply(retention.getRetention())
|
|
|
+ );
|
|
|
}
|
|
|
+
|
|
|
+ // 6. 构建返回VO
|
|
|
+ PageRetentionDetailVO detailVO = new PageRetentionDetailVO();
|
|
|
+ detailVO.setDate(date);
|
|
|
+ detailVO.setRetention(totalUsers == 0
|
|
|
+ ? BigDecimal.ZERO
|
|
|
+ : totalRetentionUsers.divide(BigDecimal.valueOf(totalUsers), 4, RoundingMode.HALF_UP)
|
|
|
+ );
|
|
|
+ retentions.add(detailVO);
|
|
|
}
|
|
|
|
|
|
- // 5. 设置分页数据
|
|
|
- page.setRecords(result);
|
|
|
+ page.setRecords(retentions);
|
|
|
return page;
|
|
|
}
|
|
|
|
|
@@ -338,96 +275,38 @@ public class UserAnalyseServiceImpl implements UserAnalyseService {
|
|
|
* @return GetNewUserTrendVO
|
|
|
*/
|
|
|
@Override
|
|
|
+
|
|
|
public GetNewUserTrendVO getActiveTrend(UserAnalyseQueryBaseDTO reqDto) {
|
|
|
GetNewUserTrendVO result = new GetNewUserTrendVO();
|
|
|
- List<String> dates = new ArrayList<>();
|
|
|
- String timeUnit = reqDto.getTimeUnit();
|
|
|
- LocalDate fromDate = reqDto.getFromDate();
|
|
|
- LocalDate toDate = reqDto.getToDate();
|
|
|
-
|
|
|
- // 生成日期列表
|
|
|
- switch (timeUnit) {
|
|
|
- case "hour":
|
|
|
- DateTimeFormatter hourFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:00");
|
|
|
- LocalDateTime hourStart = fromDate.atStartOfDay();
|
|
|
- LocalDateTime hourEnd = toDate.atTime(23, 0);
|
|
|
- while (!hourStart.isAfter(hourEnd)) {
|
|
|
- dates.add(hourStart.format(hourFormatter));
|
|
|
- hourStart = hourStart.plusHours(1);
|
|
|
- }
|
|
|
- break;
|
|
|
- case "day":
|
|
|
- DateTimeFormatter dayFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
|
|
- LocalDate dayStart = fromDate;
|
|
|
- while (!dayStart.isAfter(toDate)) {
|
|
|
- dates.add(dayStart.format(dayFormatter));
|
|
|
- dayStart = dayStart.plusDays(1);
|
|
|
- }
|
|
|
- break;
|
|
|
- case "week":
|
|
|
- DateTimeFormatter weekFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
|
|
- // 计算起始日期后的第一个周日
|
|
|
- LocalDate currentWeek = fromDate;
|
|
|
- DayOfWeek dayOfWeek = currentWeek.getDayOfWeek();
|
|
|
- int daysToSunday = 7 - dayOfWeek.getValue(); // 周日的DayOfWeek值为7
|
|
|
- LocalDate firstSunday = currentWeek.plusDays(daysToSunday);
|
|
|
-
|
|
|
- while (!firstSunday.isAfter(toDate)) {
|
|
|
- dates.add(firstSunday.format(weekFormatter));
|
|
|
- firstSunday = firstSunday.plusWeeks(1);
|
|
|
- }
|
|
|
- break;
|
|
|
- case "month":
|
|
|
- DateTimeFormatter monthFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
|
|
- // 计算范围内的每月1号
|
|
|
- LocalDate currentMonthFirstDay = LocalDate.of(fromDate.getYear(), fromDate.getMonth(), 1);
|
|
|
- // 如果当月1号在起始日期之前,则取下个月1号
|
|
|
- if (currentMonthFirstDay.isBefore(fromDate)) {
|
|
|
- currentMonthFirstDay = currentMonthFirstDay.plusMonths(1);
|
|
|
- }
|
|
|
-
|
|
|
- while (!currentMonthFirstDay.isAfter(toDate)) {
|
|
|
- dates.add(currentMonthFirstDay.format(monthFormatter));
|
|
|
- currentMonthFirstDay = currentMonthFirstDay.plusMonths(1);
|
|
|
- }
|
|
|
- break;
|
|
|
- default:
|
|
|
- // 默认按天处理
|
|
|
- DateTimeFormatter defaultFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
|
|
- LocalDate defaultStart = fromDate;
|
|
|
- while (!defaultStart.isAfter(toDate)) {
|
|
|
- dates.add(defaultStart.format(defaultFormatter));
|
|
|
- defaultStart = defaultStart.plusDays(1);
|
|
|
- }
|
|
|
- }
|
|
|
+ // 1. 生成时间轴
|
|
|
+ List<String> dates = generateTimeAxis(reqDto.getFromDate(), reqDto.getToDate(), reqDto.getTimeUnit());
|
|
|
+ result.setDates(dates);
|
|
|
|
|
|
- // 生成items数据
|
|
|
- List<TrendBaseVO> items = new ArrayList<>();
|
|
|
+ // 2. 处理版本列表(为空时按"All"处理)
|
|
|
List<String> versions = reqDto.getVersion();
|
|
|
- // 若未指定版本,使用默认版本列表
|
|
|
- if (versions == null || versions.isEmpty()) {
|
|
|
- versions = Arrays.asList("1.0", "5.1");
|
|
|
- }
|
|
|
-
|
|
|
- Random random = new Random();
|
|
|
- for (String version : versions) {
|
|
|
- TrendBaseVO trend = new TrendBaseVO();
|
|
|
- trend.setName("活跃用户");
|
|
|
- trend.setVersion(version);
|
|
|
- trend.setKey("activeUser");
|
|
|
-
|
|
|
- // 生成与日期数量匹配的随机数据
|
|
|
- List<Integer> data = new ArrayList<>(dates.size());
|
|
|
- for (int i = 0; i < dates.size(); i++) {
|
|
|
- // 生成100-1000之间的随机数,模拟用户数量
|
|
|
- data.add(random.nextInt(901) + 100);
|
|
|
- }
|
|
|
- trend.setData(data);
|
|
|
- items.add(trend);
|
|
|
- }
|
|
|
-
|
|
|
- result.setDates(dates);
|
|
|
- result.setItems(items);
|
|
|
+ List<String> processVersions = (versions == null || versions.isEmpty())
|
|
|
+ ? Collections.singletonList("All")
|
|
|
+ : versions.stream().filter(v -> v != null && !v.isEmpty()).toList();
|
|
|
+
|
|
|
+ // 3. 处理每个版本的数据
|
|
|
+ List<TrendBaseVO> voList = processVersions.stream()
|
|
|
+ .map(version -> {
|
|
|
+ TrendBaseVO trend = new TrendBaseVO();
|
|
|
+ trend.setName("活跃用户");
|
|
|
+ trend.setVersion(version);
|
|
|
+ trend.setKey("activeUser");
|
|
|
+
|
|
|
+ // 计算每个时间点的活跃用户数
|
|
|
+ List<Integer> counts = dates.stream()
|
|
|
+ .map(date -> calculateNewUserCount(date, version, reqDto, "activeUser"))
|
|
|
+ .collect(Collectors.toList());
|
|
|
+
|
|
|
+ trend.setData(counts);
|
|
|
+ return trend;
|
|
|
+ })
|
|
|
+ .collect(Collectors.toList());
|
|
|
+
|
|
|
+ result.setItems(voList);
|
|
|
return result;
|
|
|
}
|
|
|
|
|
@@ -823,94 +702,35 @@ public class UserAnalyseServiceImpl implements UserAnalyseService {
|
|
|
@Override
|
|
|
public GetNewUserTrendVO getLaunchTrend(UserAnalyseQueryBaseDTO reqDto) {
|
|
|
GetNewUserTrendVO result = new GetNewUserTrendVO();
|
|
|
- List<String> dates = new ArrayList<>();
|
|
|
- String timeUnit = reqDto.getTimeUnit();
|
|
|
- LocalDate fromDate = reqDto.getFromDate();
|
|
|
- LocalDate toDate = reqDto.getToDate();
|
|
|
-
|
|
|
- // 生成日期列表
|
|
|
- switch (timeUnit) {
|
|
|
- case "hour":
|
|
|
- DateTimeFormatter hourFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:00");
|
|
|
- LocalDateTime hourStart = fromDate.atStartOfDay();
|
|
|
- LocalDateTime hourEnd = toDate.atTime(23, 0);
|
|
|
- while (!hourStart.isAfter(hourEnd)) {
|
|
|
- dates.add(hourStart.format(hourFormatter));
|
|
|
- hourStart = hourStart.plusHours(1);
|
|
|
- }
|
|
|
- break;
|
|
|
- case "day":
|
|
|
- DateTimeFormatter dayFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
|
|
- LocalDate dayStart = fromDate;
|
|
|
- while (!dayStart.isAfter(toDate)) {
|
|
|
- dates.add(dayStart.format(dayFormatter));
|
|
|
- dayStart = dayStart.plusDays(1);
|
|
|
- }
|
|
|
- break;
|
|
|
- case "week":
|
|
|
- DateTimeFormatter weekFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
|
|
- // 计算起始日期后的第一个周日
|
|
|
- LocalDate currentWeek = fromDate;
|
|
|
- DayOfWeek dayOfWeek = currentWeek.getDayOfWeek();
|
|
|
- int daysToSunday = 7 - dayOfWeek.getValue(); // 周日的DayOfWeek值为7
|
|
|
- LocalDate firstSunday = currentWeek.plusDays(daysToSunday);
|
|
|
-
|
|
|
- while (!firstSunday.isAfter(toDate)) {
|
|
|
- dates.add(firstSunday.format(weekFormatter));
|
|
|
- firstSunday = firstSunday.plusWeeks(1);
|
|
|
- }
|
|
|
- break;
|
|
|
- case "month":
|
|
|
- DateTimeFormatter monthFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
|
|
- // 计算范围内的每月1号
|
|
|
- LocalDate currentMonthFirstDay = LocalDate.of(fromDate.getYear(), fromDate.getMonth(), 1);
|
|
|
- // 如果当月1号在起始日期之前,则取下个月1号
|
|
|
- if (currentMonthFirstDay.isBefore(fromDate)) {
|
|
|
- currentMonthFirstDay = currentMonthFirstDay.plusMonths(1);
|
|
|
- }
|
|
|
-
|
|
|
- while (!currentMonthFirstDay.isAfter(toDate)) {
|
|
|
- dates.add(currentMonthFirstDay.format(monthFormatter));
|
|
|
- currentMonthFirstDay = currentMonthFirstDay.plusMonths(1);
|
|
|
- }
|
|
|
- break;
|
|
|
- default:
|
|
|
- // 默认按天处理
|
|
|
- DateTimeFormatter defaultFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
|
|
- LocalDate defaultStart = fromDate;
|
|
|
- while (!defaultStart.isAfter(toDate)) {
|
|
|
- dates.add(defaultStart.format(defaultFormatter));
|
|
|
- defaultStart = defaultStart.plusDays(1);
|
|
|
- }
|
|
|
- }
|
|
|
+ // 1. 生成时间轴
|
|
|
+ List<String> dates = generateTimeAxis(reqDto.getFromDate(), reqDto.getToDate(), reqDto.getTimeUnit());
|
|
|
+ result.setDates(dates);
|
|
|
|
|
|
- // 生成items数据
|
|
|
- List<TrendBaseVO> items = new ArrayList<>();
|
|
|
+ // 2. 处理版本列表(为空时按"All"处理)
|
|
|
List<String> versions = reqDto.getVersion();
|
|
|
- // 若未指定版本,使用默认版本列表
|
|
|
- if (versions == null || versions.isEmpty()) {
|
|
|
- versions = Arrays.asList("1.0", "5.1");
|
|
|
- }
|
|
|
-
|
|
|
- Random random = new Random();
|
|
|
- for (String version : versions) {
|
|
|
- TrendBaseVO trend = new TrendBaseVO();
|
|
|
- trend.setName("启动次数");
|
|
|
- trend.setVersion(version);
|
|
|
- trend.setKey("launch");
|
|
|
-
|
|
|
- // 生成与日期数量匹配的随机数据
|
|
|
- List<Integer> data = new ArrayList<>(dates.size());
|
|
|
- for (int i = 0; i < dates.size(); i++) {
|
|
|
- // 生成100-1000之间的随机数,模拟用户数量
|
|
|
- data.add(random.nextInt(901) + 100);
|
|
|
- }
|
|
|
- trend.setData(data);
|
|
|
- items.add(trend);
|
|
|
- }
|
|
|
-
|
|
|
- result.setDates(dates);
|
|
|
- result.setItems(items);
|
|
|
+ List<String> processVersions = (versions == null || versions.isEmpty())
|
|
|
+ ? Collections.singletonList("All")
|
|
|
+ : versions.stream().filter(v -> v != null && !v.isEmpty()).toList();
|
|
|
+
|
|
|
+ // 3. 处理每个版本的数据
|
|
|
+ List<TrendBaseVO> voList = processVersions.stream()
|
|
|
+ .map(version -> {
|
|
|
+ TrendBaseVO trend = new TrendBaseVO();
|
|
|
+ trend.setName("启动次数");
|
|
|
+ trend.setVersion(version);
|
|
|
+ trend.setKey("launch");
|
|
|
+
|
|
|
+ // 计算每个时间点的启动次数
|
|
|
+ List<Integer> counts = dates.stream()
|
|
|
+ .map(date -> calculateNewUserCount(date, version, reqDto, "launch"))
|
|
|
+ .collect(Collectors.toList());
|
|
|
+
|
|
|
+ trend.setData(counts);
|
|
|
+ return trend;
|
|
|
+ })
|
|
|
+ .collect(Collectors.toList());
|
|
|
+
|
|
|
+ result.setItems(voList);
|
|
|
return result;
|
|
|
}
|
|
|
/**
|
|
@@ -920,101 +740,74 @@ public class UserAnalyseServiceImpl implements UserAnalyseService {
|
|
|
*/
|
|
|
@Override
|
|
|
public Page<PageLaunchDetailVO> pageLaunchDetail(PageActiveDetailDTO reqDto) {
|
|
|
- // 初始化分页对象
|
|
|
+ // 1. 初始化分页对象并计算总条数
|
|
|
Page<PageLaunchDetailVO> page = new Page<>(reqDto.getCurrent(), reqDto.getSize());
|
|
|
- List<PageLaunchDetailVO> result = new ArrayList<>();
|
|
|
- Random random = new Random();
|
|
|
+ page.setTotal(calculateTotalCount(reqDto.getFromDate(), reqDto.getToDate(), reqDto.getTimeUnit()));
|
|
|
|
|
|
- // 日期格式化器
|
|
|
- DateTimeFormatter hourFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:00");
|
|
|
- DateTimeFormatter dayFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
|
|
- DateTimeFormatter weekMonthFormatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");
|
|
|
+ // 2. 生成当前页时间轴
|
|
|
+ List<String> dates = generatePageTimeAxis(reqDto.getFromDate(), reqDto.getToDate(),
|
|
|
+ reqDto.getTimeUnit(), reqDto.getCurrent(), reqDto.getSize());
|
|
|
+ if (dates.isEmpty()) {
|
|
|
+ return page; // 无数据直接返回
|
|
|
+ }
|
|
|
|
|
|
- // 获取入参参数
|
|
|
+ // 3. 定义变量
|
|
|
+ DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");
|
|
|
+ List<PageLaunchDetailVO> detailVOS = new ArrayList<>(dates.size());
|
|
|
String timeUnit = reqDto.getTimeUnit();
|
|
|
- LocalDate fromDate = reqDto.getFromDate();
|
|
|
- LocalDate toDate = reqDto.getToDate();
|
|
|
-
|
|
|
- // 根据时间单位生成模拟数据
|
|
|
- switch (timeUnit) {
|
|
|
- case "hour":
|
|
|
- LocalDateTime currentHour = LocalDateTime.of(fromDate, LocalTime.MIN);
|
|
|
- LocalDateTime endHour = LocalDateTime.of(toDate, LocalTime.MAX);
|
|
|
- while (!currentHour.isAfter(endHour)) {
|
|
|
- PageLaunchDetailVO vo = new PageLaunchDetailVO();
|
|
|
- vo.setDate(currentHour.format(hourFormatter));
|
|
|
- // 保留两位小数:生成0-100的随机数后四舍五入保留两位
|
|
|
- double hourRate = random.nextDouble() * 100;
|
|
|
- vo.setLaunchRate(Math.round(hourRate * 100) / 100.0);
|
|
|
- vo.setLaunch(random.nextInt(10000));
|
|
|
- result.add(vo);
|
|
|
- currentHour = currentHour.plusHours(1);
|
|
|
- }
|
|
|
- break;
|
|
|
-
|
|
|
- case "day":
|
|
|
- LocalDate currentDay = fromDate;
|
|
|
- while (!currentDay.isAfter(toDate)) {
|
|
|
- PageLaunchDetailVO vo = new PageLaunchDetailVO();
|
|
|
- vo.setDate(currentDay.format(dayFormatter));
|
|
|
- // 保留两位小数
|
|
|
- double dayRate = random.nextDouble() * 100;
|
|
|
- vo.setLaunchRate(Math.round(dayRate * 100) / 100.0);
|
|
|
- vo.setLaunch(random.nextInt(40000) + 929);
|
|
|
- result.add(vo);
|
|
|
- currentDay = currentDay.plusDays(1);
|
|
|
- }
|
|
|
- break;
|
|
|
-
|
|
|
- case "week":
|
|
|
- LocalDate weekStart = fromDate.minusDays(fromDate.getDayOfWeek().getValue() - 1);
|
|
|
- while (!weekStart.isAfter(toDate)) {
|
|
|
- LocalDate weekEnd = weekStart.plusDays(6);
|
|
|
- if (weekEnd.isAfter(toDate)) {
|
|
|
- weekEnd = toDate;
|
|
|
- }
|
|
|
- PageLaunchDetailVO vo = new PageLaunchDetailVO();
|
|
|
- vo.setDate(weekStart.format(weekMonthFormatter) + "~" + weekEnd.format(weekMonthFormatter));
|
|
|
- // 保留两位小数
|
|
|
- double weekRate = random.nextDouble() * 50;
|
|
|
- vo.setLaunchRate(Math.round(weekRate * 100) / 100.0);
|
|
|
- vo.setLaunch(random.nextInt(10000) + 298);
|
|
|
- result.add(vo);
|
|
|
- weekStart = weekEnd.plusDays(1);
|
|
|
- }
|
|
|
- break;
|
|
|
-
|
|
|
- case "month":
|
|
|
- LocalDate monthStart = fromDate.withDayOfMonth(1);
|
|
|
- while (!monthStart.isAfter(toDate)) {
|
|
|
- LocalDate monthEnd = monthStart.plusMonths(1).minusDays(1);
|
|
|
- if (monthEnd.isAfter(toDate)) {
|
|
|
- monthEnd = toDate;
|
|
|
- }
|
|
|
- PageLaunchDetailVO vo = new PageLaunchDetailVO();
|
|
|
- vo.setDate(monthStart.format(weekMonthFormatter) + "~" + monthEnd.format(weekMonthFormatter));
|
|
|
- // 保留两位小数
|
|
|
- double monthRate = random.nextDouble() * 60;
|
|
|
- vo.setLaunchRate(Math.round(monthRate * 100) / 100.0);
|
|
|
- vo.setLaunch(random.nextInt(30000) + 3955);
|
|
|
- result.add(vo);
|
|
|
- monthStart = monthEnd.plusDays(1);
|
|
|
- }
|
|
|
- break;
|
|
|
+ boolean isRangeType = "week".equals(timeUnit) || "month".equals(timeUnit);
|
|
|
+
|
|
|
+ // 4. 计算时间范围内总启动次数
|
|
|
+ BigDecimal totalLaunch = BigDecimal.ZERO;
|
|
|
+ LambdaQueryWrapper<MktStatUserAnalysis> wrapper = lambdaQuery(MktStatUserAnalysis.class)
|
|
|
+ .in(reqDto.getVersion() != null && !reqDto.getVersion().isEmpty(),
|
|
|
+ MktStatUserAnalysis::getVersion, reqDto.getVersion())
|
|
|
+ .in(reqDto.getChannel() != null && !reqDto.getChannel().isEmpty(),
|
|
|
+ MktStatUserAnalysis::getChannel, reqDto.getChannel())
|
|
|
+ .ge(MktStatUserAnalysis::getStatDate, reqDto.getFromDate()+" 00:00:00")
|
|
|
+ .le(MktStatUserAnalysis::getStatDate, reqDto.getToDate()+" 23:59:59")
|
|
|
+ .select(MktStatUserAnalysis::getLaunch);
|
|
|
+ List<MktStatUserAnalysis> launchList = userAnalysisMapper.selectList(wrapper);
|
|
|
+ for (MktStatUserAnalysis userAnalysis : launchList) {
|
|
|
+ totalLaunch = totalLaunch.add(BigDecimal.valueOf(userAnalysis.getLaunch()));
|
|
|
}
|
|
|
|
|
|
- // 处理分页
|
|
|
- int total = result.size();
|
|
|
- int fromIndex = (int) ((reqDto.getCurrent() - 1) * reqDto.getSize());
|
|
|
- int toIndex = Math.min(fromIndex + (int) reqDto.getSize(), total);
|
|
|
- if (fromIndex >= 0 && fromIndex < total) {
|
|
|
- result = result.subList(fromIndex, toIndex);
|
|
|
- } else {
|
|
|
- result = new ArrayList<>();
|
|
|
+ for (String date : dates) {
|
|
|
+ // 5. 获取时间范围
|
|
|
+ LocalDateTime startTime = getStartTime(date, timeUnit);
|
|
|
+ LocalDateTime endTime = getEndTime(date, timeUnit);
|
|
|
+
|
|
|
+ // 6. 构建查询条件
|
|
|
+ LambdaQueryWrapper<MktStatUserAnalysis> queryWrapper = lambdaQuery(MktStatUserAnalysis.class)
|
|
|
+ .in(reqDto.getVersion() != null && !reqDto.getVersion().isEmpty(),
|
|
|
+ MktStatUserAnalysis::getVersion, reqDto.getVersion())
|
|
|
+ .in(reqDto.getChannel() != null && !reqDto.getChannel().isEmpty(),
|
|
|
+ MktStatUserAnalysis::getChannel, reqDto.getChannel())
|
|
|
+ .ge(MktStatUserAnalysis::getStatDate, startTime)
|
|
|
+ .lt(MktStatUserAnalysis::getStatDate, endTime)
|
|
|
+ .select(MktStatUserAnalysis::getLaunch);
|
|
|
+
|
|
|
+ // 6. 计算汇总数据
|
|
|
+ List<MktStatUserAnalysis> userAnalyses = userAnalysisMapper.selectList(queryWrapper);
|
|
|
+ BigDecimal launch = BigDecimal.ZERO;
|
|
|
+ for (MktStatUserAnalysis userAnalysis : userAnalyses) {
|
|
|
+ launch = launch.add(BigDecimal.valueOf(userAnalysis.getLaunch()));
|
|
|
+ }
|
|
|
+ // 7. 构建返回对象
|
|
|
+ PageLaunchDetailVO detailVO = new PageLaunchDetailVO();
|
|
|
+ // 处理日期显示
|
|
|
+ String displayDate = isRangeType && startTime != null && endTime != null
|
|
|
+ ? startTime.toLocalDate().format(dateFormatter) + "~" + endTime.minusDays(1).toLocalDate().format(dateFormatter)
|
|
|
+ : date;
|
|
|
+ detailVO.setDate(displayDate);
|
|
|
+ BigDecimal newUserRate = launch.compareTo(BigDecimal.ZERO) == 0 || totalLaunch.compareTo(BigDecimal.ZERO) == 0
|
|
|
+ ? BigDecimal.ZERO
|
|
|
+ : launch.divide(totalLaunch, 4, RoundingMode.HALF_UP);
|
|
|
+ detailVO.setLaunchRate(newUserRate);
|
|
|
+ detailVO.setLaunch( launch.intValue());
|
|
|
+ detailVOS.add(detailVO);
|
|
|
}
|
|
|
-
|
|
|
- page.setRecords(result);
|
|
|
- page.setTotal(total);
|
|
|
+ page.setRecords(detailVOS);
|
|
|
return page;
|
|
|
}
|
|
|
|
|
@@ -1318,4 +1111,289 @@ public class UserAnalyseServiceImpl implements UserAnalyseService {
|
|
|
return page;
|
|
|
}
|
|
|
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算单个时间点的数量
|
|
|
+ */
|
|
|
+ private Integer calculateNewUserCount(String date, String version, UserAnalyseQueryBaseDTO reqDto, String field) {
|
|
|
+ // 1. 获取时间范围(开始和结束时间)
|
|
|
+ LocalDateTime startTime = getStartTime(date, reqDto.getTimeUnit());
|
|
|
+ LocalDateTime endTime = getEndTime(date, reqDto.getTimeUnit());
|
|
|
+ if (startTime == null || endTime == null) {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 构建查询条件
|
|
|
+ LambdaQueryWrapper<MktStatUserAnalysis> queryWrapper = Wrappers.<MktStatUserAnalysis>lambdaQuery()
|
|
|
+ .ge(MktStatUserAnalysis::getStatDate, startTime)
|
|
|
+ .lt(MktStatUserAnalysis::getStatDate, endTime);
|
|
|
+ switch (field){
|
|
|
+ case "newUser":
|
|
|
+ queryWrapper.select(MktStatUserAnalysis::getNewUser);
|
|
|
+ break;
|
|
|
+ case "activeUser":
|
|
|
+ queryWrapper.select(MktStatUserAnalysis::getActiveUser);
|
|
|
+ break;
|
|
|
+ case "launch":
|
|
|
+ queryWrapper.select(MktStatUserAnalysis::getLaunch);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加渠道条件
|
|
|
+ List<String> channels = reqDto.getChannel();
|
|
|
+ if (channels != null && !channels.isEmpty()) {
|
|
|
+ queryWrapper.in(MktStatUserAnalysis::getChannel, channels);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加版本条件("All"表示不限制版本)
|
|
|
+ if (!"All".equals(version)) {
|
|
|
+ queryWrapper.eq(MktStatUserAnalysis::getVersion, version);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 执行查询并累加结果
|
|
|
+ List<MktStatUserAnalysis> dataList = userAnalysisMapper.selectList(queryWrapper);
|
|
|
+ return dataList.stream()
|
|
|
+ .mapToInt(data -> switch (field) {
|
|
|
+ case "newUser" -> data.getNewUser() != null ? data.getNewUser() : 0;
|
|
|
+ case "activeUser" -> data.getActiveUser() != null ? data.getActiveUser() : 0;
|
|
|
+ case "launch" -> data.getLaunch() != null ? data.getLaunch() : 0;
|
|
|
+ default -> 0;
|
|
|
+ })
|
|
|
+ .sum();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 根据时间粒度和日期字符串获取开始时间
|
|
|
+ */
|
|
|
+ private LocalDateTime getStartTime(String dateStr, String timeUnit) {
|
|
|
+ try {
|
|
|
+ return switch (timeUnit) {
|
|
|
+ case "hour" -> {
|
|
|
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:00");
|
|
|
+ yield LocalDateTime.parse(dateStr, formatter);
|
|
|
+ }
|
|
|
+ case "day", "week", "month" -> {
|
|
|
+ DateTimeFormatter day = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
|
|
+ yield LocalDate.parse(dateStr, day).atStartOfDay();
|
|
|
+ }
|
|
|
+ default -> {
|
|
|
+ log.warn("不支持的时间粒度: {}", timeUnit);
|
|
|
+ yield null;
|
|
|
+ }
|
|
|
+ };
|
|
|
+ } catch (DateTimeParseException e) {
|
|
|
+ log.error("日期解析失败, date: {}, timeUnit: {}", dateStr, timeUnit, e);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 根据时间粒度和日期字符串获取结束时间
|
|
|
+ */
|
|
|
+ private LocalDateTime getEndTime(String dateStr, String timeUnit) {
|
|
|
+ try {
|
|
|
+ switch (timeUnit) {
|
|
|
+ case "hour":
|
|
|
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:00");
|
|
|
+ return LocalDateTime.parse(dateStr, formatter).plusHours(1);
|
|
|
+ case "day":
|
|
|
+ DateTimeFormatter day = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
|
|
+ return LocalDate.parse(dateStr, day).atStartOfDay().plusDays(1);
|
|
|
+ case "week":
|
|
|
+ DateTimeFormatter week = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
|
|
+ return LocalDate.parse(dateStr, week).atStartOfDay().plusWeeks(1);
|
|
|
+ case "month":
|
|
|
+ DateTimeFormatter month = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
|
|
+ LocalDate startDate = LocalDate.parse(dateStr, month);
|
|
|
+ return startDate.plusMonths(1).atStartOfDay();
|
|
|
+ default:
|
|
|
+ log.warn("不支持的时间粒度: {}", timeUnit);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ } catch (DateTimeParseException e) {
|
|
|
+ log.error("日期解析失败, date: {}, timeUnit: {}", dateStr, timeUnit, e);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成时间轴列表
|
|
|
+ */
|
|
|
+ private List<String> generateTimeAxis(LocalDate fromDate, LocalDate toDate, String timeUnit) {
|
|
|
+ List<String> timeAxis = new ArrayList<>();
|
|
|
+ DateTimeFormatter formatter;
|
|
|
+
|
|
|
+ switch (timeUnit) {
|
|
|
+ case "hour":
|
|
|
+ formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:00");
|
|
|
+ LocalDateTime startHour = fromDate.atStartOfDay();
|
|
|
+ LocalDateTime endHour = toDate.atTime(23, 0);
|
|
|
+
|
|
|
+ while (!startHour.isAfter(endHour)) {
|
|
|
+ timeAxis.add(startHour.format(formatter));
|
|
|
+ startHour = startHour.plusHours(1);
|
|
|
+ }
|
|
|
+ break;
|
|
|
+
|
|
|
+ case "day":
|
|
|
+ formatter = DateTimeFormatter.ISO_LOCAL_DATE;
|
|
|
+ LocalDate currentDay = fromDate;
|
|
|
+
|
|
|
+ while (!currentDay.isAfter(toDate)) {
|
|
|
+ timeAxis.add(currentDay.format(formatter));
|
|
|
+ currentDay = currentDay.plusDays(1);
|
|
|
+ }
|
|
|
+ break;
|
|
|
+
|
|
|
+ case "week":
|
|
|
+ formatter = DateTimeFormatter.ISO_LOCAL_DATE;
|
|
|
+ LocalDate firstSunday = fromDate.with(TemporalAdjusters.previousOrSame(DayOfWeek.SUNDAY));
|
|
|
+ LocalDate lastSunday = toDate.with(TemporalAdjusters.previousOrSame(DayOfWeek.SUNDAY));
|
|
|
+ LocalDate currentSunday = firstSunday;
|
|
|
+ while (!currentSunday.isAfter(lastSunday)) {
|
|
|
+ timeAxis.add(currentSunday.format(formatter));
|
|
|
+ currentSunday = currentSunday.plusWeeks(1);
|
|
|
+ }
|
|
|
+ break;
|
|
|
+
|
|
|
+ case "month":
|
|
|
+ formatter = DateTimeFormatter.ISO_LOCAL_DATE;
|
|
|
+ LocalDate firstDayOfMonth = fromDate.with(TemporalAdjusters.firstDayOfMonth());
|
|
|
+ LocalDate lastDayOfMonth = toDate.with(TemporalAdjusters.firstDayOfMonth());
|
|
|
+
|
|
|
+ LocalDate currentMonth = firstDayOfMonth;
|
|
|
+ while (!currentMonth.isAfter(lastDayOfMonth)) {
|
|
|
+ timeAxis.add(currentMonth.format(formatter));
|
|
|
+ currentMonth = currentMonth.plusMonths(1).with(TemporalAdjusters.firstDayOfMonth());
|
|
|
+ }
|
|
|
+ break;
|
|
|
+
|
|
|
+ default:
|
|
|
+ throw new IllegalArgumentException("不支持的时间单位: " + timeUnit);
|
|
|
+ }
|
|
|
+
|
|
|
+ return timeAxis;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成指定分页的时间坐标轴
|
|
|
+ */
|
|
|
+ private List<String> generatePageTimeAxis(LocalDate fromDate, LocalDate toDate, String timeUnit, long current, long size) {
|
|
|
+ List<String> timeAxis = new ArrayList<>();
|
|
|
+ DateTimeFormatter formatter;
|
|
|
+
|
|
|
+ // 计算起始位置
|
|
|
+ long startPosition = (current - 1) * size;
|
|
|
+ long currentPosition = 0;
|
|
|
+
|
|
|
+ switch (timeUnit) {
|
|
|
+ case "hour":
|
|
|
+ formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:00");
|
|
|
+ LocalDateTime startHour = fromDate.atStartOfDay();
|
|
|
+ LocalDateTime endHour = toDate.atTime(23, 0);
|
|
|
+
|
|
|
+ // 跳过前面页的数据
|
|
|
+ while (!startHour.isAfter(endHour) && currentPosition < startPosition) {
|
|
|
+ startHour = startHour.plusHours(1);
|
|
|
+ currentPosition++;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 收集当前页的数据
|
|
|
+ while (!startHour.isAfter(endHour) && timeAxis.size() < size) {
|
|
|
+ timeAxis.add(startHour.format(formatter));
|
|
|
+ startHour = startHour.plusHours(1);
|
|
|
+ }
|
|
|
+ break;
|
|
|
+
|
|
|
+ case "day":
|
|
|
+ formatter = DateTimeFormatter.ISO_LOCAL_DATE;
|
|
|
+ LocalDate currentDay = fromDate;
|
|
|
+
|
|
|
+ // 跳过前面页的数据
|
|
|
+ while (!currentDay.isAfter(toDate) && currentPosition < startPosition) {
|
|
|
+ currentDay = currentDay.plusDays(1);
|
|
|
+ currentPosition++;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 收集当前页的数据
|
|
|
+ while (!currentDay.isAfter(toDate) && timeAxis.size() < size) {
|
|
|
+ timeAxis.add(currentDay.format(formatter));
|
|
|
+ currentDay = currentDay.plusDays(1);
|
|
|
+ }
|
|
|
+ break;
|
|
|
+
|
|
|
+ case "week":
|
|
|
+ formatter = DateTimeFormatter.ISO_LOCAL_DATE;
|
|
|
+ LocalDate firstSunday = fromDate.with(TemporalAdjusters.previousOrSame(DayOfWeek.SUNDAY));
|
|
|
+ LocalDate lastSunday = toDate.with(TemporalAdjusters.previousOrSame(DayOfWeek.SUNDAY));
|
|
|
+ LocalDate currentSunday = firstSunday;
|
|
|
+
|
|
|
+ // 跳过前面页的数据
|
|
|
+ while (!currentSunday.isAfter(lastSunday) && currentPosition < startPosition) {
|
|
|
+ currentSunday = currentSunday.plusWeeks(1);
|
|
|
+ currentPosition++;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 收集当前页的数据
|
|
|
+ while (!currentSunday.isAfter(lastSunday) && timeAxis.size() < size) {
|
|
|
+ timeAxis.add(currentSunday.format(formatter));
|
|
|
+ currentSunday = currentSunday.plusWeeks(1);
|
|
|
+ }
|
|
|
+ break;
|
|
|
+
|
|
|
+ case "month":
|
|
|
+ formatter = DateTimeFormatter.ISO_LOCAL_DATE;
|
|
|
+ LocalDate firstDayOfMonth = fromDate.with(TemporalAdjusters.firstDayOfMonth());
|
|
|
+ LocalDate lastDayOfMonth = toDate.with(TemporalAdjusters.firstDayOfMonth());
|
|
|
+
|
|
|
+ LocalDate currentMonth = firstDayOfMonth;
|
|
|
+
|
|
|
+ // 跳过前面页的数据
|
|
|
+ while (!currentMonth.isAfter(lastDayOfMonth) && currentPosition < startPosition) {
|
|
|
+ currentMonth = currentMonth.plusMonths(1).with(TemporalAdjusters.firstDayOfMonth());
|
|
|
+ currentPosition++;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 收集当前页的数据
|
|
|
+ while (!currentMonth.isAfter(lastDayOfMonth) && timeAxis.size() < size) {
|
|
|
+ timeAxis.add(currentMonth.format(formatter));
|
|
|
+ currentMonth = currentMonth.plusMonths(1).with(TemporalAdjusters.firstDayOfMonth());
|
|
|
+ }
|
|
|
+ break;
|
|
|
+
|
|
|
+ default:
|
|
|
+ throw new IllegalArgumentException("不支持的时间单位: " + timeUnit);
|
|
|
+ }
|
|
|
+
|
|
|
+ return timeAxis;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算总记录数(用于分页)
|
|
|
+ */
|
|
|
+ private long calculateTotalCount(LocalDate fromDate, LocalDate toDate, String timeUnit) {
|
|
|
+ switch (timeUnit) {
|
|
|
+ case "hour":
|
|
|
+ // 计算小时数: 天数差 * 24 + 结束日小时数 - 开始日小时数 + 1
|
|
|
+ long days = ChronoUnit.DAYS.between(fromDate, toDate);
|
|
|
+ return days * 24 + 24; // 每天24小时
|
|
|
+
|
|
|
+ case "day":
|
|
|
+ // 计算天数
|
|
|
+ return ChronoUnit.DAYS.between(fromDate, toDate) + 1;
|
|
|
+
|
|
|
+ case "week":
|
|
|
+ // 计算周数
|
|
|
+ LocalDate firstSunday = fromDate.with(TemporalAdjusters.previousOrSame(DayOfWeek.SUNDAY));
|
|
|
+ LocalDate lastSunday = toDate.with(TemporalAdjusters.previousOrSame(DayOfWeek.SUNDAY));
|
|
|
+ return ChronoUnit.WEEKS.between(firstSunday, lastSunday) + 1;
|
|
|
+
|
|
|
+ case "month":
|
|
|
+ // 计算月数
|
|
|
+ return ChronoUnit.MONTHS.between(fromDate, toDate) + 1;
|
|
|
+
|
|
|
+ default:
|
|
|
+ throw new IllegalArgumentException("不支持的时间单位: " + timeUnit);
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|