|
|
@@ -4,16 +4,28 @@ import com.fs.common.core.redis.RedisCache;
|
|
|
import com.fs.his.mapper.AppOperationReportMapper;
|
|
|
import com.fs.his.param.AppOperationReportParam;
|
|
|
import com.fs.his.service.IAppOperationReportService;
|
|
|
+import com.fs.his.vo.AppDailyReportVO;
|
|
|
import com.fs.his.vo.AppOperationReportVO;
|
|
|
+import com.fs.his.vo.AppWeeklyReportVO;
|
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
|
import org.springframework.stereotype.Service;
|
|
|
|
|
|
import java.math.BigDecimal;
|
|
|
import java.math.RoundingMode;
|
|
|
import java.text.SimpleDateFormat;
|
|
|
-import java.util.Calendar;
|
|
|
+import java.util.*;
|
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
|
|
+/**
|
|
|
+ * APP运营报表Service实现类
|
|
|
+ *
|
|
|
+ * 提供三种维度的运营数据报表:
|
|
|
+ * 1. 月度报表 - 按月统计各项运营指标
|
|
|
+ * 2. 日报表 - 按日统计各项运营指标,支持分页
|
|
|
+ * 3. 周报表 - 按周统计各项运营指标,支持分页
|
|
|
+ *
|
|
|
+ * 支持按APP版本筛选数据(动态参数)
|
|
|
+ */
|
|
|
@Service
|
|
|
public class AppOperationReportServiceImpl implements IAppOperationReportService {
|
|
|
|
|
|
@@ -27,11 +39,23 @@ public class AppOperationReportServiceImpl implements IAppOperationReportService
|
|
|
|
|
|
/**
|
|
|
* 获取月度运营报表
|
|
|
- * @param param 查询参数(year、month、retentionDays)
|
|
|
+ *
|
|
|
+ * 统计指标说明:
|
|
|
+ * - 新增用户数:当月新注册的APP用户数
|
|
|
+ * - 累计用户数:截至当月末的APP总用户数
|
|
|
+ * - 活跃用户数:当月有使用行为的用户数
|
|
|
+ * - 留存率:N天后仍活跃的用户占新增用户的比例
|
|
|
+ * - 平均使用时长:活跃用户的平均使用时长(分钟)
|
|
|
+ * - LTV(用户生命周期价值):累计订单金额/累计用户数
|
|
|
+ * - CAC(用户获取成本):红包发放金额/新增用户数
|
|
|
+ * - 月流失率:上月活跃用户中本月未活跃的比例
|
|
|
+ *
|
|
|
+ * @param param 查询参数(year、month、retentionDays、appVersion)
|
|
|
* @return 月度运营报表数据
|
|
|
*/
|
|
|
@Override
|
|
|
public AppOperationReportVO getMonthReport(AppOperationReportParam param) {
|
|
|
+ // 1. 解析年月参数,默认为当前年月
|
|
|
Integer year = param.getYear();
|
|
|
Integer month = param.getMonth();
|
|
|
|
|
|
@@ -44,70 +68,503 @@ public class AppOperationReportServiceImpl implements IAppOperationReportService
|
|
|
month = cal.get(Calendar.MONTH) + 1;
|
|
|
}
|
|
|
|
|
|
- String cacheKey = CACHE_PREFIX + year + ":" + month;
|
|
|
+ // 2. 构建缓存key(包含版本参数,确保不同版本筛选使用不同缓存)
|
|
|
+ String appVersion = param.getAppVersion();
|
|
|
+ String cacheKey = CACHE_PREFIX + year + ":" + month + ":" + (appVersion != null ? appVersion : "all");
|
|
|
AppOperationReportVO cachedVo = redisCache.getCacheObject(cacheKey);
|
|
|
if (cachedVo != null) {
|
|
|
return cachedVo;
|
|
|
}
|
|
|
|
|
|
+ // 3. 计算月份起止日期
|
|
|
String monthStart = year + "-" + String.format("%02d", month) + "-01";
|
|
|
String monthEnd = year + "-" + String.format("%02d", month) + "-" + getLastDayOfMonth(year, month);
|
|
|
|
|
|
+ // 4. 构建返回对象
|
|
|
AppOperationReportVO vo = new AppOperationReportVO();
|
|
|
vo.setYear(year);
|
|
|
vo.setMonth(month);
|
|
|
vo.setPeriod("月度统计");
|
|
|
|
|
|
- Long newUsers = appOperationReportMapper.selectNewUsers(monthStart + " 00:00:00", monthEnd + " 23:59:59");
|
|
|
+ // 5. 查询新增用户数
|
|
|
+ Long newUsers = appOperationReportMapper.selectNewUsers(monthStart + " 00:00:00", monthEnd + " 23:59:59", appVersion);
|
|
|
vo.setNewUsers(newUsers);
|
|
|
|
|
|
- Long totalUsers = appOperationReportMapper.selectTotalUsers();
|
|
|
+ // 6. 查询累计用户数
|
|
|
+ Long totalUsers = appOperationReportMapper.selectTotalUsers(appVersion);
|
|
|
vo.setTotalUsers(totalUsers);
|
|
|
|
|
|
- Long activeUsers = appOperationReportMapper.selectActiveUsers(monthStart, monthEnd);
|
|
|
+ // 7. 查询活跃用户数
|
|
|
+ Long activeUsers = appOperationReportMapper.selectActiveUsers(monthStart, monthEnd, appVersion);
|
|
|
vo.setActiveUsers(activeUsers);
|
|
|
|
|
|
+ // 8. 计算留存率(默认30日留存)
|
|
|
if (newUsers != null && newUsers > 0) {
|
|
|
int retentionDays = param.getRetentionDays() != null ? param.getRetentionDays() : 30;
|
|
|
Long retained = appOperationReportMapper.selectRetainedUsers(
|
|
|
monthStart + " 00:00:00", monthEnd + " 23:59:59",
|
|
|
- getAfterDate(monthStart, retentionDays) + " 00:00:00", getAfterDate(monthEnd, retentionDays) + " 23:59:59");
|
|
|
+ getAfterDate(monthStart, retentionDays) + " 00:00:00", getAfterDate(monthEnd, retentionDays) + " 23:59:59", appVersion);
|
|
|
vo.setRetentionRate(calculateRate(retained, newUsers));
|
|
|
vo.setRetentionDays(retentionDays);
|
|
|
}
|
|
|
|
|
|
+ // 9. 计算平均使用时长(分钟)
|
|
|
if (activeUsers != null && activeUsers > 0) {
|
|
|
- Long totalDuration = appOperationReportMapper.selectTotalWatchDuration(monthStart + " 00:00:00", monthEnd + " 23:59:59");
|
|
|
+ Long totalDuration = appOperationReportMapper.selectTotalWatchDuration(monthStart + " 00:00:00", monthEnd + " 23:59:59", appVersion);
|
|
|
BigDecimal avgDuration = new BigDecimal(totalDuration).divide(new BigDecimal(activeUsers), 2, RoundingMode.HALF_UP);
|
|
|
vo.setAvgUseDuration(avgDuration.divide(new BigDecimal(60), 2, RoundingMode.HALF_UP));
|
|
|
}
|
|
|
|
|
|
+ // 10. 计算LTV(用户生命周期价值)
|
|
|
if (totalUsers != null && totalUsers > 0) {
|
|
|
- BigDecimal totalAmount = appOperationReportMapper.selectTotalOrderAmount(monthEnd + " 23:59:59");
|
|
|
+ BigDecimal totalAmount = appOperationReportMapper.selectTotalOrderAmount(monthEnd + " 23:59:59", appVersion);
|
|
|
vo.setLtv(totalAmount.divide(new BigDecimal(totalUsers), 2, RoundingMode.HALF_UP));
|
|
|
}
|
|
|
|
|
|
+ // 11. 计算CAC(用户获取成本)
|
|
|
if (newUsers != null && newUsers > 0) {
|
|
|
- BigDecimal redPacketAmount = appOperationReportMapper.selectTotalRedPacketAmount(monthStart + " 00:00:00", monthEnd + " 23:59:59");
|
|
|
+ BigDecimal redPacketAmount = appOperationReportMapper.selectTotalRedPacketAmount(monthStart + " 00:00:00", monthEnd + " 23:59:59", appVersion);
|
|
|
vo.setCac(redPacketAmount.divide(new BigDecimal(newUsers), 2, RoundingMode.HALF_UP));
|
|
|
}
|
|
|
|
|
|
+ // 12. 计算月流失率
|
|
|
String[] lastMonthDates = getLastMonthDateRange(year, month);
|
|
|
- Long lastMonthActive = appOperationReportMapper.selectLastMonthActiveUsers(lastMonthDates[0], lastMonthDates[1]);
|
|
|
+ Long lastMonthActive = appOperationReportMapper.selectLastMonthActiveUsers(lastMonthDates[0], lastMonthDates[1], appVersion);
|
|
|
if (lastMonthActive != null && lastMonthActive > 0) {
|
|
|
Long currentMonthInactive = appOperationReportMapper.selectCurrentMonthInactiveUsers(
|
|
|
- lastMonthDates[0], lastMonthDates[1], monthStart, monthEnd);
|
|
|
+ lastMonthDates[0], lastMonthDates[1], monthStart, monthEnd, appVersion);
|
|
|
vo.setMonthlyChurnRate(calculateRate(currentMonthInactive, lastMonthActive));
|
|
|
}
|
|
|
|
|
|
+ // 13. 写入缓存
|
|
|
int cacheMinutes = isCurrentMonth(year, month) ? 5 : 60;
|
|
|
redisCache.setCacheObject(cacheKey, vo, cacheMinutes, TimeUnit.MINUTES);
|
|
|
|
|
|
return vo;
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 获取日运营报表
|
|
|
+ *
|
|
|
+ * 统计指标说明:
|
|
|
+ * - 新增用户数:当日新注册的APP用户数
|
|
|
+ * - 累计用户数:截至当日的APP总用户数
|
|
|
+ * - 活跃用户数:当日有使用行为的用户数
|
|
|
+ * - 次日留存率:当日新增用户中次日仍活跃的比例
|
|
|
+ * - 平均使用时长:活跃用户的平均使用时长(分钟)
|
|
|
+ * - LTV(用户生命周期价值):累计订单金额/累计用户数
|
|
|
+ * - CAC(用户获取成本):当日红包发放金额/当日新增用户数
|
|
|
+ * - 日流失率:前日活跃用户中当日未活跃的比例
|
|
|
+ *
|
|
|
+ * @param param 查询参数(startDate、endDate、pageNum、pageSize、appVersion)
|
|
|
+ * @return 日运营报表数据列表
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ public List<AppDailyReportVO> getDailyReport(AppOperationReportParam param) {
|
|
|
+ // 1. 解析日期参数,默认查询近30天
|
|
|
+ String startDate = param.getStartDate();
|
|
|
+ String endDate = param.getEndDate();
|
|
|
+ String appVersion = param.getAppVersion();
|
|
|
+
|
|
|
+ if (startDate == null || endDate == null) {
|
|
|
+ Calendar cal = Calendar.getInstance();
|
|
|
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
|
|
|
+ endDate = sdf.format(cal.getTime());
|
|
|
+ cal.add(Calendar.DAY_OF_MONTH, -29);
|
|
|
+ startDate = sdf.format(cal.getTime());
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 批量查询各维度数据(减少数据库查询次数)
|
|
|
+ List<AppDailyReportVO> dailyNewUsers = appOperationReportMapper.selectDailyNewUsers(startDate, endDate, appVersion);
|
|
|
+ List<AppDailyReportVO> dailyActiveUsers = appOperationReportMapper.selectDailyActiveUsers(startDate, endDate, appVersion);
|
|
|
+ List<AppDailyReportVO> dailyWatchDuration = appOperationReportMapper.selectDailyWatchDuration(startDate, endDate, appVersion);
|
|
|
+ List<AppDailyReportVO> dailyRedPacketAmount = appOperationReportMapper.selectDailyRedPacketAmount(startDate, endDate, appVersion);
|
|
|
+
|
|
|
+ // 3. 构建日期到数据的映射(便于后续按日期查找)
|
|
|
+ Map<String, AppDailyReportVO> newUsersMap = new LinkedHashMap<>();
|
|
|
+ for (AppDailyReportVO vo : dailyNewUsers) {
|
|
|
+ String key = formatDate(vo.getStatDate());
|
|
|
+ newUsersMap.put(key, vo);
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<String, AppDailyReportVO> activeUsersMap = new LinkedHashMap<>();
|
|
|
+ for (AppDailyReportVO vo : dailyActiveUsers) {
|
|
|
+ String key = formatDate(vo.getStatDate());
|
|
|
+ activeUsersMap.put(key, vo);
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<String, AppDailyReportVO> watchDurationMap = new LinkedHashMap<>();
|
|
|
+ for (AppDailyReportVO vo : dailyWatchDuration) {
|
|
|
+ String key = formatDate(vo.getStatDate());
|
|
|
+ watchDurationMap.put(key, vo);
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<String, AppDailyReportVO> redPacketMap = new LinkedHashMap<>();
|
|
|
+ for (AppDailyReportVO vo : dailyRedPacketAmount) {
|
|
|
+ String key = formatDate(vo.getStatDate());
|
|
|
+ redPacketMap.put(key, vo);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4. 查询累计数据(截至结束日期)
|
|
|
+ Long totalUsers = appOperationReportMapper.selectTotalUsers(appVersion);
|
|
|
+ BigDecimal totalOrderAmount = appOperationReportMapper.selectTotalOrderAmount(endDate + " 23:59:59", appVersion);
|
|
|
+
|
|
|
+ // 5. 生成日期范围内所有日期列表
|
|
|
+ List<String> allDates = new ArrayList<>();
|
|
|
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
|
|
|
+ try {
|
|
|
+ Calendar cal = Calendar.getInstance();
|
|
|
+ cal.setTime(sdf.parse(startDate));
|
|
|
+ Calendar endCal = Calendar.getInstance();
|
|
|
+ endCal.setTime(sdf.parse(endDate));
|
|
|
+ while (!cal.after(endCal)) {
|
|
|
+ allDates.add(sdf.format(cal.getTime()));
|
|
|
+ cal.add(Calendar.DAY_OF_MONTH, 1);
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ e.printStackTrace();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 6. 分页处理
|
|
|
+ int totalDays = allDates.size();
|
|
|
+ Integer pageNum = param.getPageNum();
|
|
|
+ Integer pageSize = param.getPageSize();
|
|
|
+
|
|
|
+ List<String> pageDates;
|
|
|
+ if (pageNum != null && pageSize != null && pageNum > 0 && pageSize > 0) {
|
|
|
+ int fromIndex = (pageNum - 1) * pageSize;
|
|
|
+ int toIndex = Math.min(fromIndex + pageSize, totalDays);
|
|
|
+ if (fromIndex >= totalDays) {
|
|
|
+ pageDates = new ArrayList<>();
|
|
|
+ } else {
|
|
|
+ pageDates = allDates.subList(fromIndex, toIndex);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ pageDates = allDates;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 7. 遍历每条日期,组装报表数据
|
|
|
+ List<AppDailyReportVO> result = new ArrayList<>();
|
|
|
+ for (String dateStr : pageDates) {
|
|
|
+ AppDailyReportVO vo = new AppDailyReportVO();
|
|
|
+ try {
|
|
|
+ vo.setStatDate(sdf.parse(dateStr));
|
|
|
+ } catch (Exception e) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 7.1 设置新增用户数
|
|
|
+ AppDailyReportVO newVo = newUsersMap.get(dateStr);
|
|
|
+ vo.setNewUsers(newVo != null ? newVo.getNewUsers() : 0L);
|
|
|
+
|
|
|
+ // 7.2 设置累计用户数
|
|
|
+ vo.setTotalUsers(totalUsers);
|
|
|
+
|
|
|
+ // 7.3 设置活跃用户数
|
|
|
+ AppDailyReportVO activeVo = activeUsersMap.get(dateStr);
|
|
|
+ vo.setActiveUsers(activeVo != null ? activeVo.getActiveUsers() : 0L);
|
|
|
+
|
|
|
+ // 7.4 计算次日留存率
|
|
|
+ String prevDate = getPrevDate(dateStr);
|
|
|
+ Long dayNewUsers = appOperationReportMapper.selectDayNewUsers(dateStr, appVersion);
|
|
|
+ if (dayNewUsers != null && dayNewUsers > 0) {
|
|
|
+ String nextDate = getNextDate(dateStr);
|
|
|
+ Long nextDayRetained = appOperationReportMapper.selectNextDayRetainedUsers(dateStr, nextDate, appVersion);
|
|
|
+ vo.setNextDayRetentionRate(calculateRate(nextDayRetained, dayNewUsers));
|
|
|
+ } else {
|
|
|
+ vo.setNextDayRetentionRate(BigDecimal.ZERO);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 7.5 计算平均使用时长
|
|
|
+ AppDailyReportVO watchVo = watchDurationMap.get(dateStr);
|
|
|
+ Long dayDuration = (watchVo != null && watchVo.getTotalWatchDuration() != null) ? watchVo.getTotalWatchDuration() : 0L;
|
|
|
+ if (vo.getActiveUsers() != null && vo.getActiveUsers() > 0) {
|
|
|
+ BigDecimal avgDuration = new BigDecimal(dayDuration)
|
|
|
+ .divide(new BigDecimal(vo.getActiveUsers()), 2, RoundingMode.HALF_UP);
|
|
|
+ vo.setAvgUseDuration(avgDuration.divide(new BigDecimal(60), 2, RoundingMode.HALF_UP));
|
|
|
+ } else {
|
|
|
+ vo.setAvgUseDuration(BigDecimal.ZERO);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 7.6 计算LTV
|
|
|
+ if (totalUsers != null && totalUsers > 0 && totalOrderAmount != null) {
|
|
|
+ vo.setLtv(totalOrderAmount.divide(new BigDecimal(totalUsers), 2, RoundingMode.HALF_UP));
|
|
|
+ } else {
|
|
|
+ vo.setLtv(BigDecimal.ZERO);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 7.7 计算CAC
|
|
|
+ AppDailyReportVO redVo = redPacketMap.get(dateStr);
|
|
|
+ BigDecimal dayRedPacket = (redVo != null && redVo.getTotalRedPacketAmount() != null) ? redVo.getTotalRedPacketAmount() : BigDecimal.ZERO;
|
|
|
+ if (vo.getNewUsers() != null && vo.getNewUsers() > 0) {
|
|
|
+ vo.setCac(dayRedPacket.divide(new BigDecimal(vo.getNewUsers()), 2, RoundingMode.HALF_UP));
|
|
|
+ } else {
|
|
|
+ vo.setCac(BigDecimal.ZERO);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 7.8 计算日流失率
|
|
|
+ Long prevDayActive = appOperationReportMapper.selectPrevDayActiveUsers(prevDate, appVersion);
|
|
|
+ if (prevDayActive != null && prevDayActive > 0) {
|
|
|
+ Long churnUsers = appOperationReportMapper.selectDailyChurnUsers(prevDate, dateStr, appVersion);
|
|
|
+ vo.setDailyChurnRate(calculateRate(churnUsers, prevDayActive));
|
|
|
+ } else {
|
|
|
+ vo.setDailyChurnRate(BigDecimal.ZERO);
|
|
|
+ }
|
|
|
+
|
|
|
+ result.add(vo);
|
|
|
+ }
|
|
|
+
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取周运营报表
|
|
|
+ *
|
|
|
+ * 统计指标说明:
|
|
|
+ * - 新增用户数:当周新注册的APP用户数
|
|
|
+ * - 累计用户数:截至当周末的APP总用户数
|
|
|
+ * - 活跃用户数:当周有使用行为的用户数
|
|
|
+ * - 留存率:当周新增用户中下周仍活跃的比例
|
|
|
+ * - 平均使用时长:活跃用户的平均使用时长(分钟)
|
|
|
+ * - LTV(用户生命周期价值):累计订单金额/累计用户数
|
|
|
+ * - CAC(用户获取成本):当周红包发放金额/当周新增用户数
|
|
|
+ * - 周流失率:上周活跃用户中本周未活跃的比例
|
|
|
+ *
|
|
|
+ * @param param 查询参数(startDate、endDate、pageNum、pageSize、appVersion)
|
|
|
+ * @return 周运营报表数据列表
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ public List<AppWeeklyReportVO> getWeeklyReport(AppOperationReportParam param) {
|
|
|
+ // 1. 解析日期参数,默认查询近90天
|
|
|
+ String startDate = param.getStartDate();
|
|
|
+ String endDate = param.getEndDate();
|
|
|
+ String appVersion = param.getAppVersion();
|
|
|
+
|
|
|
+ if (startDate == null || endDate == null) {
|
|
|
+ Calendar cal = Calendar.getInstance();
|
|
|
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
|
|
|
+ endDate = sdf.format(cal.getTime());
|
|
|
+ cal.add(Calendar.DAY_OF_MONTH, -89);
|
|
|
+ startDate = sdf.format(cal.getTime());
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 将日期范围按周拆分
|
|
|
+ List<String[]> weeks = splitIntoWeeks(startDate, endDate);
|
|
|
+
|
|
|
+ // 3. 分页处理
|
|
|
+ Integer pageNum = param.getPageNum();
|
|
|
+ Integer pageSize = param.getPageSize();
|
|
|
+
|
|
|
+ List<String[]> pageWeeks;
|
|
|
+ if (pageNum != null && pageSize != null && pageNum > 0 && pageSize > 0) {
|
|
|
+ int totalWeeks = weeks.size();
|
|
|
+ int fromIndex = (pageNum - 1) * pageSize;
|
|
|
+ int toIndex = Math.min(fromIndex + pageSize, totalWeeks);
|
|
|
+ if (fromIndex >= totalWeeks) {
|
|
|
+ pageWeeks = new ArrayList<>();
|
|
|
+ } else {
|
|
|
+ pageWeeks = weeks.subList(fromIndex, toIndex);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ pageWeeks = weeks;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4. 查询累计数据(截至结束日期)
|
|
|
+ Long totalUsers = appOperationReportMapper.selectTotalUsers(appVersion);
|
|
|
+ BigDecimal totalOrderAmount = appOperationReportMapper.selectTotalOrderAmount(endDate + " 23:59:59", appVersion);
|
|
|
+
|
|
|
+ // 5. 遍历每周,组装报表数据
|
|
|
+ List<AppWeeklyReportVO> result = new ArrayList<>();
|
|
|
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
|
|
|
+
|
|
|
+ for (int i = 0; i < pageWeeks.size(); i++) {
|
|
|
+ String[] weekRange = pageWeeks.get(i);
|
|
|
+ String weekStart = weekRange[0];
|
|
|
+ String weekEnd = weekRange[1];
|
|
|
+
|
|
|
+ AppWeeklyReportVO vo = new AppWeeklyReportVO();
|
|
|
+ try {
|
|
|
+ vo.setWeekStart(sdf.parse(weekStart));
|
|
|
+ vo.setWeekEnd(sdf.parse(weekEnd));
|
|
|
+ } catch (Exception e) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 5.1 查询新增用户数
|
|
|
+ Long newUsers = appOperationReportMapper.selectNewUsers(weekStart + " 00:00:00", weekEnd + " 23:59:59", appVersion);
|
|
|
+ vo.setNewUsers(newUsers);
|
|
|
+
|
|
|
+ // 5.2 设置累计用户数
|
|
|
+ vo.setTotalUsers(totalUsers);
|
|
|
+
|
|
|
+ // 5.3 查询活跃用户数
|
|
|
+ Long activeUsers = appOperationReportMapper.selectActiveUsers(weekStart, weekEnd, appVersion);
|
|
|
+ vo.setActiveUsers(activeUsers);
|
|
|
+
|
|
|
+ // 5.4 计算留存率(下周留存)
|
|
|
+ if (newUsers != null && newUsers > 0) {
|
|
|
+ String nextWeekStart = getNextDate(weekEnd);
|
|
|
+ String nextWeekEndDate = getAfterDate(nextWeekStart, 6);
|
|
|
+ Long retained = appOperationReportMapper.selectRetainedUsers(
|
|
|
+ weekStart + " 00:00:00", weekEnd + " 23:59:59",
|
|
|
+ nextWeekStart, nextWeekEndDate, appVersion);
|
|
|
+ vo.setRetentionRate(calculateRate(retained, newUsers));
|
|
|
+ } else {
|
|
|
+ vo.setRetentionRate(BigDecimal.ZERO);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 5.5 计算平均使用时长
|
|
|
+ if (activeUsers != null && activeUsers > 0) {
|
|
|
+ Long totalDuration = appOperationReportMapper.selectTotalWatchDuration(weekStart + " 00:00:00", weekEnd + " 23:59:59", appVersion);
|
|
|
+ BigDecimal avgDuration = new BigDecimal(totalDuration).divide(new BigDecimal(activeUsers), 2, RoundingMode.HALF_UP);
|
|
|
+ vo.setAvgUseDuration(avgDuration.divide(new BigDecimal(60), 2, RoundingMode.HALF_UP));
|
|
|
+ } else {
|
|
|
+ vo.setAvgUseDuration(BigDecimal.ZERO);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 5.6 计算LTV
|
|
|
+ if (totalUsers != null && totalUsers > 0 && totalOrderAmount != null) {
|
|
|
+ vo.setLtv(totalOrderAmount.divide(new BigDecimal(totalUsers), 2, RoundingMode.HALF_UP));
|
|
|
+ } else {
|
|
|
+ vo.setLtv(BigDecimal.ZERO);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 5.7 计算CAC
|
|
|
+ if (newUsers != null && newUsers > 0) {
|
|
|
+ BigDecimal redPacketAmount = appOperationReportMapper.selectTotalRedPacketAmount(weekStart + " 00:00:00", weekEnd + " 23:59:59", appVersion);
|
|
|
+ vo.setCac(redPacketAmount.divide(new BigDecimal(newUsers), 2, RoundingMode.HALF_UP));
|
|
|
+ } else {
|
|
|
+ vo.setCac(BigDecimal.ZERO);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 5.8 计算周流失率
|
|
|
+ if (i > 0) {
|
|
|
+ // 非第一周:使用分页列表中的前一周数据
|
|
|
+ String[] prevWeekRange = pageWeeks.get(i - 1);
|
|
|
+ String prevWeekStart = prevWeekRange[0];
|
|
|
+ String prevWeekEnd = prevWeekRange[1];
|
|
|
+ Long prevWeekActive = appOperationReportMapper.selectLastMonthActiveUsers(prevWeekStart, prevWeekEnd, appVersion);
|
|
|
+ if (prevWeekActive != null && prevWeekActive > 0) {
|
|
|
+ Long churnUsers = appOperationReportMapper.selectCurrentMonthInactiveUsers(
|
|
|
+ prevWeekStart, prevWeekEnd, weekStart, weekEnd, appVersion);
|
|
|
+ vo.setWeeklyChurnRate(calculateRate(churnUsers, prevWeekActive));
|
|
|
+ } else {
|
|
|
+ vo.setWeeklyChurnRate(BigDecimal.ZERO);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 第一周:需要查询前一周的数据
|
|
|
+ String prevWeekEnd = getPrevDate(weekStart);
|
|
|
+ String prevWeekStart = getAfterDate(prevWeekEnd, -6);
|
|
|
+ Long prevWeekActive = appOperationReportMapper.selectLastMonthActiveUsers(prevWeekStart, prevWeekEnd, appVersion);
|
|
|
+ if (prevWeekActive != null && prevWeekActive > 0) {
|
|
|
+ Long churnUsers = appOperationReportMapper.selectCurrentMonthInactiveUsers(
|
|
|
+ prevWeekStart, prevWeekEnd, weekStart, weekEnd, appVersion);
|
|
|
+ vo.setWeeklyChurnRate(calculateRate(churnUsers, prevWeekActive));
|
|
|
+ } else {
|
|
|
+ vo.setWeeklyChurnRate(BigDecimal.ZERO);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ result.add(vo);
|
|
|
+ }
|
|
|
+
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 将日期范围按周拆分
|
|
|
+ * 从起始日期所在周的周一开始,到结束日期所在周的周日结束
|
|
|
+ *
|
|
|
+ * @param startDate 起始日期
|
|
|
+ * @param endDate 结束日期
|
|
|
+ * @return 周列表,每个元素为[周开始日期, 周结束日期]
|
|
|
+ */
|
|
|
+ private List<String[]> splitIntoWeeks(String startDate, String endDate) {
|
|
|
+ List<String[]> weeks = new ArrayList<>();
|
|
|
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
|
|
|
+ try {
|
|
|
+ Calendar cal = Calendar.getInstance();
|
|
|
+ cal.setTime(sdf.parse(startDate));
|
|
|
+ // 调整到所在周的周一
|
|
|
+ cal.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY);
|
|
|
+ Calendar endCal = Calendar.getInstance();
|
|
|
+ endCal.setTime(sdf.parse(endDate));
|
|
|
+
|
|
|
+ Calendar weekStart = (Calendar) cal.clone();
|
|
|
+ while (!weekStart.after(endCal)) {
|
|
|
+ // 周结束日期为周日(开始日期+6天)
|
|
|
+ Calendar weekEnd = (Calendar) weekStart.clone();
|
|
|
+ weekEnd.add(Calendar.DAY_OF_MONTH, 6);
|
|
|
+ // 如果超过结束日期,则使用结束日期
|
|
|
+ if (weekEnd.after(endCal)) {
|
|
|
+ weekEnd = (Calendar) endCal.clone();
|
|
|
+ }
|
|
|
+ String ws = sdf.format(weekStart.getTime());
|
|
|
+ String we = sdf.format(weekEnd.getTime());
|
|
|
+ weeks.add(new String[]{ws, we});
|
|
|
+ // 移动到下一周
|
|
|
+ weekStart.add(Calendar.DAY_OF_MONTH, 7);
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ e.printStackTrace();
|
|
|
+ }
|
|
|
+ return weeks;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 格式化日期为字符串
|
|
|
+ *
|
|
|
+ * @param date 日期对象
|
|
|
+ * @return 格式化后的日期字符串(yyyy-MM-dd)
|
|
|
+ */
|
|
|
+ private String formatDate(Date date) {
|
|
|
+ if (date == null) return "";
|
|
|
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
|
|
|
+ return sdf.format(date);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取前一天的日期
|
|
|
+ *
|
|
|
+ * @param dateStr 当前日期字符串
|
|
|
+ * @return 前一天的日期字符串
|
|
|
+ */
|
|
|
+ private String getPrevDate(String dateStr) {
|
|
|
+ try {
|
|
|
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
|
|
|
+ Calendar cal = Calendar.getInstance();
|
|
|
+ cal.setTime(sdf.parse(dateStr));
|
|
|
+ cal.add(Calendar.DAY_OF_MONTH, -1);
|
|
|
+ return sdf.format(cal.getTime());
|
|
|
+ } catch (Exception e) {
|
|
|
+ return dateStr;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取后一天的日期
|
|
|
+ *
|
|
|
+ * @param dateStr 当前日期字符串
|
|
|
+ * @return 后一天的日期字符串
|
|
|
+ */
|
|
|
+ private String getNextDate(String dateStr) {
|
|
|
+ try {
|
|
|
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
|
|
|
+ Calendar cal = Calendar.getInstance();
|
|
|
+ cal.setTime(sdf.parse(dateStr));
|
|
|
+ cal.add(Calendar.DAY_OF_MONTH, 1);
|
|
|
+ return sdf.format(cal.getTime());
|
|
|
+ } catch (Exception e) {
|
|
|
+ return dateStr;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 判断是否为当前月份
|
|
|
+ *
|
|
|
+ * @param year 年份
|
|
|
+ * @param month 月份
|
|
|
+ * @return 是否为当前月份
|
|
|
*/
|
|
|
private boolean isCurrentMonth(int year, int month) {
|
|
|
Calendar cal = Calendar.getInstance();
|
|
|
@@ -115,10 +572,11 @@ public class AppOperationReportServiceImpl implements IAppOperationReportService
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 计算比率
|
|
|
+ * 计算比率(百分比)
|
|
|
+ *
|
|
|
* @param part 部分值
|
|
|
* @param total 总值
|
|
|
- * @return 比率(百分比)
|
|
|
+ * @return 比率(百分比,保留两位小数)
|
|
|
*/
|
|
|
private BigDecimal calculateRate(Long part, Long total) {
|
|
|
if (total == null || total == 0 || part == null) {
|
|
|
@@ -129,10 +587,11 @@ public class AppOperationReportServiceImpl implements IAppOperationReportService
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 获取指定日期后的日期
|
|
|
+ * 获取指定日期后N天的日期
|
|
|
+ *
|
|
|
* @param date 基准日期
|
|
|
- * @param days 天数
|
|
|
- * @return 计算后的日期
|
|
|
+ * @param days 天数(可为负数,表示往前推)
|
|
|
+ * @return 计算后的日期字符串
|
|
|
*/
|
|
|
private String getAfterDate(String date, int days) {
|
|
|
try {
|
|
|
@@ -148,6 +607,7 @@ public class AppOperationReportServiceImpl implements IAppOperationReportService
|
|
|
|
|
|
/**
|
|
|
* 获取指定月份的最后一天
|
|
|
+ *
|
|
|
* @param year 年份
|
|
|
* @param month 月份
|
|
|
* @return 该月最后一天的日期(1-31)
|
|
|
@@ -162,22 +622,23 @@ public class AppOperationReportServiceImpl implements IAppOperationReportService
|
|
|
|
|
|
/**
|
|
|
* 获取上个月的日期范围
|
|
|
+ *
|
|
|
* @param year 当前年份
|
|
|
* @param month 当前月份
|
|
|
- * @return 上个月的开始日期和结束日期数组
|
|
|
+ * @return 数组,[上月开始日期, 上月结束日期]
|
|
|
*/
|
|
|
private String[] getLastMonthDateRange(int year, int month) {
|
|
|
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
|
|
|
Calendar cal = Calendar.getInstance();
|
|
|
cal.set(Calendar.YEAR, year);
|
|
|
- cal.set(Calendar.MONTH, month - 2);
|
|
|
+ cal.set(Calendar.MONTH, month - 1);
|
|
|
cal.set(Calendar.DAY_OF_MONTH, 1);
|
|
|
+ cal.add(Calendar.DAY_OF_MONTH, -1);
|
|
|
+ String endDate = sdf.format(cal.getTime());
|
|
|
|
|
|
- SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
|
|
|
+ cal.set(Calendar.DAY_OF_MONTH, 1);
|
|
|
String startDate = sdf.format(cal.getTime());
|
|
|
|
|
|
- cal.set(Calendar.DAY_OF_MONTH, cal.getActualMaximum(Calendar.DAY_OF_MONTH));
|
|
|
- String endDate = sdf.format(cal.getTime());
|
|
|
-
|
|
|
return new String[]{startDate, endDate};
|
|
|
}
|
|
|
}
|