|
@@ -0,0 +1,343 @@
|
|
|
|
|
+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 org.springframework.stereotype.Service;
|
|
|
|
|
+import org.springframework.util.StringUtils;
|
|
|
|
|
+import shop.alien.entity.analytics.*;
|
|
|
|
|
+import shop.alien.entity.analytics.vo.dashboard.detail.*;
|
|
|
|
|
+import shop.alien.entity.store.StoreInfo;
|
|
|
|
|
+import shop.alien.mapper.AnalyticsDashboardDetailMapper;
|
|
|
|
|
+import shop.alien.mapper.StoreInfoMapper;
|
|
|
|
|
+import shop.alien.store.service.analytics.AnalyticsDashboardDetailService;
|
|
|
|
|
+import shop.alien.store.util.analytics.AnalyticsDateUtil;
|
|
|
|
|
+import shop.alien.store.util.analytics.AnalyticsPeriodContext;
|
|
|
|
|
+
|
|
|
|
|
+import java.math.BigDecimal;
|
|
|
|
|
+import java.math.RoundingMode;
|
|
|
|
|
+import java.util.Calendar;
|
|
|
|
|
+import java.util.Date;
|
|
|
|
|
+import java.util.List;
|
|
|
|
|
+import java.util.Map;
|
|
|
|
|
+
|
|
|
|
|
+@Service
|
|
|
|
|
+@RequiredArgsConstructor
|
|
|
|
|
+public class AnalyticsDashboardDetailServiceImpl implements AnalyticsDashboardDetailService {
|
|
|
|
|
+
|
|
|
|
|
+ private static final int DEFAULT_ONLINE_WITHIN_MIN = 5;
|
|
|
|
|
+
|
|
|
|
|
+ private final AnalyticsDashboardDetailMapper detailMapper;
|
|
|
|
|
+ private final StoreInfoMapper storeInfoMapper;
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public IPage<AnalyticsDauDetailVo> pageDau(String period, long page, long size) {
|
|
|
|
|
+ AnalyticsPeriodContext ctx = AnalyticsPeriodContext.resolve(period);
|
|
|
|
|
+ IPage<AnalyticsUserStat> raw;
|
|
|
|
|
+ if (isSingleDayPeriod(ctx)) {
|
|
|
|
|
+ raw = detailMapper.pageDauByStatDate(new Page<>(page, size), ctx.getStartDate());
|
|
|
|
|
+ } else {
|
|
|
|
|
+ raw = detailMapper.pageDauInDateRange(new Page<>(page, size), ctx.getStartDate(), ctx.getEndDate());
|
|
|
|
|
+ }
|
|
|
|
|
+ return raw.convert(this::toDauVo);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public IPage<AnalyticsNewUserDetailVo> pageNewUser(String period, long page, long size) {
|
|
|
|
|
+ AnalyticsPeriodContext ctx = AnalyticsPeriodContext.resolve(period);
|
|
|
|
|
+ IPage<AnalyticsUserStat> raw = detailMapper.pageNewUserInRange(
|
|
|
|
|
+ new Page<>(page, size), ctx.getStartTime(), ctx.getEndTimeExclusive());
|
|
|
|
|
+ return raw.convert(this::toNewUserVo);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public IPage<AnalyticsAiChatDetailVo> pageAiChat(String period, long page, long size) {
|
|
|
|
|
+ AnalyticsPeriodContext ctx = AnalyticsPeriodContext.resolve(period);
|
|
|
|
|
+ IPage<AnalyticsAiChatStat> raw = detailMapper.pageAiChatInRange(
|
|
|
|
|
+ new Page<>(page, size), ctx.getStartTime(), ctx.getEndTimeExclusive());
|
|
|
|
|
+ return raw.convert(this::toAiChatVo);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public IPage<AnalyticsContentPublishDetailVo> pageContentPublish(String period, long page, long size) {
|
|
|
|
|
+ AnalyticsPeriodContext ctx = AnalyticsPeriodContext.resolve(period);
|
|
|
|
|
+ IPage<Map<String, Object>> raw = detailMapper.pageContentPublishInRange(
|
|
|
|
|
+ new Page<>(page, size), ctx.getStartTime(), ctx.getEndTimeExclusive());
|
|
|
|
|
+ return raw.convert(this::toContentVo);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public IPage<AnalyticsMerchantUvDetailVo> pageMerchantUv(String period, long page, long size) {
|
|
|
|
|
+ AnalyticsPeriodContext ctx = AnalyticsPeriodContext.resolve(period);
|
|
|
|
|
+ IPage<Map<String, Object>> raw = detailMapper.pageMerchantUvAggregated(
|
|
|
|
|
+ new Page<>(page, size), ctx.getStartDate(), ctx.getEndDate());
|
|
|
|
|
+ return raw.convert(this::toMerchantVo);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public IPage<AnalyticsAiResponseDetailVo> pageAiResponse(String period, long page, long size) {
|
|
|
|
|
+ AnalyticsPeriodContext ctx = AnalyticsPeriodContext.resolve(period);
|
|
|
|
|
+ IPage<Map<String, Object>> raw = detailMapper.pageAiResponseHourly(
|
|
|
|
|
+ new Page<>(page, size), ctx.getStartTime(), ctx.getEndTimeExclusive());
|
|
|
|
|
+ IPage<AnalyticsAiResponseDetailVo> converted = raw.convert(this::toAiResponseVo);
|
|
|
|
|
+ for (AnalyticsAiResponseDetailVo row : converted.getRecords()) {
|
|
|
|
|
+ fillP95(row);
|
|
|
|
|
+ }
|
|
|
|
|
+ return converted;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public IPage<AnalyticsOnlineUserDetailVo> pageOnline(String period, long page, long size, int onlineWithinMin) {
|
|
|
|
|
+ if (!AnalyticsStatPeriod.TODAY.equals(AnalyticsPeriodContext.resolve(period).getPeriod())) {
|
|
|
|
|
+ return new Page<>(page, size, 0);
|
|
|
|
|
+ }
|
|
|
|
|
+ int withinMin = onlineWithinMin > 0 ? onlineWithinMin : DEFAULT_ONLINE_WITHIN_MIN;
|
|
|
|
|
+ Calendar threshold = Calendar.getInstance();
|
|
|
|
|
+ threshold.add(Calendar.MINUTE, -withinMin);
|
|
|
|
|
+ IPage<AnalyticsUserStat> raw = detailMapper.pageOnlineUsers(new Page<>(page, size), threshold.getTime());
|
|
|
|
|
+ return raw.convert(this::toOnlineVo);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public IPage<AnalyticsConversionDetailVo> pageConversion(String period, long page, long size) {
|
|
|
|
|
+ AnalyticsPeriodContext ctx = AnalyticsPeriodContext.resolve(period);
|
|
|
|
|
+ IPage<AnalyticsDailySummary> raw = detailMapper.pageConversionDaily(
|
|
|
|
|
+ new Page<>(page, size), ctx.getStartDate(), ctx.getEndDate());
|
|
|
|
|
+ return raw.convert(this::toConversionVo);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private boolean isSingleDayPeriod(AnalyticsPeriodContext ctx) {
|
|
|
|
|
+ return AnalyticsStatPeriod.TODAY.equals(ctx.getPeriod())
|
|
|
|
|
+ || AnalyticsStatPeriod.YESTERDAY.equals(ctx.getPeriod());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private AnalyticsDauDetailVo toDauVo(AnalyticsUserStat stat) {
|
|
|
|
|
+ AnalyticsDauDetailVo vo = new AnalyticsDauDetailVo();
|
|
|
|
|
+ vo.setUserId(stat.getUserId());
|
|
|
|
|
+ vo.setDisplayUserId(formatUserId(stat.getUserId()));
|
|
|
|
|
+ vo.setFirstLaunchTime(stat.getFirstLaunchTime());
|
|
|
|
|
+ vo.setLastActiveTime(stat.getLastActiveTime());
|
|
|
|
|
+ vo.setCity(stat.getCity());
|
|
|
|
|
+ vo.setDevice(stat.getDeviceType());
|
|
|
|
|
+ return vo;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private AnalyticsNewUserDetailVo toNewUserVo(AnalyticsUserStat stat) {
|
|
|
|
|
+ AnalyticsNewUserDetailVo vo = new AnalyticsNewUserDetailVo();
|
|
|
|
|
+ vo.setUserId(stat.getUserId());
|
|
|
|
|
+ vo.setDisplayUserId(formatUserId(stat.getUserId()));
|
|
|
|
|
+ vo.setMaskedPhone(maskPhone(stat.getUserPhone()));
|
|
|
|
|
+ vo.setRegisterTime(stat.getRegisterTime());
|
|
|
|
|
+ vo.setChannel(stat.getChannel());
|
|
|
|
|
+ vo.setCity(stat.getCity());
|
|
|
|
|
+ return vo;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private AnalyticsAiChatDetailVo toAiChatVo(AnalyticsAiChatStat stat) {
|
|
|
|
|
+ AnalyticsAiChatDetailVo vo = new AnalyticsAiChatDetailVo();
|
|
|
|
|
+ vo.setChatId(stat.getChatId());
|
|
|
|
|
+ vo.setDisplayChatId(formatChatId(stat.getChatId()));
|
|
|
|
|
+ vo.setUserId(stat.getUserId());
|
|
|
|
|
+ vo.setDisplayUserId(formatUserId(stat.getUserId()));
|
|
|
|
|
+ vo.setStartTime(stat.getStartTime());
|
|
|
|
|
+ vo.setMessageCount(stat.getMessageCount());
|
|
|
|
|
+ vo.setAiResponseDurationMs(stat.getAiResponseDurationMs());
|
|
|
|
|
+ return vo;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private AnalyticsContentPublishDetailVo toContentVo(Map<String, Object> row) {
|
|
|
|
|
+ AnalyticsContentPublishDetailVo vo = new AnalyticsContentPublishDetailVo();
|
|
|
|
|
+ Long contentId = longValue(row.get("contentId"));
|
|
|
|
|
+ vo.setContentId(contentId);
|
|
|
|
|
+ vo.setDisplayContentId(formatContentId(contentId));
|
|
|
|
|
+ vo.setTitle(stringValue(row.get("contentTitle")));
|
|
|
|
|
+ vo.setCategory(resolveBusinessCategoryName(intValue(row.get("businessCategory"))));
|
|
|
|
|
+ Long authorId = longValue(row.get("authorId"));
|
|
|
|
|
+ vo.setAuthorId(authorId);
|
|
|
|
|
+ vo.setDisplayAuthorId(formatUserId(authorId));
|
|
|
|
|
+ Object publishTime = row.get("publishTime");
|
|
|
|
|
+ if (publishTime instanceof Date) {
|
|
|
|
|
+ vo.setPublishTime((Date) publishTime);
|
|
|
|
|
+ }
|
|
|
|
|
+ vo.setInteractionCount(intValue(row.get("interactionCount")));
|
|
|
|
|
+ return vo;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private AnalyticsMerchantUvDetailVo toMerchantVo(Map<String, Object> row) {
|
|
|
|
|
+ AnalyticsMerchantUvDetailVo vo = new AnalyticsMerchantUvDetailVo();
|
|
|
|
|
+ Long merchantId = longValue(row.get("merchantId"));
|
|
|
|
|
+ vo.setMerchantId(merchantId);
|
|
|
|
|
+ vo.setDisplayMerchantId(formatMerchantId(merchantId));
|
|
|
|
|
+ vo.setVisitUv(intValue(row.get("visitUv")));
|
|
|
|
|
+ vo.setVisitPv(intValue(row.get("visitPv")));
|
|
|
|
|
+ int verifyCount = intValue(row.get("verifyCount"));
|
|
|
|
|
+ if (vo.getVisitUv() != null && vo.getVisitUv() > 0 && verifyCount >= 0) {
|
|
|
|
|
+ vo.setVerifyConversionRate(BigDecimal.valueOf(verifyCount * 100.0 / vo.getVisitUv())
|
|
|
|
|
+ .setScale(1, RoundingMode.HALF_UP));
|
|
|
|
|
+ }
|
|
|
|
|
+ if (merchantId != null) {
|
|
|
|
|
+ StoreInfo store = storeInfoMapper.selectById(merchantId);
|
|
|
|
|
+ if (store != null) {
|
|
|
|
|
+ vo.setMerchantName(store.getStoreName());
|
|
|
|
|
+ vo.setCategory(firstNonBlank(store.getBusinessCategoryName(), store.getBusinessTypeName(),
|
|
|
|
|
+ resolveShopTypeName(intValue(row.get("shopType")))));
|
|
|
|
|
+ } else {
|
|
|
|
|
+ vo.setCategory(resolveShopTypeName(intValue(row.get("shopType"))));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return vo;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private AnalyticsAiResponseDetailVo toAiResponseVo(Map<String, Object> row) {
|
|
|
|
|
+ AnalyticsAiResponseDetailVo vo = new AnalyticsAiResponseDetailVo();
|
|
|
|
|
+ Object statTime = row.get("statTime");
|
|
|
|
|
+ if (statTime instanceof Date) {
|
|
|
|
|
+ vo.setStatTime((Date) statTime);
|
|
|
|
|
+ } else if (statTime != null) {
|
|
|
|
|
+ vo.setStatTime(AnalyticsDateUtil.parseDateTime(String.valueOf(statTime)));
|
|
|
|
|
+ }
|
|
|
|
|
+ vo.setRequestCount(intValue(row.get("requestCount")));
|
|
|
|
|
+ vo.setAvgResponseMs(longValue(row.get("avgResponseMs")));
|
|
|
|
|
+ vo.setTimeoutCount(intValue(row.get("timeoutCount")));
|
|
|
|
|
+ return vo;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void fillP95(AnalyticsAiResponseDetailVo row) {
|
|
|
|
|
+ if (row.getStatTime() == null) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ Date hourStart = row.getStatTime();
|
|
|
|
|
+ Date hourEnd = AnalyticsDateUtil.addHours(hourStart, 1);
|
|
|
|
|
+ List<Long> durations = detailMapper.listAiResponseDurationsForHour(hourStart, hourEnd);
|
|
|
|
|
+ if (durations == null || durations.isEmpty()) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ int idx = (int) Math.ceil(durations.size() * 0.95) - 1;
|
|
|
|
|
+ if (idx < 0) {
|
|
|
|
|
+ idx = 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (idx >= durations.size()) {
|
|
|
|
|
+ idx = durations.size() - 1;
|
|
|
|
|
+ }
|
|
|
|
|
+ row.setP95ResponseMs(durations.get(idx));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private AnalyticsOnlineUserDetailVo toOnlineVo(AnalyticsUserStat stat) {
|
|
|
|
|
+ AnalyticsOnlineUserDetailVo vo = new AnalyticsOnlineUserDetailVo();
|
|
|
|
|
+ vo.setUserId(stat.getUserId());
|
|
|
|
|
+ vo.setDisplayUserId(formatUserId(stat.getUserId()));
|
|
|
|
|
+ vo.setCity(stat.getCity());
|
|
|
|
|
+ vo.setDevice(stat.getDeviceType());
|
|
|
|
|
+ vo.setLastHeartbeatTime(stat.getLastActiveTime());
|
|
|
|
|
+ vo.setOnlineDurationMin(stat.getOnlineDurationMin());
|
|
|
|
|
+ return vo;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private AnalyticsConversionDetailVo toConversionVo(AnalyticsDailySummary summary) {
|
|
|
|
|
+ AnalyticsConversionDetailVo vo = new AnalyticsConversionDetailVo();
|
|
|
|
|
+ vo.setStatDate(summary.getStatDate());
|
|
|
|
|
+ vo.setDau(summary.getDau());
|
|
|
|
|
+ vo.setPayUserCount(summary.getPayUserCount());
|
|
|
|
|
+ vo.setConversionRate(summary.getConversionRate());
|
|
|
|
|
+ vo.setAvgOrderAmount(summary.getAvgOrderAmount());
|
|
|
|
|
+ return vo;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String formatUserId(Long userId) {
|
|
|
|
|
+ return userId != null ? "U" + userId : null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String formatMerchantId(Long merchantId) {
|
|
|
|
|
+ return merchantId != null ? "M" + merchantId : null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String formatContentId(Long contentId) {
|
|
|
|
|
+ return contentId != null ? "C" + contentId : null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String formatChatId(String chatId) {
|
|
|
|
|
+ if (!StringUtils.hasText(chatId)) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ return chatId.startsWith("C") ? chatId : chatId;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String maskPhone(String phone) {
|
|
|
|
|
+ if (!StringUtils.hasText(phone) || phone.length() < 7) {
|
|
|
|
|
+ return phone;
|
|
|
|
|
+ }
|
|
|
|
|
+ return phone.substring(0, 3) + "****" + phone.substring(phone.length() - 4);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String resolveBusinessCategoryName(Integer businessCategory) {
|
|
|
|
|
+ if (businessCategory == null) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ switch (businessCategory) {
|
|
|
|
|
+ case AnalyticsBusinessCategory.FOOD: return "美食";
|
|
|
|
|
+ case AnalyticsBusinessCategory.LEISURE: return "休闲娱乐";
|
|
|
|
|
+ case AnalyticsBusinessCategory.LIFE_SERVICE: return "生活服务";
|
|
|
|
|
+ case AnalyticsBusinessCategory.TRAVEL: return "旅游";
|
|
|
|
|
+ case AnalyticsBusinessCategory.HOTEL: return "酒店";
|
|
|
|
|
+ case AnalyticsBusinessCategory.SHOPPING: return "购物";
|
|
|
|
|
+ case AnalyticsBusinessCategory.OTHER: return "其他";
|
|
|
|
|
+ default: return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String resolveShopTypeName(Integer shopType) {
|
|
|
|
|
+ if (shopType == null) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ switch (shopType) {
|
|
|
|
|
+ case 1: return "美食";
|
|
|
|
|
+ case 2: return "休闲娱乐";
|
|
|
|
|
+ case 3: return "生活服务";
|
|
|
|
|
+ default: return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String firstNonBlank(String... values) {
|
|
|
|
|
+ if (values == null) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ for (String v : values) {
|
|
|
|
|
+ if (StringUtils.hasText(v)) {
|
|
|
|
|
+ return v;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String stringValue(Object o) {
|
|
|
|
|
+ return o != null ? String.valueOf(o) : null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Integer intValue(Object o) {
|
|
|
|
|
+ if (o == null) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (o instanceof Number) {
|
|
|
|
|
+ return ((Number) o).intValue();
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ return Integer.parseInt(String.valueOf(o));
|
|
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Long longValue(Object o) {
|
|
|
|
|
+ if (o == null) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (o instanceof Number) {
|
|
|
|
|
+ return ((Number) o).longValue();
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ return Long.parseLong(String.valueOf(o));
|
|
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|