Ver código fonte

update: 新增用户、启动次数功能

lwh 1 mês atrás
pai
commit
593b325b3a
13 arquivos alterados com 869 adições e 514 exclusões
  1. 108 0
      pig-statistics/pig-statistics-api/src/main/java/com/pig4cloud/pig/statistics/api/entity/MktStatUserAnalysis.java
  2. 97 0
      pig-statistics/pig-statistics-api/src/main/java/com/pig4cloud/pig/statistics/api/entity/MktStatUserRetention.java
  3. 2 1
      pig-statistics/pig-statistics-api/src/main/java/com/pig4cloud/pig/statistics/api/vo/user/GetNewUserRetentionVO.java
  4. 2 1
      pig-statistics/pig-statistics-api/src/main/java/com/pig4cloud/pig/statistics/api/vo/user/PageLaunchDetailVO.java
  5. 2 1
      pig-statistics/pig-statistics-api/src/main/java/com/pig4cloud/pig/statistics/api/vo/user/PageNewUserTrendDetailVO.java
  6. 2 1
      pig-statistics/pig-statistics-api/src/main/java/com/pig4cloud/pig/statistics/api/vo/user/PageRetentionDetailVO.java
  7. 13 0
      pig-statistics/pig-statistics-biz/Dockerfile
  8. 1 1
      pig-statistics/pig-statistics-biz/src/main/java/com/pig4cloud/pig/statistics/controller/UserAnalyseController.java
  9. 16 0
      pig-statistics/pig-statistics-biz/src/main/java/com/pig4cloud/pig/statistics/mapper/MktStatUserAnalysisMapper.java
  10. 15 0
      pig-statistics/pig-statistics-biz/src/main/java/com/pig4cloud/pig/statistics/mapper/MktStatUserRetentionMapper.java
  11. 1 1
      pig-statistics/pig-statistics-biz/src/main/java/com/pig4cloud/pig/statistics/service/UserAnalyseService.java
  12. 586 508
      pig-statistics/pig-statistics-biz/src/main/java/com/pig4cloud/pig/statistics/service/impl/UserAnalyseServiceImpl.java
  13. 24 0
      pig-statistics/pig-statistics-biz/src/main/resources/mapper/MktStatUserAnalysisMapper.xml

+ 108 - 0
pig-statistics/pig-statistics-api/src/main/java/com/pig4cloud/pig/statistics/api/entity/MktStatUserAnalysis.java

@@ -0,0 +1,108 @@
+package com.pig4cloud.pig.statistics.api.entity;
+
+
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.extension.activerecord.Model;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.LocalDateTime;
+
+/**
+ * @author: lwh
+ * @date: 2025-08-11
+ * @description: 营销统计-用户分析
+ */
+@Data
+@Schema(description = "用户分析")
+@EqualsAndHashCode(callSuper = true)
+public class MktStatUserAnalysis extends Model<MktStatUserAnalysis> {
+
+	/**
+	 * id
+	 */
+	@TableId(value = "id")
+	private Long id;
+
+	/**
+	 * 渠道
+	 */
+	@Schema(description = "渠道")
+	private String channel;
+
+	/**
+	 * 版本
+	 */
+	@Schema(description = "版本")
+	private String version;
+
+	/**
+	 * 应用ID
+	 */
+	@Schema(description = "应用ID")
+	private String appId;
+
+	/**
+	 * 新增用户
+	 */
+	@Schema(description = "新增用户")
+	private Integer newUser;
+
+	/**
+	 * 活跃用户
+	 */
+	@Schema(description = "活跃用户")
+	private Integer activeUser;
+
+	/**
+	 * 启动次数
+	 */
+	@Schema(description = "启动次数")
+	private Integer launch;
+
+	/**
+	 * 统计时间
+	 */
+	@Schema(description = "统计时间")
+	private LocalDateTime statDate;
+
+	/**
+	 * 删除标记
+	 */
+	@TableLogic
+	@TableField(fill = FieldFill.INSERT)
+	@Schema(description = "删除标记,1:已删除,0:正常")
+	private String delFlag;
+
+	/**
+	 * 创建人
+	 */
+	@TableField(fill = FieldFill.INSERT)
+	@Schema(description = "创建人")
+	private String createBy;
+
+	/**
+	 * 修改人
+	 */
+	@TableField(fill = FieldFill.UPDATE)
+	@Schema(description = "修改人")
+	private String updateBy;
+
+	/**
+	 * 创建时间
+	 */
+	@TableField(fill = FieldFill.INSERT)
+	@Schema(description = "创建时间")
+	private LocalDateTime createTime;
+
+	/**
+	 * 更新时间
+	 */
+	@TableField(fill = FieldFill.UPDATE)
+	@Schema(description = "更新时间")
+	private LocalDateTime updateTime;
+}

+ 97 - 0
pig-statistics/pig-statistics-api/src/main/java/com/pig4cloud/pig/statistics/api/entity/MktStatUserRetention.java

@@ -0,0 +1,97 @@
+package com.pig4cloud.pig.statistics.api.entity;
+
+
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.extension.activerecord.Model;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * @author: lwh
+ * @date: 2025-08-11
+ * @description: 营销统计-用户留存
+ */
+@Data
+@Schema(description = "用户留存")
+@EqualsAndHashCode(callSuper = true)
+public class MktStatUserRetention extends Model<MktStatUserRetention> {
+
+	/**
+	 * id
+	 */
+	@TableId(value = "id")
+	private Long id;
+
+	/**
+	 * 留存率
+	 */
+	@Schema(description = "留存率")
+	private BigDecimal retention;
+
+	/**
+	 * 新增用户
+	 */
+	@Schema(description = "新增用户")
+	private Integer newUser;
+
+	/**
+	 * 统计时间
+	 */
+	@Schema(description = "统计时间")
+	private LocalDateTime statDate;
+
+	/**
+	 * 渠道
+	 */
+	@Schema(description = "渠道")
+	private String channel;
+
+	/**
+	 * 版本
+	 */
+	@Schema(description = "版本")
+	private String version;
+
+	/**
+	 * 删除标记
+	 */
+	@TableLogic
+	@TableField(fill = FieldFill.INSERT)
+	@Schema(description = "删除标记,1:已删除,0:正常")
+	private String delFlag;
+
+	/**
+	 * 创建人
+	 */
+	@TableField(fill = FieldFill.INSERT)
+	@Schema(description = "创建人")
+	private String createBy;
+
+	/**
+	 * 修改人
+	 */
+	@TableField(fill = FieldFill.UPDATE)
+	@Schema(description = "修改人")
+	private String updateBy;
+
+	/**
+	 * 创建时间
+	 */
+	@TableField(fill = FieldFill.INSERT)
+	@Schema(description = "创建时间")
+	private LocalDateTime createTime;
+
+	/**
+	 * 更新时间
+	 */
+	@TableField(fill = FieldFill.UPDATE)
+	@Schema(description = "更新时间")
+	private LocalDateTime updateTime;
+}

+ 2 - 1
pig-statistics/pig-statistics-api/src/main/java/com/pig4cloud/pig/statistics/api/vo/user/GetNewUserRetentionVO.java

@@ -6,6 +6,7 @@ import lombok.Data;
 
 import java.io.Serial;
 import java.io.Serializable;
+import java.math.BigDecimal;
 import java.util.List;
 
 /**
@@ -30,5 +31,5 @@ public class GetNewUserRetentionVO implements Serializable {
 	 * 数量
 	 */
 	@Schema(description = "数量")
-	private List<Double> retention;
+	private List<BigDecimal> retentions;
 }

+ 2 - 1
pig-statistics/pig-statistics-api/src/main/java/com/pig4cloud/pig/statistics/api/vo/user/PageLaunchDetailVO.java

@@ -6,6 +6,7 @@ import lombok.Data;
 
 import java.io.Serial;
 import java.io.Serializable;
+import java.math.BigDecimal;
 
 /**
  * @author: lwh
@@ -29,7 +30,7 @@ public class PageLaunchDetailVO implements Serializable {
 	 * 启动次数占比
 	 */
 	@Schema(description = "启动次数占比")
-	private Double launchRate;
+	private BigDecimal launchRate;
 
 	/**
 	 * 启动次数

+ 2 - 1
pig-statistics/pig-statistics-api/src/main/java/com/pig4cloud/pig/statistics/api/vo/user/PageNewUserTrendDetailVO.java

@@ -6,6 +6,7 @@ import lombok.Data;
 
 import java.io.Serial;
 import java.io.Serializable;
+import java.math.BigDecimal;
 
 /**
  * @author: lwh
@@ -35,5 +36,5 @@ public class PageNewUserTrendDetailVO implements Serializable {
 	 * 新增用户占比
 	 */
 	@Schema(description = "新增用户占比")
-	private Double newUserRate;
+	private BigDecimal newUserRate;
 }

+ 2 - 1
pig-statistics/pig-statistics-api/src/main/java/com/pig4cloud/pig/statistics/api/vo/user/PageRetentionDetailVO.java

@@ -6,6 +6,7 @@ import lombok.Data;
 
 import java.io.Serial;
 import java.io.Serializable;
+import java.math.BigDecimal;
 
 /**
  * @author: lwh
@@ -29,5 +30,5 @@ public class PageRetentionDetailVO implements Serializable {
 	 * 留存率
 	 */
 	@Schema(description = "留存率")
-	private Double retention;
+	private BigDecimal retention;
 }

+ 13 - 0
pig-statistics/pig-statistics-biz/Dockerfile

@@ -0,0 +1,13 @@
+FROM registry.cn-hangzhou.aliyuncs.com/dockerhub_mirror/java:21-anolis
+
+WORKDIR /pig-statistics
+
+ARG JAR_FILE=target/pig-statistics-biz.jar
+
+COPY ${JAR_FILE} app.jar
+
+EXPOSE 17000
+
+ENV TZ=Asia/Shanghai JAVA_OPTS="-Xms128m -Xmx256m -Djava.security.egd=file:/dev/./urandom"
+
+CMD sleep 60; java $JAVA_OPTS -jar app.jar

+ 1 - 1
pig-statistics/pig-statistics-biz/src/main/java/com/pig4cloud/pig/statistics/controller/UserAnalyseController.java

@@ -35,7 +35,7 @@ public class UserAnalyseController {
 
 	@PostMapping("/new/trend")
 	@Operation(summary = "查询新增趋势")
-	public R getNewUserTrend(@Valid @RequestBody GetNewUserTrendDTO reqDto) {
+	public R getNewUserTrend(@Valid @RequestBody UserAnalyseQueryBaseDTO reqDto) {
 		GetNewUserTrendVO result = userAnalyseService.getNewUserTrend(reqDto);
 		return R.ok(result);
 	}

+ 16 - 0
pig-statistics/pig-statistics-biz/src/main/java/com/pig4cloud/pig/statistics/mapper/MktStatUserAnalysisMapper.java

@@ -0,0 +1,16 @@
+package com.pig4cloud.pig.statistics.mapper;
+
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.pig4cloud.pig.statistics.api.entity.MktStatUserAnalysis;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * @author: lwh
+ * @date: 2025-08-11
+ * @description: 营销统计-用户分析Mapper
+ */
+@Mapper
+public interface MktStatUserAnalysisMapper extends BaseMapper<MktStatUserAnalysis> {
+}
+

+ 15 - 0
pig-statistics/pig-statistics-biz/src/main/java/com/pig4cloud/pig/statistics/mapper/MktStatUserRetentionMapper.java

@@ -0,0 +1,15 @@
+package com.pig4cloud.pig.statistics.mapper;
+
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.pig4cloud.pig.statistics.api.entity.MktStatUserRetention;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * @author: lwh
+ * @date: 2025-08-11
+ * @description: 营销统计-留存率Mapper
+ */
+@Mapper
+public interface MktStatUserRetentionMapper extends BaseMapper<MktStatUserRetention> {
+}

+ 1 - 1
pig-statistics/pig-statistics-biz/src/main/java/com/pig4cloud/pig/statistics/service/UserAnalyseService.java

@@ -21,7 +21,7 @@ public interface UserAnalyseService {
 	 * @param reqDto 请求参数
 	 * @return GetNewUserTrendVO
 	 */
-	GetNewUserTrendVO getNewUserTrend(@Valid GetNewUserTrendDTO reqDto);
+	GetNewUserTrendVO getNewUserTrend(@Valid UserAnalyseQueryBaseDTO reqDto);
 
 	/**
 	 * 分页查询新增趋势详情

+ 586 - 508
pig-statistics/pig-statistics-biz/src/main/java/com/pig4cloud/pig/statistics/service/impl/UserAnalyseServiceImpl.java

@@ -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);
+		}
+	}
 }

+ 24 - 0
pig-statistics/pig-statistics-biz/src/main/resources/mapper/MktStatUserAnalysisMapper.xml

@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~
+  ~      Copyright (c) 2018-2025, lengleng All rights reserved.
+  ~
+  ~  Redistribution and use in source and binary forms, with or without
+  ~  modification, are permitted provided that the following conditions are met:
+  ~
+  ~ Redistributions of source code must retain the above copyright notice,
+  ~  this list of conditions and the following disclaimer.
+  ~  Redistributions in binary form must reproduce the above copyright
+  ~  notice, this list of conditions and the following disclaimer in the
+  ~  documentation and/or other materials provided with the distribution.
+  ~  Neither the name of the pig4cloud.com developer nor the names of its
+  ~  contributors may be used to endorse or promote products derived from
+  ~  this software without specific prior written permission.
+  ~  Author: lengleng ([email protected])
+  ~
+  -->
+
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.pig4cloud.pig.statistics.mapper.MktStatUserAnalysisMapper">
+
+</mapper>