|
|
@@ -1,17 +1,22 @@
|
|
|
package shop.alien.store.service.impl;
|
|
|
|
|
|
-import com.alibaba.fastjson.JSON;
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
|
|
+import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
import lombok.RequiredArgsConstructor;
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
import org.springframework.stereotype.Service;
|
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
|
import shop.alien.entity.store.StoreTrackEvent;
|
|
|
+import shop.alien.entity.store.StoreTrackStatistics;
|
|
|
import shop.alien.mapper.StoreTrackEventMapper;
|
|
|
+import shop.alien.mapper.StoreTrackStatisticsMapper;
|
|
|
import shop.alien.store.config.BaseRedisService;
|
|
|
import shop.alien.store.service.TrackEventService;
|
|
|
|
|
|
+import java.util.Date;
|
|
|
import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
|
|
|
/**
|
|
|
* 埋点事件服务实现类
|
|
|
@@ -26,17 +31,16 @@ import java.util.List;
|
|
|
public class TrackEventServiceImpl extends ServiceImpl<StoreTrackEventMapper, StoreTrackEvent> implements TrackEventService {
|
|
|
|
|
|
private final BaseRedisService baseRedisService;
|
|
|
+ private final StoreTrackStatisticsMapper trackStatisticsMapper;
|
|
|
+ private final ObjectMapper objectMapper;
|
|
|
|
|
|
private static final String REDIS_QUEUE_KEY = "track:event:queue";
|
|
|
|
|
|
@Override
|
|
|
public void saveTrackEvent(StoreTrackEvent trackEvent) {
|
|
|
- // 异步写入Redis List
|
|
|
try {
|
|
|
- String eventJson = JSON.toJSONString(trackEvent);
|
|
|
+ String eventJson = objectMapper.writeValueAsString(trackEvent);
|
|
|
baseRedisService.setListRight(REDIS_QUEUE_KEY, eventJson);
|
|
|
- log.debug("埋点事件已写入Redis List: eventType={}, storeId={}",
|
|
|
- trackEvent.getEventType(), trackEvent.getStoreId());
|
|
|
} catch (Exception e) {
|
|
|
log.error("写入Redis List失败", e);
|
|
|
throw new RuntimeException("写入Redis List失败", e);
|
|
|
@@ -50,7 +54,6 @@ public class TrackEventServiceImpl extends ServiceImpl<StoreTrackEventMapper, St
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
- // 批量保存到数据库
|
|
|
boolean result = this.saveBatch(trackEvents, 100);
|
|
|
log.info("批量保存埋点事件: 总数={}, 成功={}", trackEvents.size(), result);
|
|
|
} catch (Exception e) {
|
|
|
@@ -58,4 +61,331 @@ public class TrackEventServiceImpl extends ServiceImpl<StoreTrackEventMapper, St
|
|
|
throw new RuntimeException("批量保存埋点事件失败", e);
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void calculateAndSaveStatistics(Integer storeId, Date statDate, String statType) {
|
|
|
+ log.info("开始计算统计数据: storeId={}, statDate={}, statType={}", storeId, statDate, statType);
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 计算统计日期范围
|
|
|
+ Date startDate = statDate;
|
|
|
+ Date endDate = statDate;
|
|
|
+
|
|
|
+ // 根据统计类型确定日期范围
|
|
|
+ if ("WEEKLY".equals(statType)) {
|
|
|
+ // 周统计:从周一(statDate)到周日
|
|
|
+ java.util.Calendar cal = java.util.Calendar.getInstance();
|
|
|
+ cal.setTime(statDate);
|
|
|
+ cal.set(java.util.Calendar.DAY_OF_WEEK, java.util.Calendar.MONDAY);
|
|
|
+ startDate = cal.getTime();
|
|
|
+ cal.add(java.util.Calendar.DAY_OF_WEEK, 6);
|
|
|
+ endDate = cal.getTime();
|
|
|
+ } else if ("MONTHLY".equals(statType)) {
|
|
|
+ // 月统计:从月初到月末
|
|
|
+ java.util.Calendar cal = java.util.Calendar.getInstance();
|
|
|
+ cal.setTime(statDate);
|
|
|
+ cal.set(java.util.Calendar.DAY_OF_MONTH, 1);
|
|
|
+ startDate = cal.getTime();
|
|
|
+ cal.add(java.util.Calendar.MONTH, 1);
|
|
|
+ cal.add(java.util.Calendar.DAY_OF_MONTH, -1);
|
|
|
+ endDate = cal.getTime();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 查询或创建统计记录
|
|
|
+ LambdaQueryWrapper<StoreTrackStatistics> queryWrapper = new LambdaQueryWrapper<>();
|
|
|
+ queryWrapper.eq(StoreTrackStatistics::getStoreId, storeId)
|
|
|
+ .eq(StoreTrackStatistics::getStatDate, statDate)
|
|
|
+ .eq(StoreTrackStatistics::getStatType, statType);
|
|
|
+
|
|
|
+ StoreTrackStatistics statistics = trackStatisticsMapper.selectOne(queryWrapper);
|
|
|
+ if (statistics == null) {
|
|
|
+ statistics = new StoreTrackStatistics();
|
|
|
+ statistics.setStoreId(storeId);
|
|
|
+ statistics.setStatDate(statDate);
|
|
|
+ statistics.setStatType(statType);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 从store_track_event表统计基础数据
|
|
|
+ LambdaQueryWrapper<StoreTrackEvent> eventWrapper = new LambdaQueryWrapper<>();
|
|
|
+ eventWrapper.eq(StoreTrackEvent::getStoreId, storeId)
|
|
|
+ .ge(StoreTrackEvent::getEventTime, startDate)
|
|
|
+ .le(StoreTrackEvent::getEventTime, endDate)
|
|
|
+ .eq(StoreTrackEvent::getDeleteFlag, 0);
|
|
|
+
|
|
|
+ List<StoreTrackEvent> events = this.list(eventWrapper);
|
|
|
+
|
|
|
+ // 按事件分类统计(符合文档格式)
|
|
|
+ Map<String, Object> trafficData = calculateTrafficData(events, storeId, startDate);
|
|
|
+ Map<String, Object> interactionData = calculateInteractionData(events, storeId);
|
|
|
+ Map<String, Object> couponData = calculateCouponData(events, storeId);
|
|
|
+ Map<String, Object> voucherData = calculateVoucherData(events, storeId);
|
|
|
+ Map<String, Object> serviceData = calculateServiceData(events, storeId);
|
|
|
+ List<Map<String, Object>> priceData = calculatePriceRankingData(events);
|
|
|
+
|
|
|
+ // 设置统计数据(JSON格式)
|
|
|
+ statistics.setTrafficData(objectMapper.writeValueAsString(trafficData));
|
|
|
+ statistics.setInteractionData(objectMapper.writeValueAsString(interactionData));
|
|
|
+ statistics.setCouponData(objectMapper.writeValueAsString(couponData));
|
|
|
+ statistics.setVoucherData(objectMapper.writeValueAsString(voucherData));
|
|
|
+ statistics.setServiceData(objectMapper.writeValueAsString(serviceData));
|
|
|
+ statistics.setPriceRankingData(objectMapper.writeValueAsString(priceData));
|
|
|
+
|
|
|
+ // 保存统计记录
|
|
|
+ if (statistics.getId() == null) {
|
|
|
+ trackStatisticsMapper.insert(statistics);
|
|
|
+ log.info("统计数据保存成功: storeId={}, statDate={}, statType={}", storeId, statDate, statType);
|
|
|
+ } else {
|
|
|
+ trackStatisticsMapper.updateById(statistics);
|
|
|
+ log.info("统计数据更新成功: storeId={}, statDate={}, statType={}", storeId, statDate, statType);
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("计算统计数据失败: storeId={}, statDate={}, statType={}", storeId, statDate, statType, e);
|
|
|
+ throw new RuntimeException("计算统计数据失败", e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算流量数据(符合文档格式)
|
|
|
+ */
|
|
|
+ private Map<String, Object> calculateTrafficData(List<StoreTrackEvent> events, Integer storeId, Date startDate) {
|
|
|
+ Map<String, Object> result = new java.util.HashMap<>();
|
|
|
+
|
|
|
+ List<StoreTrackEvent> trafficEvents = events.stream()
|
|
|
+ .filter(e -> "TRAFFIC".equals(e.getEventCategory()))
|
|
|
+ .collect(java.util.stream.Collectors.toList());
|
|
|
+
|
|
|
+ // 搜索量
|
|
|
+ long searchCount = trafficEvents.stream()
|
|
|
+ .filter(e -> "SEARCH".equals(e.getEventType()))
|
|
|
+ .count();
|
|
|
+ result.put("searchCount", searchCount);
|
|
|
+
|
|
|
+ // 浏览量
|
|
|
+ long viewCount = trafficEvents.stream()
|
|
|
+ .filter(e -> "VIEW".equals(e.getEventType()))
|
|
|
+ .count();
|
|
|
+ result.put("viewCount", viewCount);
|
|
|
+
|
|
|
+ // 访客数(去重userId)
|
|
|
+ long visitorCount = trafficEvents.stream()
|
|
|
+ .filter(e -> e.getUserId() != null)
|
|
|
+ .map(StoreTrackEvent::getUserId)
|
|
|
+ .distinct()
|
|
|
+ .count();
|
|
|
+ result.put("visitorCount", visitorCount);
|
|
|
+
|
|
|
+ // 新增访客数(在统计日期之前没有访问记录的用户)
|
|
|
+ // TODO: 需要查询历史数据,暂时返回0
|
|
|
+ result.put("newVisitorCount", 0L);
|
|
|
+
|
|
|
+ // 总访问时长(所有浏览事件duration字段的总和)
|
|
|
+ long totalDuration = trafficEvents.stream()
|
|
|
+ .filter(e -> "VIEW".equals(e.getEventType()) && e.getDuration() != null)
|
|
|
+ .mapToLong(StoreTrackEvent::getDuration)
|
|
|
+ .sum();
|
|
|
+ result.put("totalDuration", totalDuration);
|
|
|
+
|
|
|
+ // 平均访问时长
|
|
|
+ long viewEventsWithDuration = trafficEvents.stream()
|
|
|
+ .filter(e -> "VIEW".equals(e.getEventType()) && e.getDuration() != null)
|
|
|
+ .count();
|
|
|
+ long avgDuration = viewEventsWithDuration > 0 ? totalDuration / viewEventsWithDuration : 0L;
|
|
|
+ result.put("avgDuration", avgDuration);
|
|
|
+
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算互动数据(符合文档格式)
|
|
|
+ */
|
|
|
+ private Map<String, Object> calculateInteractionData(List<StoreTrackEvent> events, Integer storeId) {
|
|
|
+ Map<String, Object> result = new java.util.HashMap<>();
|
|
|
+
|
|
|
+ List<StoreTrackEvent> interactionEvents = events.stream()
|
|
|
+ .filter(e -> "INTERACTION".equals(e.getEventCategory()))
|
|
|
+ .collect(java.util.stream.Collectors.toList());
|
|
|
+
|
|
|
+ result.put("collectCount", countByEventType(interactionEvents, "COLLECT"));
|
|
|
+ result.put("shareCount", countByEventType(interactionEvents, "SHARE"));
|
|
|
+ result.put("checkinCount", countByEventType(interactionEvents, "CHECKIN"));
|
|
|
+ result.put("consultCount", countByEventType(interactionEvents, "CONSULT"));
|
|
|
+ result.put("postLikeCount", countByEventType(interactionEvents, "POST_LIKE"));
|
|
|
+ result.put("postCommentCount", countByEventType(interactionEvents, "POST_COMMENT"));
|
|
|
+ result.put("postRepostCount", countByEventType(interactionEvents, "POST_REPOST"));
|
|
|
+ result.put("reportCount", countByEventType(interactionEvents, "REPORT"));
|
|
|
+ result.put("blockCount", countByEventType(interactionEvents, "BLOCK"));
|
|
|
+
|
|
|
+ // TODO: 需要从其他表查询的数据,暂时返回0
|
|
|
+ result.put("friendCount", 0L);
|
|
|
+ result.put("followCount", 0L);
|
|
|
+ result.put("fansCount", 0L);
|
|
|
+ result.put("postCount", 0L);
|
|
|
+
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算优惠券数据(符合文档格式)
|
|
|
+ */
|
|
|
+ private Map<String, Object> calculateCouponData(List<StoreTrackEvent> events, Integer storeId) {
|
|
|
+ Map<String, Object> result = new java.util.HashMap<>();
|
|
|
+
|
|
|
+ // TODO: 需要从 life_discount_coupon_user 等表查询,暂时返回0
|
|
|
+ result.put("giveToFriendCount", 0L);
|
|
|
+ result.put("giveToFriendAmount", java.math.BigDecimal.ZERO);
|
|
|
+ result.put("giveToFriendUseCount", 0L);
|
|
|
+ result.put("giveToFriendUseAmount", java.math.BigDecimal.ZERO);
|
|
|
+ result.put("giveToFriendUseAmountPercent", 0.0);
|
|
|
+ result.put("friendGiveCount", 0L);
|
|
|
+ result.put("friendGiveAmount", java.math.BigDecimal.ZERO);
|
|
|
+ result.put("friendGiveUseCount", 0L);
|
|
|
+ result.put("friendGiveUseAmount", java.math.BigDecimal.ZERO);
|
|
|
+ result.put("friendGiveUseAmountPercent", 0.0);
|
|
|
+
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算代金券数据(符合文档格式)
|
|
|
+ */
|
|
|
+ private Map<String, Object> calculateVoucherData(List<StoreTrackEvent> events, Integer storeId) {
|
|
|
+ Map<String, Object> result = new java.util.HashMap<>();
|
|
|
+
|
|
|
+ List<StoreTrackEvent> voucherEvents = events.stream()
|
|
|
+ .filter(e -> "VOUCHER".equals(e.getEventCategory()))
|
|
|
+ .collect(java.util.stream.Collectors.toList());
|
|
|
+
|
|
|
+ // 赠送好友数量
|
|
|
+ long giveToFriendCount = countByEventType(voucherEvents, "VOUCHER_GIVE");
|
|
|
+ result.put("giveToFriendCount", giveToFriendCount);
|
|
|
+
|
|
|
+ // 赠送好友金额合计
|
|
|
+ java.math.BigDecimal giveToFriendAmount = voucherEvents.stream()
|
|
|
+ .filter(e -> "VOUCHER_GIVE".equals(e.getEventType()) && e.getAmount() != null)
|
|
|
+ .map(StoreTrackEvent::getAmount)
|
|
|
+ .reduce(java.math.BigDecimal.ZERO, java.math.BigDecimal::add);
|
|
|
+ result.put("giveToFriendAmount", giveToFriendAmount);
|
|
|
+
|
|
|
+ // 赠送好友使用数量
|
|
|
+ long giveToFriendUseCount = countByEventType(voucherEvents, "VOUCHER_USE");
|
|
|
+ result.put("giveToFriendUseCount", giveToFriendUseCount);
|
|
|
+
|
|
|
+ // 赠送好友使用金额合计
|
|
|
+ java.math.BigDecimal giveToFriendUseAmount = voucherEvents.stream()
|
|
|
+ .filter(e -> "VOUCHER_USE".equals(e.getEventType()) && e.getAmount() != null)
|
|
|
+ .map(StoreTrackEvent::getAmount)
|
|
|
+ .reduce(java.math.BigDecimal.ZERO, java.math.BigDecimal::add);
|
|
|
+ result.put("giveToFriendUseAmount", giveToFriendUseAmount);
|
|
|
+
|
|
|
+ // 赠送好友使用金额占比
|
|
|
+ double giveToFriendUseAmountPercent = 0.0;
|
|
|
+ if (giveToFriendAmount.compareTo(java.math.BigDecimal.ZERO) > 0) {
|
|
|
+ giveToFriendUseAmountPercent = giveToFriendUseAmount.divide(giveToFriendAmount, 4, java.math.RoundingMode.HALF_UP)
|
|
|
+ .multiply(new java.math.BigDecimal("100")).doubleValue();
|
|
|
+ }
|
|
|
+ result.put("giveToFriendUseAmountPercent", giveToFriendUseAmountPercent);
|
|
|
+
|
|
|
+ // TODO: 需要从 life_discount_coupon_store_friend 表查询的数据,暂时返回0
|
|
|
+ result.put("friendGiveCount", 0L);
|
|
|
+ result.put("friendGiveAmount", java.math.BigDecimal.ZERO);
|
|
|
+ result.put("friendGiveUseCount", 0L);
|
|
|
+ result.put("friendGiveUseAmount", java.math.BigDecimal.ZERO);
|
|
|
+ result.put("friendGiveUseAmountPercent", 0.0);
|
|
|
+
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算服务质量数据(符合文档格式)
|
|
|
+ */
|
|
|
+ private Map<String, Object> calculateServiceData(List<StoreTrackEvent> events, Integer storeId) {
|
|
|
+ Map<String, Object> result = new java.util.HashMap<>();
|
|
|
+
|
|
|
+ List<StoreTrackEvent> serviceEvents = events.stream()
|
|
|
+ .filter(e -> "SERVICE".equals(e.getEventCategory()))
|
|
|
+ .collect(java.util.stream.Collectors.toList());
|
|
|
+
|
|
|
+ // 差评申诉次数
|
|
|
+ long appealCount = countByEventType(serviceEvents, "APPEAL");
|
|
|
+ result.put("appealCount", appealCount);
|
|
|
+
|
|
|
+ // TODO: 需要从 common_rating 和 store_comment_appeal 表查询的数据,暂时返回0或null
|
|
|
+ result.put("storeScore", null);
|
|
|
+ result.put("tasteScore", null);
|
|
|
+ result.put("environmentScore", null);
|
|
|
+ result.put("serviceScore", null);
|
|
|
+ result.put("ratingCount", 0L);
|
|
|
+ result.put("goodRatingCount", 0L);
|
|
|
+ result.put("midRatingCount", 0L);
|
|
|
+ result.put("badRatingCount", 0L);
|
|
|
+ result.put("badRatingPercent", 0.0);
|
|
|
+ result.put("appealSuccessCount", 0L);
|
|
|
+ result.put("appealSuccessPercent", 0.0);
|
|
|
+
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算价目表排名数据(符合文档格式)
|
|
|
+ */
|
|
|
+ private List<Map<String, Object>> calculatePriceRankingData(List<StoreTrackEvent> events) {
|
|
|
+ List<StoreTrackEvent> priceEvents = events.stream()
|
|
|
+ .filter(e -> "PRICE".equals(e.getEventCategory()))
|
|
|
+ .collect(java.util.stream.Collectors.toList());
|
|
|
+
|
|
|
+ // 按 targetId (priceId) 分组统计
|
|
|
+ Map<Integer, List<StoreTrackEvent>> eventsByPriceId = priceEvents.stream()
|
|
|
+ .filter(e -> e.getTargetId() != null)
|
|
|
+ .collect(java.util.stream.Collectors.groupingBy(StoreTrackEvent::getTargetId));
|
|
|
+
|
|
|
+ List<Map<String, Object>> result = new java.util.ArrayList<>();
|
|
|
+
|
|
|
+ for (Map.Entry<Integer, List<StoreTrackEvent>> entry : eventsByPriceId.entrySet()) {
|
|
|
+ Integer priceId = entry.getKey();
|
|
|
+ List<StoreTrackEvent> priceIdEvents = entry.getValue();
|
|
|
+
|
|
|
+ Map<String, Object> priceData = new java.util.HashMap<>();
|
|
|
+ priceData.put("priceId", priceId);
|
|
|
+
|
|
|
+ // 浏览量(PRICE_VIEW事件数量)
|
|
|
+ long viewCount = priceIdEvents.stream()
|
|
|
+ .filter(e -> "PRICE_VIEW".equals(e.getEventType()))
|
|
|
+ .count();
|
|
|
+ priceData.put("viewCount", (int) viewCount);
|
|
|
+
|
|
|
+ // 访客数(去重后的用户ID数量)
|
|
|
+ long visitorCount = priceIdEvents.stream()
|
|
|
+ .filter(e -> e.getUserId() != null)
|
|
|
+ .map(StoreTrackEvent::getUserId)
|
|
|
+ .distinct()
|
|
|
+ .count();
|
|
|
+ priceData.put("visitorCount", (int) visitorCount);
|
|
|
+
|
|
|
+ // 分享数(PRICE_SHARE事件数量)
|
|
|
+ long shareCount = priceIdEvents.stream()
|
|
|
+ .filter(e -> "PRICE_SHARE".equals(e.getEventType()))
|
|
|
+ .count();
|
|
|
+ priceData.put("shareCount", (int) shareCount);
|
|
|
+
|
|
|
+ result.add(priceData);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 按 viewCount 降序排列
|
|
|
+ result.sort((a, b) -> {
|
|
|
+ Integer viewCountA = (Integer) a.get("viewCount");
|
|
|
+ Integer viewCountB = (Integer) b.get("viewCount");
|
|
|
+ return viewCountB.compareTo(viewCountA);
|
|
|
+ });
|
|
|
+
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 统计指定事件类型的数量
|
|
|
+ */
|
|
|
+ private long countByEventType(List<StoreTrackEvent> events, String eventType) {
|
|
|
+ return events.stream()
|
|
|
+ .filter(e -> eventType.equals(e.getEventType()))
|
|
|
+ .count();
|
|
|
+ }
|
|
|
}
|