Pārlūkot izejas kodu

new: 留存分析相关接口定义、工具抽取

lwh 2 nedēļas atpakaļ
vecāks
revīzija
39c02c2ab0

+ 262 - 0
pig-common/pig-common-core/src/main/java/com/pig4cloud/pig/common/core/util/stat/TimeUtil.java

@@ -0,0 +1,262 @@
+package com.pig4cloud.pig.common.core.util.stat;
+
+
+import lombok.extern.slf4j.Slf4j;
+
+import java.time.DayOfWeek;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.time.temporal.ChronoUnit;
+import java.time.temporal.TemporalAdjusters;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author: lwh
+ * @date: 2025-08-25
+ * @description: 统计模块时间工具类
+ */
+@Slf4j
+public class TimeUtil {
+
+	/**
+	 * 根据时间粒度和日期字符串获取开始时间
+	 */
+	public static 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;
+		}
+	}
+
+	/**
+	 * 根据时间粒度和日期字符串获取结束时间
+	 */
+	public static 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;
+		}
+	}
+
+	/**
+	 * 生成时间轴列表
+	 */
+	public static 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;
+	}
+
+	/**
+	 * 生成指定分页的时间坐标轴
+	 */
+	public static 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;
+	}
+
+	/**
+	 * 计算总记录数(用于分页)
+	 */
+	public static 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":
+				// 计算月数:基于月份第一天计算,与generatePageTimeAxis逻辑保持一致
+				LocalDate firstMonDay = fromDate.with(TemporalAdjusters.firstDayOfMonth());
+				LocalDate lastMonDay = toDate.with(TemporalAdjusters.firstDayOfMonth());
+				return ChronoUnit.MONTHS.between(firstMonDay, lastMonDay) + 1;
+
+			default:
+				throw new IllegalArgumentException("不支持的时间单位: " + timeUnit);
+		}
+	}
+
+
+}

+ 53 - 0
pig-statistics/pig-statistics-api/src/main/java/com/pig4cloud/pig/statistics/api/dto/retention/GetFreshnessTrendDTO.java

@@ -0,0 +1,53 @@
+package com.pig4cloud.pig.statistics.api.dto.retention;
+
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.time.LocalDate;
+
+/**
+ * @author: lwh
+ * @date: 2025-08-25
+ * @description: 查询用户新鲜度趋势入参
+ */
+@Data
+@Schema(description = "查询用户新鲜度趋势入参")
+public class GetFreshnessTrendDTO implements Serializable {
+
+	@Serial
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * 应用ID
+	 */
+	@NotBlank(message = "应用ID不能为空")
+	@Schema(description = "应用ID", example = "Fqs2CL9CUn7U1AqilSFXgb")
+	private String appId;
+
+	/**
+	 * 开始时间
+	 */
+	@NotNull(message = "开始时间不能为空")
+	@Schema(description = "开始时间", example = "2025-07-29")
+	private LocalDate fromDate;
+
+	/**
+	 * 结束时间
+	 */
+	@NotNull(message = "结束时间不能为空")
+	@Schema(description = "结束时间", example = "2025-08-05")
+	private LocalDate toDate;
+
+	/**
+	 * 时间单位
+	 */
+	@NotBlank(message = "时间单位不能为空")
+	@Schema(description = "时间单位,day-天,week-周,month-月", example = "day")
+	private String timeUnit;
+
+}

+ 35 - 0
pig-statistics/pig-statistics-api/src/main/java/com/pig4cloud/pig/statistics/api/vo/retention/GetFreshnessTrendVO.java

@@ -0,0 +1,35 @@
+package com.pig4cloud.pig.statistics.api.vo.retention;
+
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * @author: lwh
+ * @date: 2025-08-25
+ * @description: TODO
+ */
+
+@Data
+@Schema(description = "用户新鲜度趋势出参")
+public class GetFreshnessTrendVO implements Serializable {
+
+	@Serial
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * 日期
+	 */
+	@Schema(description = "日期", example = "2025-08-05")
+	private String date;
+
+	/**
+	 * 数据
+	 */
+	@Schema(description = "数据")
+	private List<Integer> data;
+}

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

@@ -3,8 +3,10 @@ package com.pig4cloud.pig.statistics.controller;
 
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.pig4cloud.pig.common.core.util.R;
+import com.pig4cloud.pig.statistics.api.dto.retention.GetFreshnessTrendDTO;
 import com.pig4cloud.pig.statistics.api.dto.retention.GetRetentionTrendDTO;
 import com.pig4cloud.pig.statistics.api.dto.retention.PageQueryRetentionDTO;
+import com.pig4cloud.pig.statistics.api.vo.retention.GetFreshnessTrendVO;
 import com.pig4cloud.pig.statistics.api.vo.retention.GetRetentionTrendVO;
 import com.pig4cloud.pig.statistics.api.vo.retention.PageQueryRetentionVO;
 import com.pig4cloud.pig.statistics.service.RetentionAnalyseService;
@@ -45,7 +47,8 @@ public class RetentionAnalyseController
 	@PostMapping("/trend")
 	@Operation(summary = "查询留存趋势")
 	public R<GetRetentionTrendVO> getRetentionTrend(@Valid @RequestBody GetRetentionTrendDTO reqDto) {
-		return R.ok();
+		GetRetentionTrendVO result = retentionAnalyseService.getRetentionTrend(reqDto);
+		return R.ok(result);
 	}
 
 	@PostMapping("/view/export")
@@ -53,4 +56,35 @@ public class RetentionAnalyseController
 	public R exportRetentionDetail(@Valid @RequestBody GetRetentionTrendDTO reqDto) {
 		return R.ok();
 	}
+
+/***************************************** 用户新鲜度 *****************************************/
+
+	@PostMapping("/freshness/trend")
+	@Operation(summary = "查询用户新鲜度趋势")
+	public R<GetFreshnessTrendVO> getFreshnessTrend(@Valid @RequestBody GetFreshnessTrendDTO reqDto) {
+		GetFreshnessTrendVO result = retentionAnalyseService.getFreshnessTrend(reqDto);
+		return R.ok(result);
+	}
+
+	@PostMapping("/freshness/export")
+	@Operation(summary = "导出用户新鲜度趋势")
+	public R exportFreshnessTrend(@Valid @RequestBody GetFreshnessTrendDTO reqDto) {
+		return R.ok();
+	}
+
+/***************************************** 用户活跃度 *****************************************/
+
+	@PostMapping("/engagement/trend")
+	@Operation(summary = "查询用户活跃度趋势")
+	public R<GetFreshnessTrendVO> getEngagementTrend(@Valid @RequestBody GetFreshnessTrendDTO reqDto) {
+		GetFreshnessTrendVO result = retentionAnalyseService.getEngagementTrend(reqDto);
+		return R.ok(result);
+	}
+
+	@PostMapping("/engagement/export")
+	@Operation(summary = "导出用户活跃度趋势")
+	public R exportEngagementTrend(@Valid @RequestBody GetFreshnessTrendDTO reqDto) {
+		return R.ok();
+	}
+
 }

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

@@ -216,8 +216,7 @@ public class UserAnalyseController {
 				excludeFields.add("wauRate");
 				break;
 			default:
-				// 可根据需要处理默认情况
-				break;
+				throw new BusinessException("时间单位错误");
 		}
 
 		// 2. 分页查询数据(保持原有逻辑)

+ 25 - 0
pig-statistics/pig-statistics-biz/src/main/java/com/pig4cloud/pig/statistics/service/RetentionAnalyseService.java

@@ -2,7 +2,11 @@ package com.pig4cloud.pig.statistics.service;
 
 
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.pig4cloud.pig.statistics.api.dto.retention.GetFreshnessTrendDTO;
+import com.pig4cloud.pig.statistics.api.dto.retention.GetRetentionTrendDTO;
 import com.pig4cloud.pig.statistics.api.dto.retention.PageQueryRetentionDTO;
+import com.pig4cloud.pig.statistics.api.vo.retention.GetFreshnessTrendVO;
+import com.pig4cloud.pig.statistics.api.vo.retention.GetRetentionTrendVO;
 import com.pig4cloud.pig.statistics.api.vo.retention.PageQueryRetentionVO;
 import jakarta.validation.Valid;
 
@@ -20,4 +24,25 @@ public interface RetentionAnalyseService {
 	 * @return 分页数据
 	 */
 	Page<PageQueryRetentionVO> pageRetentionDetail(@Valid PageQueryRetentionDTO reqDto);
+
+	/**
+	 * 查询留存趋势
+	 * @param reqDto 查询参数
+	 * @return 留存趋势数据
+	 */
+    GetRetentionTrendVO getRetentionTrend(@Valid GetRetentionTrendDTO reqDto);
+
+	/**
+	 * 查询用户新鲜度趋势
+	 * @param reqDto 获取参数
+	 * @return 新鲜度趋势数据
+	 */
+	GetFreshnessTrendVO getFreshnessTrend(@Valid GetFreshnessTrendDTO reqDto);
+
+	/**
+	 * 获取用户活跃度趋势
+	 * @param reqDto 获取参数
+	 * @return 活跃度趋势数据
+	 */
+	GetFreshnessTrendVO getEngagementTrend(@Valid GetFreshnessTrendDTO reqDto);
 }

+ 169 - 0
pig-statistics/pig-statistics-biz/src/main/java/com/pig4cloud/pig/statistics/service/impl/RetentionAnalyseServiceImpl.java

@@ -1,8 +1,17 @@
 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.common.core.exception.BusinessException;
+import com.pig4cloud.pig.statistics.api.dto.retention.GetFreshnessTrendDTO;
+import com.pig4cloud.pig.statistics.api.dto.retention.GetRetentionTrendDTO;
 import com.pig4cloud.pig.statistics.api.dto.retention.PageQueryRetentionDTO;
+import com.pig4cloud.pig.statistics.api.entity.user.MktStatActiveUser;
+import com.pig4cloud.pig.statistics.api.entity.user.MktStatNewUser;
+import com.pig4cloud.pig.statistics.api.vo.retention.GetFreshnessTrendVO;
+import com.pig4cloud.pig.statistics.api.vo.retention.GetRetentionTrendVO;
 import com.pig4cloud.pig.statistics.api.vo.retention.PageQueryRetentionVO;
 import com.pig4cloud.pig.statistics.mapper.MktStatActiveUserMapper;
 import com.pig4cloud.pig.statistics.mapper.MktStatNewUserMapper;
@@ -11,6 +20,17 @@ import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 
+import java.time.DayOfWeek;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoUnit;
+import java.time.temporal.TemporalAdjusters;
+import java.util.ArrayList;
+import java.util.List;
+
+import static com.pig4cloud.pig.common.core.util.stat.TimeUtil.*;
+
 /**
  * @author: lwh
  * @date: 2025-08-22
@@ -32,6 +52,155 @@ public class RetentionAnalyseServiceImpl implements RetentionAnalyseService {
 	 */
 	@Override
 	public Page<PageQueryRetentionVO> pageRetentionDetail(PageQueryRetentionDTO reqDto) {
+		if ("day".equals(reqDto.getTimeUnit())){
+			reqDto.setToDate(reqDto.getToDate().minusDays(1));
+		}else if ("week".equals(reqDto.getTimeUnit())){
+			reqDto.setToDate(reqDto.getToDate().minusWeeks(1));
+		}else if ("month".equals(reqDto.getTimeUnit())){
+			reqDto.setToDate(reqDto.getToDate().minusMonths(1));
+		}
+
+		Page<PageQueryRetentionVO> page = new Page<>(reqDto.getCurrent(), reqDto.getSize());
+		// 生成时间坐标轴
+		List<String> dates = generatePageTimeAxis(reqDto.getFromDate(), reqDto.getToDate(), reqDto.getTimeUnit(), reqDto.getCurrent(), reqDto.getSize());
+		// 设置总条数
+		page.setTotal(calculateTotalCount(reqDto.getFromDate(), reqDto.getToDate(), reqDto.getTimeUnit()));
+
+		// 获取渠道和版本
+		List<String> channel = reqDto.getChannel();
+		String version = null;
+		if (reqDto.getVersion() != null && !reqDto.getVersion().isEmpty()){
+			version = reqDto.getVersion().get(0);
+		}
+
+		for (String date : dates) {
+			LocalDateTime startTime = getStartTime(date, reqDto.getTimeUnit());
+			LocalDateTime endTime = getEndTime(date, reqDto.getTimeUnit());
+			List<String> userList;
+			PageQueryRetentionVO result = new PageQueryRetentionVO();
+			result.setDate(date);
+			if ("newUser".equals(reqDto.getType())) {
+				// 查询新增用户数量
+				userList = getNewUserIdList(startTime, endTime, reqDto.getAppId(), version, channel);
+				result.setNewUser(userList.size());
+			}
+			else if ("activeUser".equals(reqDto.getType())) {
+				// 查询活跃用户数量
+				userList = getActiveUserIdList(startTime, endTime, reqDto.getAppId(), version, channel);
+				result.setActiveUser(userList.size());
+			}
+
+			// 计算留存率
+			// 计算一天后的留存率
+			// 计算2天后的留存率
+			// 计算3天后的留存率
+			// 计算4天后的留存率
+			// 计算5天后的留存率
+			// 计算6天后的留存率
+			// 计算7天后的留存率
+			// 计算14天后的留存率
+			// 计算30天后的留存率
+		}
+
 		return null;
 	}
+
+	/**
+	 * 获取留存趋势
+	 * @param reqDto 获取留存趋势参数
+	 * @return GetRetentionTrendVO
+	 */
+	@Override
+	public GetRetentionTrendVO getRetentionTrend(GetRetentionTrendDTO reqDto) {
+		return null;
+	}
+
+	/**
+	 * 获取用户新鲜度趋势
+	 * @param reqDto 获取用户新鲜度趋势参数
+	 * @return GetFreshnessTrendVO
+	 */
+	@Override
+	public GetFreshnessTrendVO getFreshnessTrend(GetFreshnessTrendDTO reqDto) {
+		return null;
+	}
+
+	/**
+	 * 获取用户活跃度趋势
+	 * @param reqDto 获取用户活跃度趋势参数
+	 * @return GetFreshnessTrendVO
+	 */
+	@Override
+	public GetFreshnessTrendVO getEngagementTrend(GetFreshnessTrendDTO reqDto) {
+		return null;
+	}
+
+
+	/************************************** 公用方法 **************************************
+	 * 统计指定时间范围内的新增用户数
+	 */
+	private Long statNewUserCount(LocalDateTime startTime, LocalDateTime endTime, String appId, String version, List<String> channels){
+		List<String> newUsers = getNewUserIdList(startTime, endTime, appId, version, channels);
+		return newUsers == null ? 0L : newUsers.size();
+	}
+
+	/**
+	 * 统计指定时间范围内的活跃用户数
+	 */
+	private Long statActiveUserCount(LocalDateTime startTime, LocalDateTime endTime, String appId, String version, List<String> channels){
+		List<String> activeUsers = getActiveUserIdList(startTime, endTime, appId, version, channels);
+		return activeUsers == null ? 0L : activeUsers.size();
+	}
+
+	/**
+	 * 查询指定时间范围内的活跃用户Id列表(去重)
+	 */
+	private List<String> getActiveUserIdList(LocalDateTime startTime, LocalDateTime endTime, String appId, String version, List<String> channels){
+		// 1. 组装查询条件
+		LambdaQueryWrapper<MktStatActiveUser> queryWrapper = Wrappers.<MktStatActiveUser>lambdaQuery()
+				.eq(MktStatActiveUser::getAppId, appId)
+				.ge(startTime != null, MktStatActiveUser::getStatDate, startTime)
+				.lt(endTime != null, MktStatActiveUser::getStatDate, endTime)
+				.select(MktStatActiveUser::getUserId)
+				.groupBy(MktStatActiveUser::getUserId);
+
+		// 2. 添加渠道条件
+		if (channels != null && !channels.isEmpty()) {
+			queryWrapper.in(MktStatActiveUser::getChannel, channels);
+		}
+		// 3. 添加版本条件
+		if (version != null && !version.isEmpty()){
+			queryWrapper.eq(MktStatActiveUser::getVersion, version);
+		}
+		// 4. 统计活跃用户数,根据userID去重
+		List<MktStatActiveUser> activeUsers = activeUserMapper.selectList(queryWrapper);
+		return activeUsers.stream().map(MktStatActiveUser::getUserId).toList();
+	}
+
+	/**
+	 * 查询指定时间范围内的新增用户Id列表(去重)
+	 */
+	private List<String> getNewUserIdList(LocalDateTime startTime, LocalDateTime endTime, String appId, String version, List<String> channels){
+
+		// 1. 组装查询条件
+		LambdaQueryWrapper<MktStatNewUser> queryWrapper = Wrappers.<MktStatNewUser>lambdaQuery()
+				.eq(MktStatNewUser::getAppId, appId)
+				.ge(startTime != null ,MktStatNewUser::getStatDate, startTime)
+				.lt(endTime != null ,MktStatNewUser::getStatDate, endTime)
+				.select(MktStatNewUser::getUserId)
+				.groupBy(MktStatNewUser::getUserId);
+
+		// 2. 添加渠道条件
+		if (channels != null && !channels.isEmpty()) {
+			queryWrapper.in(MktStatNewUser::getChannel, channels);
+		}
+		// 3. 添加版本条件
+		if (version != null && !version.isEmpty()){
+			queryWrapper.eq(MktStatNewUser::getVersion, version);
+		}
+		// 4. 统计新增用户数
+		List<MktStatNewUser> newUsers = newUserMapper.selectList(queryWrapper);
+		return newUsers.stream().map(MktStatNewUser::getUserId).toList();
+	}
+
 }

+ 2 - 241
pig-statistics/pig-statistics-biz/src/main/java/com/pig4cloud/pig/statistics/service/impl/UserAnalyseServiceImpl.java

@@ -21,16 +21,14 @@ import org.springframework.stereotype.Service;
 
 import java.math.BigDecimal;
 import java.math.RoundingMode;
-import java.time.DayOfWeek;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
-import java.time.format.DateTimeParseException;
-import java.time.temporal.ChronoUnit;
-import java.time.temporal.TemporalAdjusters;
 import java.util.*;
 import java.util.stream.Collectors;
 
+import static com.pig4cloud.pig.common.core.util.stat.TimeUtil.*;
+
 
 /**
  * @author: lwh
@@ -1164,243 +1162,6 @@ public class UserAnalyseServiceImpl implements UserAnalyseService {
 
 
 	/************************************** 公用方法 **************************************
-	 * 根据时间粒度和日期字符串获取开始时间
-	 */
-	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":
-				// 计算月数:基于月份第一天计算,与generatePageTimeAxis逻辑保持一致
-				LocalDate firstMonDay = fromDate.with(TemporalAdjusters.firstDayOfMonth());
-				LocalDate lastMonDay = toDate.with(TemporalAdjusters.firstDayOfMonth());
-				return ChronoUnit.MONTHS.between(firstMonDay, lastMonDay) + 1;
-
-			default:
-				throw new IllegalArgumentException("不支持的时间单位: " + timeUnit);
-		}
-	}
-
-	/**
 	 * 统计指定时间范围内的新增用户数
 	 */
 	private Long statNewUserCount(LocalDateTime startTime, LocalDateTime endTime, String appId, String version, List<String> channels){