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");
|
query.getStartTime() != null ? DateUtil.formatDate(query.getStartTime()) : "null");
|
||||||
if (!query.isRealtime()) {
|
if (!query.isRealtime()) {
|
||||||
if (!(DateUtil.isIn(query.getStartTime(), DateUtil.yesterday(), DateUtil.tomorrow()) && DateUtil.isIn(query.getEndTime(), DateUtil.yesterday(), DateUtil.tomorrow()))) {
|
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());
|
List<AppStatisticsFunnelVO> list = statisticsMapper.listStatByScenic(query.getScenicId(), query.getStartTime(), query.getEndTime());
|
||||||
AppStatisticsFunnelVO resp = new AppStatisticsFunnelVO();
|
AppStatisticsFunnelVO resp = new AppStatisticsFunnelVO();
|
||||||
if (list != null && !list.isEmpty()) {
|
if (list != null && !list.isEmpty()) {
|
||||||
@@ -191,6 +257,7 @@ public class AppStatisticsServiceImpl implements AppStatisticsService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (!query.isRealtime()) {
|
if (!query.isRealtime()) {
|
||||||
if (DateUtil.isIn(query.getStartTime(), DateUtil.yesterday(), DateUtil.tomorrow()) && DateUtil.isIn(query.getEndTime(), DateUtil.yesterday(), DateUtil.tomorrow())) {
|
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 = queryRealtimeData(query);
|
||||||
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));
|
|
||||||
|
|
||||||
// 仅对当天数据启用 Redis 缓存(短期缓存,减少实时查询压力)
|
// 仅对当天数据启用 Redis 缓存(短期缓存,减少实时查询压力)
|
||||||
// 历史数据已在 scenic_stats 表中持久化,不需要 Redis 缓存
|
// 历史数据已在 scenic_stats 表中持久化,不需要 Redis 缓存
|
||||||
@@ -302,6 +314,64 @@ public class AppStatisticsServiceImpl implements AppStatisticsService {
|
|||||||
return int1 == null ? 0 : int1 + (int2 == null ? 0 : int2);
|
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
|
@Override
|
||||||
public ApiResponse orderChart(CommonQueryReq query) {
|
public ApiResponse orderChart(CommonQueryReq query) {
|
||||||
if(query.getEndTime()==null && query.getStartTime()==null){
|
if(query.getEndTime()==null && query.getStartTime()==null){
|
||||||
|
|||||||
Reference in New Issue
Block a user