|
@@ -1,14 +1,21 @@
|
|
|
package shop.alien.store.service.analytics.impl;
|
|
package shop.alien.store.service.analytics.impl;
|
|
|
|
|
|
|
|
|
|
+import com.baomidou.mybatisplus.core.metadata.IPage;
|
|
|
|
|
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
|
import lombok.RequiredArgsConstructor;
|
|
import lombok.RequiredArgsConstructor;
|
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.stereotype.Service;
|
|
|
import shop.alien.entity.analytics.AnalyticsDailySummary;
|
|
import shop.alien.entity.analytics.AnalyticsDailySummary;
|
|
|
import shop.alien.entity.analytics.AnalyticsEventCode;
|
|
import shop.alien.entity.analytics.AnalyticsEventCode;
|
|
|
|
|
+import shop.alien.entity.analytics.AnalyticsStatPeriod;
|
|
|
import shop.alien.entity.analytics.vo.dashboard.*;
|
|
import shop.alien.entity.analytics.vo.dashboard.*;
|
|
|
|
|
+import shop.alien.mapper.AnalyticsAiRequestMapper;
|
|
|
import shop.alien.mapper.AnalyticsDashboardMapper;
|
|
import shop.alien.mapper.AnalyticsDashboardMapper;
|
|
|
import shop.alien.mapper.AnalyticsEventMapper;
|
|
import shop.alien.mapper.AnalyticsEventMapper;
|
|
|
|
|
+import shop.alien.mapper.AnalyticsFunnelMapper;
|
|
|
|
|
+import shop.alien.mapper.AnalyticsUserStatTodayMapper;
|
|
|
import shop.alien.store.service.analytics.AnalyticsDashboardService;
|
|
import shop.alien.store.service.analytics.AnalyticsDashboardService;
|
|
|
import shop.alien.store.util.analytics.AnalyticsDateUtil;
|
|
import shop.alien.store.util.analytics.AnalyticsDateUtil;
|
|
|
|
|
+import shop.alien.store.util.analytics.AnalyticsFunnelStageCodes;
|
|
|
import shop.alien.store.util.analytics.AnalyticsPeriodContext;
|
|
import shop.alien.store.util.analytics.AnalyticsPeriodContext;
|
|
|
import shop.alien.store.util.analytics.AnalyticsTrendFillUtil;
|
|
import shop.alien.store.util.analytics.AnalyticsTrendFillUtil;
|
|
|
|
|
|
|
@@ -26,6 +33,11 @@ public class AnalyticsDashboardServiceImpl implements AnalyticsDashboardService
|
|
|
|
|
|
|
|
private final AnalyticsDashboardMapper dashboardMapper;
|
|
private final AnalyticsDashboardMapper dashboardMapper;
|
|
|
private final AnalyticsEventMapper eventMapper;
|
|
private final AnalyticsEventMapper eventMapper;
|
|
|
|
|
+ private final AnalyticsFunnelMapper funnelMapper;
|
|
|
|
|
+ private final AnalyticsAiRequestMapper aiRequestMapper;
|
|
|
|
|
+ private final AnalyticsUserStatTodayMapper userStatTodayMapper;
|
|
|
|
|
+
|
|
|
|
|
+ private static final int ONLINE_WINDOW_MIN = 5;
|
|
|
|
|
|
|
|
@Override
|
|
@Override
|
|
|
public AnalyticsDashboardChartsVo loadAllCharts(String period) {
|
|
public AnalyticsDashboardChartsVo loadAllCharts(String period) {
|
|
@@ -44,45 +56,256 @@ public class AnalyticsDashboardServiceImpl implements AnalyticsDashboardService
|
|
|
@Override
|
|
@Override
|
|
|
public AnalyticsDashboardSummaryVo summary(String period) {
|
|
public AnalyticsDashboardSummaryVo summary(String period) {
|
|
|
AnalyticsPeriodContext ctx = AnalyticsPeriodContext.resolve(period);
|
|
AnalyticsPeriodContext ctx = AnalyticsPeriodContext.resolve(period);
|
|
|
- List<AnalyticsDailySummary> rows = dashboardMapper.listDailySummaryBetween(ctx.getStartDate(), ctx.getEndDate());
|
|
|
|
|
|
|
+ AnalyticsPeriodContext prevCtx = AnalyticsPeriodContext.buildPreviousPeriod(ctx);
|
|
|
|
|
+ String compareLabel = resolveCompareLabel(ctx.getPeriod());
|
|
|
|
|
+
|
|
|
|
|
+ PeriodMetrics now = loadPeriodMetrics(ctx);
|
|
|
|
|
+ PeriodMetrics prev = loadPeriodMetrics(prevCtx);
|
|
|
|
|
|
|
|
AnalyticsDashboardSummaryVo vo = new AnalyticsDashboardSummaryVo();
|
|
AnalyticsDashboardSummaryVo vo = new AnalyticsDashboardSummaryVo();
|
|
|
- int dayCount = dayCount(ctx.getStartDate(), ctx.getEndDate());
|
|
|
|
|
|
|
+ vo.setPeriod(ctx.getPeriod());
|
|
|
|
|
+
|
|
|
|
|
+ vo.setActiveUser(buildCountCard("DAU", resolveActiveUserLabel(ctx.getPeriod()),
|
|
|
|
|
+ now.activeUsers, prev.activeUsers, compareLabel));
|
|
|
|
|
+ vo.setNewUser(buildCountCard("NEW_USER", resolvePeriodPrefix(ctx.getPeriod()) + "新增用户",
|
|
|
|
|
+ now.newUsers, prev.newUsers, compareLabel));
|
|
|
|
|
+ vo.setAiChat(buildCountCard("AI_CHAT", resolvePeriodPrefix(ctx.getPeriod()) + "对话次数",
|
|
|
|
|
+ now.aiChatCount, prev.aiChatCount, compareLabel));
|
|
|
|
|
+ vo.setContentPublish(buildCountCard("CONTENT_PUBLISH", resolvePeriodPrefix(ctx.getPeriod()) + "内容发布",
|
|
|
|
|
+ now.contentPublishCount, prev.contentPublishCount, compareLabel));
|
|
|
|
|
+ vo.setMerchantVisitUvCard(buildCountCard("MERCHANT_UV", resolvePeriodPrefix(ctx.getPeriod()) + "商家访问UV",
|
|
|
|
|
+ now.merchantVisitUv, prev.merchantVisitUv, compareLabel));
|
|
|
|
|
+ vo.setAiResponse(buildDurationCard(resolvePeriodPrefix(ctx.getPeriod()) + "AI响应时间",
|
|
|
|
|
+ now.aiAvgResponseMs, prev.aiAvgResponseMs, compareLabel));
|
|
|
|
|
+ vo.setOnlineUser(buildOnlineCard(ctx, now.onlineUsers, prev.onlineUsers, compareLabel));
|
|
|
|
|
+ vo.setConversionRateCard(buildRateCard(resolvePeriodPrefix(ctx.getPeriod()) + "转化率",
|
|
|
|
|
+ now.conversionRate, prev.conversionRate, compareLabel));
|
|
|
|
|
+
|
|
|
|
|
+ syncLegacyFields(vo, now);
|
|
|
|
|
+ return vo;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- if (rows.isEmpty()) {
|
|
|
|
|
- fillSummaryFromEvents(vo, ctx);
|
|
|
|
|
- return vo;
|
|
|
|
|
|
|
+ private PeriodMetrics loadPeriodMetrics(AnalyticsPeriodContext ctx) {
|
|
|
|
|
+ PeriodMetrics m = new PeriodMetrics();
|
|
|
|
|
+ List<AnalyticsDailySummary> rows = dashboardMapper.listDailySummaryBetween(
|
|
|
|
|
+ ctx.getStartDate(), ctx.getEndDate());
|
|
|
|
|
+
|
|
|
|
|
+ m.activeUsers = intValue(eventMapper.countActiveUsersInRange(
|
|
|
|
|
+ ctx.getStartTime(), ctx.getEndTimeExclusive()));
|
|
|
|
|
+ m.newUsers = sumInt(rows, AnalyticsDailySummary::getNewUserCount);
|
|
|
|
|
+ if (m.newUsers <= 0) {
|
|
|
|
|
+ m.newUsers = intValue(eventMapper.countRegisterUsers(ctx.getStartTime(), ctx.getEndTimeExclusive()));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- if (dayCount == 1) {
|
|
|
|
|
- AnalyticsDailySummary s = rows.get(0);
|
|
|
|
|
- vo.setDau(resolveDau(s, ctx));
|
|
|
|
|
- vo.setOnlineUserCount(defaultInt(s.getOnlineUserCount()));
|
|
|
|
|
- } else {
|
|
|
|
|
- vo.setDau((int) Math.round(rows.stream().mapToInt(s -> resolveDau(s, ctx)).average().orElse(0)));
|
|
|
|
|
- vo.setOnlineUserCount(null);
|
|
|
|
|
|
|
+ m.aiChatCount = sumInt(rows, AnalyticsDailySummary::getAiChatCount);
|
|
|
|
|
+ if (m.aiChatCount <= 0) {
|
|
|
|
|
+ m.aiChatCount = intValue(eventMapper.countByEventCode(
|
|
|
|
|
+ AnalyticsEventCode.AI_CHAT_END, ctx.getStartTime(), ctx.getEndTimeExclusive()));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- vo.setNewUserCount(rows.stream().mapToInt(s -> defaultInt(s.getNewUserCount())).sum());
|
|
|
|
|
- vo.setAiChatCount(rows.stream().mapToInt(s -> defaultInt(s.getAiChatCount())).sum());
|
|
|
|
|
- vo.setContentPublishCount(rows.stream().mapToInt(s -> defaultInt(s.getContentPublishCount())).sum());
|
|
|
|
|
- if (dayCount == 1) {
|
|
|
|
|
- vo.setMerchantVisitUv(defaultInt(rows.isEmpty() ? 0 : rows.get(0).getMerchantVisitUv()));
|
|
|
|
|
- } else {
|
|
|
|
|
- vo.setMerchantVisitUv(intValue(eventMapper.countMerchantVisitUv(ctx.getStartTime(), ctx.getEndTimeExclusive())));
|
|
|
|
|
|
|
+ m.contentPublishCount = sumInt(rows, AnalyticsDailySummary::getContentPublishCount);
|
|
|
|
|
+ if (m.contentPublishCount <= 0) {
|
|
|
|
|
+ m.contentPublishCount = intValue(eventMapper.countByEventCode(
|
|
|
|
|
+ AnalyticsEventCode.CONTENT_PUBLISH, ctx.getStartTime(), ctx.getEndTimeExclusive()));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ m.merchantVisitUv = intValue(eventMapper.countMerchantVisitUv(
|
|
|
|
|
+ ctx.getStartTime(), ctx.getEndTimeExclusive()));
|
|
|
|
|
+
|
|
|
long totalAiMs = rows.stream()
|
|
long totalAiMs = rows.stream()
|
|
|
.mapToLong(s -> s.getAiResponseDurationTotalMs() != null ? s.getAiResponseDurationTotalMs() : 0L)
|
|
.mapToLong(s -> s.getAiResponseDurationTotalMs() != null ? s.getAiResponseDurationTotalMs() : 0L)
|
|
|
.sum();
|
|
.sum();
|
|
|
int totalAiReq = rows.stream().mapToInt(s -> defaultInt(s.getAiRequestCount())).sum();
|
|
int totalAiReq = rows.stream().mapToInt(s -> defaultInt(s.getAiRequestCount())).sum();
|
|
|
- vo.setAiAvgResponseMs(totalAiReq > 0 ? totalAiMs / totalAiReq : 0L);
|
|
|
|
|
- vo.setConversionRate(calcWeightedConversionRate(rows));
|
|
|
|
|
|
|
+ if (totalAiReq <= 0) {
|
|
|
|
|
+ totalAiMs = defaultLong(aiRequestMapper.sumResponseDuration(ctx.getStartTime(), ctx.getEndTimeExclusive()));
|
|
|
|
|
+ totalAiReq = intValue(aiRequestMapper.countRequests(ctx.getStartTime(), ctx.getEndTimeExclusive()));
|
|
|
|
|
+ }
|
|
|
|
|
+ m.aiAvgResponseMs = totalAiReq > 0 ? totalAiMs / totalAiReq : 0L;
|
|
|
|
|
+
|
|
|
|
|
+ m.conversionRate = calcPeriodConversionRate(ctx, rows, m.activeUsers);
|
|
|
|
|
|
|
|
- if (vo.getDau() == null || vo.getDau() == 0) {
|
|
|
|
|
- fillSummaryFromEvents(vo, ctx);
|
|
|
|
|
|
|
+ if (isCurrentDayPeriod(ctx)) {
|
|
|
|
|
+ m.onlineUsers = countRealtimeOnline();
|
|
|
|
|
+ } else if (!rows.isEmpty()) {
|
|
|
|
|
+ m.onlineUsers = defaultInt(rows.get(rows.size() - 1).getOnlineUserCount());
|
|
|
}
|
|
}
|
|
|
- return vo;
|
|
|
|
|
|
|
+
|
|
|
|
|
+ return m;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private AnalyticsKpiCardVo buildCountCard(String code, String label, int value, int previous, String compareLabel) {
|
|
|
|
|
+ AnalyticsKpiCardVo card = new AnalyticsKpiCardVo();
|
|
|
|
|
+ card.setMetricCode(code);
|
|
|
|
|
+ card.setLabel(label);
|
|
|
|
|
+ card.setValue(BigDecimal.valueOf(value));
|
|
|
|
|
+ card.setDelta(BigDecimal.valueOf(value - previous));
|
|
|
|
|
+ card.setChangeRate(calcChangeRate(value, previous));
|
|
|
|
|
+ card.setCompareLabel(compareLabel);
|
|
|
|
|
+ card.setValueType("COUNT");
|
|
|
|
|
+ card.setRealTime(false);
|
|
|
|
|
+ card.setLowerIsBetter(false);
|
|
|
|
|
+ return card;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private AnalyticsKpiCardVo buildDurationCard(String label, long value, long previous, String compareLabel) {
|
|
|
|
|
+ AnalyticsKpiCardVo card = new AnalyticsKpiCardVo();
|
|
|
|
|
+ card.setMetricCode("AI_RESPONSE");
|
|
|
|
|
+ card.setLabel(label);
|
|
|
|
|
+ card.setValue(BigDecimal.valueOf(value));
|
|
|
|
|
+ card.setDelta(BigDecimal.valueOf(value - previous));
|
|
|
|
|
+ card.setChangeRate(calcChangeRate(value, previous));
|
|
|
|
|
+ card.setCompareLabel(compareLabel);
|
|
|
|
|
+ card.setValueType("DURATION");
|
|
|
|
|
+ card.setUnit("ms");
|
|
|
|
|
+ card.setRealTime(false);
|
|
|
|
|
+ card.setLowerIsBetter(true);
|
|
|
|
|
+ return card;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private AnalyticsKpiCardVo buildRateCard(String label, BigDecimal value, BigDecimal previous, String compareLabel) {
|
|
|
|
|
+ BigDecimal current = value != null ? value : BigDecimal.ZERO;
|
|
|
|
|
+ BigDecimal prev = previous != null ? previous : BigDecimal.ZERO;
|
|
|
|
|
+ AnalyticsKpiCardVo card = new AnalyticsKpiCardVo();
|
|
|
|
|
+ card.setMetricCode("CONVERSION");
|
|
|
|
|
+ card.setLabel(label);
|
|
|
|
|
+ card.setValue(current);
|
|
|
|
|
+ card.setDelta(current.subtract(prev).setScale(2, RoundingMode.HALF_UP));
|
|
|
|
|
+ card.setChangeRate(calcChangeRate(current.doubleValue(), prev.doubleValue()));
|
|
|
|
|
+ card.setCompareLabel(compareLabel);
|
|
|
|
|
+ card.setValueType("RATE");
|
|
|
|
|
+ card.setUnit("%");
|
|
|
|
|
+ card.setRealTime(false);
|
|
|
|
|
+ card.setLowerIsBetter(false);
|
|
|
|
|
+ return card;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private AnalyticsKpiCardVo buildOnlineCard(AnalyticsPeriodContext ctx, int value, int previous, String compareLabel) {
|
|
|
|
|
+ AnalyticsKpiCardVo card = new AnalyticsKpiCardVo();
|
|
|
|
|
+ card.setMetricCode("ONLINE");
|
|
|
|
|
+ card.setLabel("当前在线");
|
|
|
|
|
+ card.setValue(BigDecimal.valueOf(value));
|
|
|
|
|
+ card.setValueType("COUNT");
|
|
|
|
|
+ card.setUnit(null);
|
|
|
|
|
+ if (isCurrentDayPeriod(ctx)) {
|
|
|
|
|
+ card.setRealTime(true);
|
|
|
|
|
+ card.setDelta(BigDecimal.valueOf(value - previous));
|
|
|
|
|
+ card.setChangeRate(calcChangeRate(value, previous));
|
|
|
|
|
+ card.setCompareLabel(compareLabel);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ card.setRealTime(false);
|
|
|
|
|
+ card.setDelta(null);
|
|
|
|
|
+ card.setChangeRate(null);
|
|
|
|
|
+ card.setCompareLabel(null);
|
|
|
|
|
+ }
|
|
|
|
|
+ card.setLowerIsBetter(false);
|
|
|
|
|
+ return card;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void syncLegacyFields(AnalyticsDashboardSummaryVo vo, PeriodMetrics metrics) {
|
|
|
|
|
+ vo.setDau(metrics.activeUsers);
|
|
|
|
|
+ vo.setNewUserCount(metrics.newUsers);
|
|
|
|
|
+ vo.setAiChatCount(metrics.aiChatCount);
|
|
|
|
|
+ vo.setContentPublishCount(metrics.contentPublishCount);
|
|
|
|
|
+ vo.setMerchantVisitUv(metrics.merchantVisitUv);
|
|
|
|
|
+ vo.setAiAvgResponseMs(metrics.aiAvgResponseMs);
|
|
|
|
|
+ vo.setOnlineUserCount(metrics.onlineUsers);
|
|
|
|
|
+ vo.setConversionRate(metrics.conversionRate);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private BigDecimal calcPeriodConversionRate(AnalyticsPeriodContext ctx,
|
|
|
|
|
+ List<AnalyticsDailySummary> rows,
|
|
|
|
|
+ int activeUsers) {
|
|
|
|
|
+ if (!rows.isEmpty()) {
|
|
|
|
|
+ BigDecimal weighted = calcWeightedConversionRate(rows);
|
|
|
|
|
+ if (weighted != null && weighted.compareTo(BigDecimal.ZERO) > 0) {
|
|
|
|
|
+ return weighted;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ Long payCount = eventMapper.countByEventCode(
|
|
|
|
|
+ AnalyticsEventCode.PAY_SUCCESS, ctx.getStartTime(), ctx.getEndTimeExclusive());
|
|
|
|
|
+ return AnalyticsDateUtil.calcRate(payCount, activeUsers);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private int countRealtimeOnline() {
|
|
|
|
|
+ Calendar threshold = Calendar.getInstance();
|
|
|
|
|
+ threshold.add(Calendar.MINUTE, -ONLINE_WINDOW_MIN);
|
|
|
|
|
+ Long count = userStatTodayMapper.countOnlineSince(threshold.getTime());
|
|
|
|
|
+ return count != null ? count.intValue() : 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private boolean isCurrentDayPeriod(AnalyticsPeriodContext ctx) {
|
|
|
|
|
+ Date today = AnalyticsDateUtil.truncateToDate(new Date());
|
|
|
|
|
+ return AnalyticsStatPeriod.TODAY.equals(ctx.getPeriod())
|
|
|
|
|
+ || (ctx.getEndDate() != null && AnalyticsDateUtil.truncateToDate(ctx.getEndDate()).equals(today)
|
|
|
|
|
+ && ctx.getStartDate() != null && AnalyticsDateUtil.truncateToDate(ctx.getStartDate()).equals(today));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String resolvePeriodPrefix(String period) {
|
|
|
|
|
+ switch (period) {
|
|
|
|
|
+ case AnalyticsStatPeriod.TODAY:
|
|
|
|
|
+ return "今日";
|
|
|
|
|
+ case AnalyticsStatPeriod.YESTERDAY:
|
|
|
|
|
+ return "昨日";
|
|
|
|
|
+ case AnalyticsStatPeriod.LAST_7D:
|
|
|
|
|
+ return "近7日";
|
|
|
|
|
+ case AnalyticsStatPeriod.LAST_30D:
|
|
|
|
|
+ return "近30日";
|
|
|
|
|
+ default:
|
|
|
|
|
+ return "";
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String resolveActiveUserLabel(String period) {
|
|
|
|
|
+ switch (period) {
|
|
|
|
|
+ case AnalyticsStatPeriod.TODAY:
|
|
|
|
|
+ return "今日 DAU";
|
|
|
|
|
+ case AnalyticsStatPeriod.YESTERDAY:
|
|
|
|
|
+ return "昨日 DAU";
|
|
|
|
|
+ case AnalyticsStatPeriod.LAST_7D:
|
|
|
|
|
+ return "近7日活跃";
|
|
|
|
|
+ case AnalyticsStatPeriod.LAST_30D:
|
|
|
|
|
+ return "近30日活跃";
|
|
|
|
|
+ default:
|
|
|
|
|
+ return "活跃用户";
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String resolveCompareLabel(String period) {
|
|
|
|
|
+ switch (period) {
|
|
|
|
|
+ case AnalyticsStatPeriod.TODAY:
|
|
|
|
|
+ return "较昨日";
|
|
|
|
|
+ case AnalyticsStatPeriod.YESTERDAY:
|
|
|
|
|
+ return "较前日";
|
|
|
|
|
+ default:
|
|
|
|
|
+ return "较上期";
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private BigDecimal calcChangeRate(double current, double previous) {
|
|
|
|
|
+ if (previous <= 0) {
|
|
|
|
|
+ return current > 0 ? BigDecimal.valueOf(100) : BigDecimal.ZERO;
|
|
|
|
|
+ }
|
|
|
|
|
+ return BigDecimal.valueOf((current - previous) * 100.0 / previous)
|
|
|
|
|
+ .setScale(1, RoundingMode.HALF_UP);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private int sumInt(List<AnalyticsDailySummary> rows, java.util.function.Function<AnalyticsDailySummary, Integer> getter) {
|
|
|
|
|
+ return rows.stream().mapToInt(s -> defaultInt(getter.apply(s))).sum();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private long defaultLong(Long value) {
|
|
|
|
|
+ return value != null ? value : 0L;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static final class PeriodMetrics {
|
|
|
|
|
+ private int activeUsers;
|
|
|
|
|
+ private int newUsers;
|
|
|
|
|
+ private int aiChatCount;
|
|
|
|
|
+ private int contentPublishCount;
|
|
|
|
|
+ private int merchantVisitUv;
|
|
|
|
|
+ private long aiAvgResponseMs;
|
|
|
|
|
+ private int onlineUsers;
|
|
|
|
|
+ private BigDecimal conversionRate = BigDecimal.ZERO;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
@Override
|
|
@@ -176,6 +399,38 @@ public class AnalyticsDashboardServiceImpl implements AnalyticsDashboardService
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
@Override
|
|
|
|
|
+ public IPage<AnalyticsFunnelDetailVo> pageConversionFunnelDetail(String period, String stageCode, long page, long size) {
|
|
|
|
|
+ List<String> eventCodes = AnalyticsFunnelStageCodes.merchantStageEvents(stageCode);
|
|
|
|
|
+ if (eventCodes.isEmpty()) {
|
|
|
|
|
+ throw new IllegalArgumentException("未知漏斗阶段: " + stageCode);
|
|
|
|
|
+ }
|
|
|
|
|
+ AnalyticsPeriodContext ctx = AnalyticsPeriodContext.resolve(period);
|
|
|
|
|
+ IPage<Map<String, Object>> raw = funnelMapper.pageFunnelUsers(
|
|
|
|
|
+ new Page<>(page, size), ctx.getStartTime(), ctx.getEndTimeExclusive(), eventCodes);
|
|
|
|
|
+ return raw.convert(this::toFunnelDetailVo);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private AnalyticsFunnelDetailVo toFunnelDetailVo(Map<String, Object> row) {
|
|
|
|
|
+ AnalyticsFunnelDetailVo vo = new AnalyticsFunnelDetailVo();
|
|
|
|
|
+ Long userId = longValue(row.get("userId"));
|
|
|
|
|
+ vo.setUserId(userId);
|
|
|
|
|
+ vo.setDisplayUserId(userId != null ? "U" + userId : null);
|
|
|
|
|
+ Long merchantId = longValue(row.get("merchantId"));
|
|
|
|
|
+ if (merchantId != null && merchantId > 0) {
|
|
|
|
|
+ vo.setMerchantId(merchantId);
|
|
|
|
|
+ vo.setDisplayMerchantId("M" + merchantId);
|
|
|
|
|
+ }
|
|
|
|
|
+ vo.setCity(stringValue(row.get("city")));
|
|
|
|
|
+ vo.setDeviceType(stringValue(row.get("deviceType")));
|
|
|
|
|
+ vo.setChannel(stringValue(row.get("channel")));
|
|
|
|
|
+ Object eventTime = row.get("eventTime");
|
|
|
|
|
+ if (eventTime instanceof Date) {
|
|
|
|
|
+ vo.setEventTime((Date) eventTime);
|
|
|
|
|
+ }
|
|
|
|
|
+ return vo;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
public List<AnalyticsRetentionPointVo> userRetention(String period) {
|
|
public List<AnalyticsRetentionPointVo> userRetention(String period) {
|
|
|
AnalyticsPeriodContext ctx = AnalyticsPeriodContext.resolve(period);
|
|
AnalyticsPeriodContext ctx = AnalyticsPeriodContext.resolve(period);
|
|
|
Date cohortStart = AnalyticsDateUtil.dayStart(ctx.getStartDate());
|
|
Date cohortStart = AnalyticsDateUtil.dayStart(ctx.getStartDate());
|
|
@@ -342,30 +597,6 @@ public class AnalyticsDashboardServiceImpl implements AnalyticsDashboardService
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private void fillSummaryFromEvents(AnalyticsDashboardSummaryVo vo, AnalyticsPeriodContext ctx) {
|
|
|
|
|
- vo.setDau(intValue(eventMapper.countDau(ctx.getStartTime(), ctx.getEndTimeExclusive())));
|
|
|
|
|
- vo.setNewUserCount(intValue(eventMapper.countRegisterUsers(ctx.getStartTime(), ctx.getEndTimeExclusive())));
|
|
|
|
|
- vo.setAiChatCount(intValue(eventMapper.countByEventCode(
|
|
|
|
|
- AnalyticsEventCode.AI_CHAT_END, ctx.getStartTime(), ctx.getEndTimeExclusive())));
|
|
|
|
|
- vo.setContentPublishCount(intValue(eventMapper.countByEventCode(
|
|
|
|
|
- AnalyticsEventCode.CONTENT_PUBLISH, ctx.getStartTime(), ctx.getEndTimeExclusive())));
|
|
|
|
|
- vo.setMerchantVisitUv(intValue(eventMapper.countMerchantVisitUv(ctx.getStartTime(), ctx.getEndTimeExclusive())));
|
|
|
|
|
- vo.setAiAvgResponseMs(0L);
|
|
|
|
|
- vo.setConversionRate(BigDecimal.ZERO);
|
|
|
|
|
- if (ctx.isHourly()) {
|
|
|
|
|
- vo.setOnlineUserCount(vo.getDau());
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private int resolveDau(AnalyticsDailySummary summary, AnalyticsPeriodContext ctx) {
|
|
|
|
|
- if (summary.getDau() != null && summary.getDau() > 0) {
|
|
|
|
|
- return summary.getDau();
|
|
|
|
|
- }
|
|
|
|
|
- Date day = AnalyticsDateUtil.truncateToDate(summary.getStatDate());
|
|
|
|
|
- return intValue(eventMapper.countDau(
|
|
|
|
|
- AnalyticsDateUtil.dayStart(day), AnalyticsDateUtil.dayEndExclusive(day)));
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
private BigDecimal calcWeightedConversionRate(List<AnalyticsDailySummary> rows) {
|
|
private BigDecimal calcWeightedConversionRate(List<AnalyticsDailySummary> rows) {
|
|
|
BigDecimal weighted = BigDecimal.ZERO;
|
|
BigDecimal weighted = BigDecimal.ZERO;
|
|
|
int weight = 0;
|
|
int weight = 0;
|
|
@@ -381,10 +612,6 @@ public class AnalyticsDashboardServiceImpl implements AnalyticsDashboardService
|
|
|
return weighted.divide(BigDecimal.valueOf(weight), 2, RoundingMode.HALF_UP);
|
|
return weighted.divide(BigDecimal.valueOf(weight), 2, RoundingMode.HALF_UP);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private int dayCount(Date start, Date end) {
|
|
|
|
|
- return (int) ((AnalyticsDateUtil.dayStart(end).getTime() - AnalyticsDateUtil.dayStart(start).getTime()) / 86400000L) + 1;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
private AnalyticsFunnelStageVo buildFunnelStage(String code, String name, long count, Long previous) {
|
|
private AnalyticsFunnelStageVo buildFunnelStage(String code, String name, long count, Long previous) {
|
|
|
AnalyticsFunnelStageVo vo = new AnalyticsFunnelStageVo();
|
|
AnalyticsFunnelStageVo vo = new AnalyticsFunnelStageVo();
|
|
|
vo.setStageCode(code);
|
|
vo.setStageCode(code);
|