You've already forked FrameTour-BE
refactor(statistics): 优化应用统计漏斗查询逻辑
- 实现跨日期范围查询时分离历史数据和实时数据的处理策略 - 添加包含今天日期的跨范围查询特殊处理逻辑 - 将实时数据查询提取为独立的 queryRealtimeData 方法 - 优化数据累加逻辑,支持历史数据和今日数据合并计算 - 修复 BigDecimal 安全相加方法中的空值处理问题 - 统一数值字段的安全累加操作,防止空指针异常 - 调整 Redis 缓存策略,仅对当天数据启用短期缓存 - 改进查询条件判断逻辑,提高多日查询性能表现
This commit is contained in:
@@ -161,7 +161,73 @@ public class AppStatisticsServiceImpl implements AppStatisticsService {
|
||||
query.getStartTime() != null ? DateUtil.formatDate(query.getStartTime()) : "null");
|
||||
if (!query.isRealtime()) {
|
||||
if (!(DateUtil.isIn(query.getStartTime(), DateUtil.yesterday(), DateUtil.tomorrow()) && DateUtil.isIn(query.getEndTime(), DateUtil.yesterday(), DateUtil.tomorrow()))) {
|
||||
// 查询缓存数据
|
||||
// 判断是否为跨范围查询且包含今天
|
||||
Date today = DateUtil.beginOfDay(new Date());
|
||||
boolean containsToday = query.getEndTime() != null && !query.getEndTime().before(today);
|
||||
boolean isMultiDayQuery = query.getStartTime() != null && query.getStartTime().before(today);
|
||||
|
||||
if (containsToday && isMultiDayQuery) {
|
||||
// 跨范围查询且包含今天:需要分别查询历史数据和今天数据
|
||||
AppStatisticsFunnelVO result = new AppStatisticsFunnelVO();
|
||||
|
||||
// 1. 查询历史数据(从 startDate 到昨天结束)
|
||||
Date yesterday = DateUtil.endOfDay(DateUtil.yesterday());
|
||||
List<AppStatisticsFunnelVO> historyList = statisticsMapper.listStatByScenic(
|
||||
query.getScenicId(),
|
||||
query.getStartTime(),
|
||||
yesterday
|
||||
);
|
||||
|
||||
// 累加历史数据
|
||||
if (historyList != null && !historyList.isEmpty()) {
|
||||
for (AppStatisticsFunnelVO item : historyList) {
|
||||
result.setCameraShotOfMemberNum(addIntSafely(result.getCameraShotOfMemberNum(), item.getCameraShotOfMemberNum()));
|
||||
result.setScanCodeVisitorOfMemberNum(addIntSafely(result.getScanCodeVisitorOfMemberNum(), item.getScanCodeVisitorOfMemberNum()));
|
||||
result.setUploadFaceOfMemberNum(addIntSafely(result.getUploadFaceOfMemberNum(), item.getUploadFaceOfMemberNum()));
|
||||
result.setPushOfMemberNum(addIntSafely(result.getPushOfMemberNum(), item.getPushOfMemberNum()));
|
||||
result.setCompleteVideoOfMemberNum(addIntSafely(result.getCompleteVideoOfMemberNum(), item.getCompleteVideoOfMemberNum()));
|
||||
result.setPreviewVideoOfMemberNum(addIntSafely(result.getPreviewVideoOfMemberNum(), item.getPreviewVideoOfMemberNum()));
|
||||
result.setClickOnPayOfMemberNum(addIntSafely(result.getClickOnPayOfMemberNum(), item.getClickOnPayOfMemberNum()));
|
||||
result.setPayOfMemberNum(addIntSafely(result.getPayOfMemberNum(), item.getPayOfMemberNum()));
|
||||
result.setTotalVisitorOfMemberNum(addIntSafely(result.getTotalVisitorOfMemberNum(), item.getTotalVisitorOfMemberNum()));
|
||||
result.setCompleteOfVideoNum(addIntSafely(result.getCompleteOfVideoNum(), item.getCompleteOfVideoNum()));
|
||||
result.setPreviewOfVideoNum(addIntSafely(result.getPreviewOfVideoNum(), item.getPreviewOfVideoNum()));
|
||||
result.setPayOfOrderNum(addIntSafely(result.getPayOfOrderNum(), item.getPayOfOrderNum()));
|
||||
result.setRefundOfOrderNum(addIntSafely(result.getRefundOfOrderNum(), item.getRefundOfOrderNum()));
|
||||
result.setPayOfOrderAmount(addBigDecimalSafely(result.payOfOrderAmount(), item.payOfOrderAmount()));
|
||||
result.setRefundOfOrderAmount(addBigDecimalSafely(result.refundOfOrderAmount(), item.refundOfOrderAmount()));
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 查询今天的实时数据
|
||||
CommonQueryReq todayQuery = new CommonQueryReq();
|
||||
todayQuery.setScenicId(query.getScenicId());
|
||||
todayQuery.setStartTime(today);
|
||||
todayQuery.setEndTime(query.getEndTime());
|
||||
|
||||
// 执行今天的实时查询
|
||||
AppStatisticsFunnelVO todayData = queryRealtimeData(todayQuery);
|
||||
|
||||
// 3. 合并今天的数据到结果中
|
||||
result.setCameraShotOfMemberNum(addIntSafely(result.getCameraShotOfMemberNum(), todayData.getCameraShotOfMemberNum()));
|
||||
result.setScanCodeVisitorOfMemberNum(addIntSafely(result.getScanCodeVisitorOfMemberNum(), todayData.getScanCodeVisitorOfMemberNum()));
|
||||
result.setUploadFaceOfMemberNum(addIntSafely(result.getUploadFaceOfMemberNum(), todayData.getUploadFaceOfMemberNum()));
|
||||
result.setPushOfMemberNum(addIntSafely(result.getPushOfMemberNum(), todayData.getPushOfMemberNum()));
|
||||
result.setCompleteVideoOfMemberNum(addIntSafely(result.getCompleteVideoOfMemberNum(), todayData.getCompleteVideoOfMemberNum()));
|
||||
result.setPreviewVideoOfMemberNum(addIntSafely(result.getPreviewVideoOfMemberNum(), todayData.getPreviewVideoOfMemberNum()));
|
||||
result.setClickOnPayOfMemberNum(addIntSafely(result.getClickOnPayOfMemberNum(), todayData.getClickOnPayOfMemberNum()));
|
||||
result.setPayOfMemberNum(addIntSafely(result.getPayOfMemberNum(), todayData.getPayOfMemberNum()));
|
||||
result.setTotalVisitorOfMemberNum(addIntSafely(result.getTotalVisitorOfMemberNum(), todayData.getTotalVisitorOfMemberNum()));
|
||||
result.setCompleteOfVideoNum(addIntSafely(result.getCompleteOfVideoNum(), todayData.getCompleteOfVideoNum()));
|
||||
result.setPreviewOfVideoNum(addIntSafely(result.getPreviewOfVideoNum(), todayData.getPreviewOfVideoNum()));
|
||||
result.setPayOfOrderNum(addIntSafely(result.getPayOfOrderNum(), todayData.getPayOfOrderNum()));
|
||||
result.setRefundOfOrderNum(addIntSafely(result.getRefundOfOrderNum(), todayData.getRefundOfOrderNum()));
|
||||
result.setPayOfOrderAmount(addBigDecimalSafely(result.payOfOrderAmount(), todayData.payOfOrderAmount()));
|
||||
result.setRefundOfOrderAmount(addBigDecimalSafely(result.refundOfOrderAmount(), todayData.refundOfOrderAmount()));
|
||||
|
||||
return ApiResponse.success(result);
|
||||
} else {
|
||||
// 纯历史查询(不包含今天)
|
||||
List<AppStatisticsFunnelVO> list = statisticsMapper.listStatByScenic(query.getScenicId(), query.getStartTime(), query.getEndTime());
|
||||
AppStatisticsFunnelVO resp = new AppStatisticsFunnelVO();
|
||||
if (list != null && !list.isEmpty()) {
|
||||
@@ -191,6 +257,7 @@ public class AppStatisticsServiceImpl implements AppStatisticsService {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!query.isRealtime()) {
|
||||
if (DateUtil.isIn(query.getStartTime(), DateUtil.yesterday(), DateUtil.tomorrow()) && DateUtil.isIn(query.getEndTime(), DateUtil.yesterday(), DateUtil.tomorrow())) {
|
||||
// 缓存
|
||||
@@ -215,64 +282,9 @@ public class AppStatisticsServiceImpl implements AppStatisticsService {
|
||||
}
|
||||
}
|
||||
}
|
||||
//镜头检测游客数
|
||||
// Integer cameraShotOfMemberNum=statisticsMapper.countCameraShotOfMember(query);
|
||||
//扫码访问人数
|
||||
// 扫小程序码或景区码进入访问的用户数,包括授权用户(使用OpenID进行精准统计)和未授权用户(使用 UUID统计访问)。但当用户授权时,获取OpenID并与UUID关联,删除本地UUID,避免重复记录。
|
||||
Integer scanCodeVisitorOfMemberNum=statsQueryService.countScanCodeOfMember(query);
|
||||
//上传头像(人脸)人数
|
||||
// 上传了人脸的用户数(包括本地临时ID和获取到OpenID的,同一设备微信获取到OpenID要覆盖掉之前生成的临时ID),上传多张人脸都只算一个人。
|
||||
Integer uploadFaceOfMemberNum=statsQueryService.countUploadFaceOfMember(query);
|
||||
//推送订阅人数
|
||||
// 只要点了允许通知,哪怕只勾选1条订阅都算
|
||||
Integer pushOfMemberNum =statsQueryService.countPushOfMember(query);
|
||||
//生成视频人数
|
||||
// 生成过Vlog视频的用户ID数,要注意屏蔽掉以前没有片段也能生成的情况
|
||||
Integer completeVideoOfMemberNum =statsQueryService.countCompleteVideoOfMember(query);
|
||||
//预览视频人数
|
||||
// 购买前播放了5秒的视频条数。
|
||||
Integer previewVideoOfMemberNum =statsQueryService.countPreviewVideoOfMember(query);
|
||||
if (previewVideoOfMemberNum==null){
|
||||
previewVideoOfMemberNum=0;
|
||||
}
|
||||
//点击购买人数
|
||||
// 点了立即购买按钮的用户ID就算,包括支付的和未支付的都算,只要点击了。
|
||||
Integer clickOnPayOfMemberNum =statisticsMapper.countClickPayOfMember(query);
|
||||
//支付订单人数
|
||||
Integer payOfMemberNum =statisticsMapper.countPayOfMember(query);
|
||||
//总访问人数
|
||||
// 通过任何途径访问到小程序的总人数,包括授权用户和未授权用户。
|
||||
Integer totalVisitorOfMemberNum =statsQueryService.countTotalVisitorOfMember(query);
|
||||
// Integer totalVisitorOfMemberNum =scanCodeVisitorOfMemberNum;
|
||||
//生成视频条数
|
||||
// 仅指代生成的Vlog条数,不包含录像原片。
|
||||
Integer completeOfVideoNum =statsQueryService.countCompleteOfVideo(query);
|
||||
//预览视频条数
|
||||
Integer previewOfVideoNum =statsQueryService.countPreviewOfVideo(query);
|
||||
//支付订单数
|
||||
Integer payOfOrderNum =statisticsMapper.countPayOfOrder(query);
|
||||
//支付订单金额
|
||||
BigDecimal payOfOrderAmount =statisticsMapper.countOrderAmount(query);
|
||||
//退款订单数
|
||||
Integer refundOfOrderNum =statisticsMapper.countRefundOfOrder(query);
|
||||
//退款订单金额
|
||||
BigDecimal refundOfOrderAmount =statisticsMapper.countRefundAmount(query);
|
||||
|
||||
vo.setScanCodeVisitorOfMemberNum(scanCodeVisitorOfMemberNum);
|
||||
vo.setUploadFaceOfMemberNum(uploadFaceOfMemberNum);
|
||||
vo.setPushOfMemberNum(pushOfMemberNum);
|
||||
vo.setCompleteVideoOfMemberNum(completeVideoOfMemberNum);
|
||||
vo.setPreviewVideoOfMemberNum(previewVideoOfMemberNum);
|
||||
vo.setClickOnPayOfMemberNum(clickOnPayOfMemberNum);
|
||||
vo.setPayOfMemberNum(payOfMemberNum);
|
||||
|
||||
vo.setTotalVisitorOfMemberNum(totalVisitorOfMemberNum);
|
||||
vo.setCompleteOfVideoNum(completeOfVideoNum);
|
||||
vo.setPreviewOfVideoNum(previewOfVideoNum);
|
||||
vo.setPayOfOrderNum(payOfOrderNum);
|
||||
vo.setPayOfOrderAmount(payOfOrderAmount.setScale(2, RoundingMode.HALF_UP));
|
||||
vo.setRefundOfOrderNum(refundOfOrderNum);
|
||||
vo.setRefundOfOrderAmount(refundOfOrderAmount.setScale(2, RoundingMode.HALF_UP));
|
||||
// 执行实时查询
|
||||
vo = queryRealtimeData(query);
|
||||
|
||||
// 仅对当天数据启用 Redis 缓存(短期缓存,减少实时查询压力)
|
||||
// 历史数据已在 scenic_stats 表中持久化,不需要 Redis 缓存
|
||||
@@ -302,6 +314,64 @@ public class AppStatisticsServiceImpl implements AppStatisticsService {
|
||||
return int1 == null ? 0 : int1 + (int2 == null ? 0 : int2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行实时数据查询(从 ClickHouse 和 MySQL 查询最新数据)
|
||||
* @param query 查询条件
|
||||
* @return 实时统计数据
|
||||
*/
|
||||
private AppStatisticsFunnelVO queryRealtimeData(CommonQueryReq query) {
|
||||
AppStatisticsFunnelVO vo = new AppStatisticsFunnelVO();
|
||||
|
||||
//扫码访问人数
|
||||
Integer scanCodeVisitorOfMemberNum = statsQueryService.countScanCodeOfMember(query);
|
||||
//上传头像(人脸)人数
|
||||
Integer uploadFaceOfMemberNum = statsQueryService.countUploadFaceOfMember(query);
|
||||
//推送订阅人数
|
||||
Integer pushOfMemberNum = statsQueryService.countPushOfMember(query);
|
||||
//生成视频人数
|
||||
Integer completeVideoOfMemberNum = statsQueryService.countCompleteVideoOfMember(query);
|
||||
//预览视频人数
|
||||
Integer previewVideoOfMemberNum = statsQueryService.countPreviewVideoOfMember(query);
|
||||
if (previewVideoOfMemberNum == null) {
|
||||
previewVideoOfMemberNum = 0;
|
||||
}
|
||||
//点击购买人数
|
||||
Integer clickOnPayOfMemberNum = statisticsMapper.countClickPayOfMember(query);
|
||||
//支付订单人数
|
||||
Integer payOfMemberNum = statisticsMapper.countPayOfMember(query);
|
||||
//总访问人数
|
||||
Integer totalVisitorOfMemberNum = statsQueryService.countTotalVisitorOfMember(query);
|
||||
//生成视频条数
|
||||
Integer completeOfVideoNum = statsQueryService.countCompleteOfVideo(query);
|
||||
//预览视频条数
|
||||
Integer previewOfVideoNum = statsQueryService.countPreviewOfVideo(query);
|
||||
//支付订单数
|
||||
Integer payOfOrderNum = statisticsMapper.countPayOfOrder(query);
|
||||
//支付订单金额
|
||||
BigDecimal payOfOrderAmount = statisticsMapper.countOrderAmount(query);
|
||||
//退款订单数
|
||||
Integer refundOfOrderNum = statisticsMapper.countRefundOfOrder(query);
|
||||
//退款订单金额
|
||||
BigDecimal refundOfOrderAmount = statisticsMapper.countRefundAmount(query);
|
||||
|
||||
vo.setScanCodeVisitorOfMemberNum(scanCodeVisitorOfMemberNum);
|
||||
vo.setUploadFaceOfMemberNum(uploadFaceOfMemberNum);
|
||||
vo.setPushOfMemberNum(pushOfMemberNum);
|
||||
vo.setCompleteVideoOfMemberNum(completeVideoOfMemberNum);
|
||||
vo.setPreviewVideoOfMemberNum(previewVideoOfMemberNum);
|
||||
vo.setClickOnPayOfMemberNum(clickOnPayOfMemberNum);
|
||||
vo.setPayOfMemberNum(payOfMemberNum);
|
||||
vo.setTotalVisitorOfMemberNum(totalVisitorOfMemberNum);
|
||||
vo.setCompleteOfVideoNum(completeOfVideoNum);
|
||||
vo.setPreviewOfVideoNum(previewOfVideoNum);
|
||||
vo.setPayOfOrderNum(payOfOrderNum);
|
||||
vo.setPayOfOrderAmount(payOfOrderAmount != null ? payOfOrderAmount.setScale(2, RoundingMode.HALF_UP) : BigDecimal.ZERO);
|
||||
vo.setRefundOfOrderNum(refundOfOrderNum);
|
||||
vo.setRefundOfOrderAmount(refundOfOrderAmount != null ? refundOfOrderAmount.setScale(2, RoundingMode.HALF_UP) : BigDecimal.ZERO);
|
||||
|
||||
return vo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiResponse orderChart(CommonQueryReq query) {
|
||||
if(query.getEndTime()==null && query.getStartTime()==null){
|
||||
|
||||
Reference in New Issue
Block a user