refactor(statistics): 优化应用统计漏斗查询逻辑

- 实现跨日期范围查询时分离历史数据和实时数据的处理策略
- 添加包含今天日期的跨范围查询特殊处理逻辑
- 将实时数据查询提取为独立的 queryRealtimeData 方法
- 优化数据累加逻辑,支持历史数据和今日数据合并计算
- 修复 BigDecimal 安全相加方法中的空值处理问题
- 统一数值字段的安全累加操作,防止空指针异常
- 调整 Redis 缓存策略,仅对当天数据启用短期缓存
- 改进查询条件判断逻辑,提高多日查询性能表现
This commit is contained in:
2026-02-06 13:38:16 +08:00
parent 1e71add551
commit 34839276cf

View File

@@ -161,33 +161,100 @@ 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()))) {
// 查询缓存数据 // 判断是否为跨范围查询且包含今天
List<AppStatisticsFunnelVO> list = statisticsMapper.listStatByScenic(query.getScenicId(), query.getStartTime(), query.getEndTime()); Date today = DateUtil.beginOfDay(new Date());
AppStatisticsFunnelVO resp = new AppStatisticsFunnelVO(); boolean containsToday = query.getEndTime() != null && !query.getEndTime().before(today);
if (list != null && !list.isEmpty()) { boolean isMultiDayQuery = query.getStartTime() != null && query.getStartTime().before(today);
for (AppStatisticsFunnelVO item : list) {
// Integer fields
resp.setCameraShotOfMemberNum(addIntSafely(resp.getCameraShotOfMemberNum(), item.getCameraShotOfMemberNum()));
resp.setScanCodeVisitorOfMemberNum(addIntSafely(resp.getScanCodeVisitorOfMemberNum(), item.getScanCodeVisitorOfMemberNum()));
resp.setUploadFaceOfMemberNum(addIntSafely(resp.getUploadFaceOfMemberNum(), item.getUploadFaceOfMemberNum()));
resp.setPushOfMemberNum(addIntSafely(resp.getPushOfMemberNum(), item.getPushOfMemberNum()));
resp.setCompleteVideoOfMemberNum(addIntSafely(resp.getCompleteVideoOfMemberNum(), item.getCompleteVideoOfMemberNum()));
resp.setPreviewVideoOfMemberNum(addIntSafely(resp.getPreviewVideoOfMemberNum(), item.getPreviewVideoOfMemberNum()));
resp.setClickOnPayOfMemberNum(addIntSafely(resp.getClickOnPayOfMemberNum(), item.getClickOnPayOfMemberNum()));
resp.setPayOfMemberNum(addIntSafely(resp.getPayOfMemberNum(), item.getPayOfMemberNum()));
resp.setTotalVisitorOfMemberNum(addIntSafely(resp.getTotalVisitorOfMemberNum(), item.getTotalVisitorOfMemberNum()));
resp.setCompleteOfVideoNum(addIntSafely(resp.getCompleteOfVideoNum(), item.getCompleteOfVideoNum()));
resp.setPreviewOfVideoNum(addIntSafely(resp.getPreviewOfVideoNum(), item.getPreviewOfVideoNum()));
resp.setPayOfOrderNum(addIntSafely(resp.getPayOfOrderNum(), item.getPayOfOrderNum()));
resp.setRefundOfOrderNum(addIntSafely(resp.getRefundOfOrderNum(), item.getRefundOfOrderNum()));
// BigDecimal fields if (containsToday && isMultiDayQuery) {
resp.setPayOfOrderAmount(addBigDecimalSafely(resp.payOfOrderAmount(), item.payOfOrderAmount())); // 跨范围查询且包含今天:需要分别查询历史数据和今天数据
resp.setRefundOfOrderAmount(addBigDecimalSafely(resp.refundOfOrderAmount(), item.refundOfOrderAmount())); 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()));
}
} }
return ApiResponse.success(resp);
// 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 { } else {
query.setRealtime(true); // 纯历史查询(不包含今天)
List<AppStatisticsFunnelVO> list = statisticsMapper.listStatByScenic(query.getScenicId(), query.getStartTime(), query.getEndTime());
AppStatisticsFunnelVO resp = new AppStatisticsFunnelVO();
if (list != null && !list.isEmpty()) {
for (AppStatisticsFunnelVO item : list) {
// Integer fields
resp.setCameraShotOfMemberNum(addIntSafely(resp.getCameraShotOfMemberNum(), item.getCameraShotOfMemberNum()));
resp.setScanCodeVisitorOfMemberNum(addIntSafely(resp.getScanCodeVisitorOfMemberNum(), item.getScanCodeVisitorOfMemberNum()));
resp.setUploadFaceOfMemberNum(addIntSafely(resp.getUploadFaceOfMemberNum(), item.getUploadFaceOfMemberNum()));
resp.setPushOfMemberNum(addIntSafely(resp.getPushOfMemberNum(), item.getPushOfMemberNum()));
resp.setCompleteVideoOfMemberNum(addIntSafely(resp.getCompleteVideoOfMemberNum(), item.getCompleteVideoOfMemberNum()));
resp.setPreviewVideoOfMemberNum(addIntSafely(resp.getPreviewVideoOfMemberNum(), item.getPreviewVideoOfMemberNum()));
resp.setClickOnPayOfMemberNum(addIntSafely(resp.getClickOnPayOfMemberNum(), item.getClickOnPayOfMemberNum()));
resp.setPayOfMemberNum(addIntSafely(resp.getPayOfMemberNum(), item.getPayOfMemberNum()));
resp.setTotalVisitorOfMemberNum(addIntSafely(resp.getTotalVisitorOfMemberNum(), item.getTotalVisitorOfMemberNum()));
resp.setCompleteOfVideoNum(addIntSafely(resp.getCompleteOfVideoNum(), item.getCompleteOfVideoNum()));
resp.setPreviewOfVideoNum(addIntSafely(resp.getPreviewOfVideoNum(), item.getPreviewOfVideoNum()));
resp.setPayOfOrderNum(addIntSafely(resp.getPayOfOrderNum(), item.getPayOfOrderNum()));
resp.setRefundOfOrderNum(addIntSafely(resp.getRefundOfOrderNum(), item.getRefundOfOrderNum()));
// BigDecimal fields
resp.setPayOfOrderAmount(addBigDecimalSafely(resp.payOfOrderAmount(), item.payOfOrderAmount()));
resp.setRefundOfOrderAmount(addBigDecimalSafely(resp.refundOfOrderAmount(), item.refundOfOrderAmount()));
}
return ApiResponse.success(resp);
} else {
query.setRealtime(true);
}
} }
} }
} }
@@ -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){