|
|
@@ -0,0 +1,410 @@
|
|
|
+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 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.*;
|
|
|
+import java.util.stream.Collectors;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 埋点事件服务实现类
|
|
|
+ *
|
|
|
+ * @author system
|
|
|
+ * @since 2026-01-14
|
|
|
+ */
|
|
|
+@Slf4j
|
|
|
+@Service
|
|
|
+@RequiredArgsConstructor
|
|
|
+@Transactional
|
|
|
+public class TrackEventServiceImpl extends ServiceImpl<StoreTrackEventMapper, StoreTrackEvent> implements TrackEventService {
|
|
|
+
|
|
|
+ private final StoreTrackEventMapper trackEventMapper;
|
|
|
+ private final StoreTrackStatisticsMapper trackStatisticsMapper;
|
|
|
+ private final BaseRedisService baseRedisService;
|
|
|
+
|
|
|
+ private static final String REDIS_QUEUE_KEY = "track:event:queue";
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void saveTrackEvent(StoreTrackEvent trackEvent) {
|
|
|
+ // 异步写入Redis List
|
|
|
+ try {
|
|
|
+ String eventJson = JSON.toJSONString(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);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void batchSaveTrackEvents(List<StoreTrackEvent> trackEvents) {
|
|
|
+ if (trackEvents == null || trackEvents.isEmpty()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 批量保存到数据库
|
|
|
+ boolean result = this.saveBatch(trackEvents, 100);
|
|
|
+ log.info("批量保存埋点事件: 总数={}, 成功={}", trackEvents.size(), result);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("批量保存埋点事件失败", e);
|
|
|
+ throw new RuntimeException("批量保存埋点事件失败", e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Map<String, Object> getBusinessData(Integer storeId, Date startDate, Date endDate, String category) {
|
|
|
+ Map<String, Object> result = new HashMap<>();
|
|
|
+
|
|
|
+ if (category == null || "TRAFFIC".equals(category)) {
|
|
|
+ Map<String, Object> trafficData = getTrafficData(storeId, startDate, endDate);
|
|
|
+ result.put("trafficData", trafficData);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (category == null || "INTERACTION".equals(category)) {
|
|
|
+ Map<String, Object> interactionData = getInteractionData(storeId, startDate, endDate);
|
|
|
+ result.put("interactionData", interactionData);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (category == null || "COUPON".equals(category)) {
|
|
|
+ Map<String, Object> couponData = getCouponData(storeId, startDate, endDate);
|
|
|
+ result.put("couponData", couponData);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (category == null || "VOUCHER".equals(category)) {
|
|
|
+ Map<String, Object> voucherData = getVoucherData(storeId, startDate, endDate);
|
|
|
+ result.put("voucherData", voucherData);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (category == null || "SERVICE".equals(category)) {
|
|
|
+ Map<String, Object> serviceData = getServiceData(storeId, startDate, endDate);
|
|
|
+ result.put("serviceData", serviceData);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (category == null || "PRICE".equals(category)) {
|
|
|
+ List<Map<String, Object>> priceRankingData = getPriceRankingData(storeId, startDate, endDate, 10);
|
|
|
+ result.put("priceRankingData", priceRankingData);
|
|
|
+ }
|
|
|
+
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Map<String, Object> compareBusinessData(Integer storeId, Date startDate1, Date endDate1, Date startDate2, Date endDate2) {
|
|
|
+ Map<String, Object> result = new HashMap<>();
|
|
|
+
|
|
|
+ // 获取时间段1的数据
|
|
|
+ Map<String, Object> period1Data = getBusinessData(storeId, startDate1, endDate1, null);
|
|
|
+ result.put("period1", period1Data);
|
|
|
+
|
|
|
+ // 获取时间段2的数据
|
|
|
+ Map<String, Object> period2Data = getBusinessData(storeId, startDate2, endDate2, null);
|
|
|
+ result.put("period2", period2Data);
|
|
|
+
|
|
|
+ // 计算对比数据
|
|
|
+ Map<String, Object> compareData = calculateCompareData(period1Data, period2Data);
|
|
|
+ result.put("compare", compareData);
|
|
|
+
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void calculateAndSaveStatistics(Integer storeId, Date statDate, String statType) {
|
|
|
+ // 计算统计数据
|
|
|
+ Map<String, Object> businessData = getBusinessData(storeId, statDate, statDate, null);
|
|
|
+
|
|
|
+ // 查询或创建统计记录
|
|
|
+ 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);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 设置统计数据
|
|
|
+ Map<String, Object> trafficData = (Map<String, Object>) businessData.get("trafficData");
|
|
|
+ if (trafficData != null) {
|
|
|
+ statistics.setTrafficData(JSON.toJSONString(trafficData));
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<String, Object> interactionData = (Map<String, Object>) businessData.get("interactionData");
|
|
|
+ if (interactionData != null) {
|
|
|
+ statistics.setInteractionData(JSON.toJSONString(interactionData));
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<String, Object> couponData = (Map<String, Object>) businessData.get("couponData");
|
|
|
+ if (couponData != null) {
|
|
|
+ statistics.setCouponData(JSON.toJSONString(couponData));
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<String, Object> voucherData = (Map<String, Object>) businessData.get("voucherData");
|
|
|
+ if (voucherData != null) {
|
|
|
+ statistics.setVoucherData(JSON.toJSONString(voucherData));
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<String, Object> serviceData = (Map<String, Object>) businessData.get("serviceData");
|
|
|
+ if (serviceData != null) {
|
|
|
+ statistics.setServiceData(JSON.toJSONString(serviceData));
|
|
|
+ }
|
|
|
+
|
|
|
+ List<Map<String, Object>> priceRankingData = (List<Map<String, Object>>) businessData.get("priceRankingData");
|
|
|
+ if (priceRankingData != null) {
|
|
|
+ statistics.setPriceRankingData(JSON.toJSONString(priceRankingData));
|
|
|
+ }
|
|
|
+
|
|
|
+ // 保存统计记录
|
|
|
+ if (statistics.getId() == null) {
|
|
|
+ trackStatisticsMapper.insert(statistics);
|
|
|
+ } else {
|
|
|
+ trackStatisticsMapper.updateById(statistics);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public List<Map<String, Object>> getPriceRankingData(Integer storeId, Date startDate, Date endDate, Integer limit) {
|
|
|
+ // 查询价目表相关的埋点数据
|
|
|
+ LambdaQueryWrapper<StoreTrackEvent> queryWrapper = new LambdaQueryWrapper<>();
|
|
|
+ queryWrapper.eq(StoreTrackEvent::getStoreId, storeId)
|
|
|
+ .in(StoreTrackEvent::getEventType, Arrays.asList("PRICE_VIEW", "PRICE_SHARE"))
|
|
|
+ .eq(StoreTrackEvent::getTargetType, "PRICE")
|
|
|
+ .ge(StoreTrackEvent::getEventTime, startDate)
|
|
|
+ .le(StoreTrackEvent::getEventTime, endDate)
|
|
|
+ .eq(StoreTrackEvent::getDeleteFlag, 0);
|
|
|
+
|
|
|
+ List<StoreTrackEvent> events = trackEventMapper.selectList(queryWrapper);
|
|
|
+
|
|
|
+ // 按targetId聚合统计
|
|
|
+ Map<Integer, Map<String, Object>> priceMap = new HashMap<>();
|
|
|
+ for (StoreTrackEvent event : events) {
|
|
|
+ Integer targetId = event.getTargetId();
|
|
|
+ if (targetId == null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<String, Object> priceStat = priceMap.computeIfAbsent(targetId, k -> {
|
|
|
+ Map<String, Object> stat = new HashMap<>();
|
|
|
+ stat.put("priceId", targetId);
|
|
|
+ stat.put("viewCount", 0);
|
|
|
+ stat.put("visitorCount", 0);
|
|
|
+ stat.put("shareCount", 0);
|
|
|
+ stat.put("visitors", new HashSet<>());
|
|
|
+ return stat;
|
|
|
+ });
|
|
|
+
|
|
|
+ if ("PRICE_VIEW".equals(event.getEventType())) {
|
|
|
+ Integer viewCount = (Integer) priceStat.get("viewCount");
|
|
|
+ priceStat.put("viewCount", viewCount + 1);
|
|
|
+
|
|
|
+ // 统计访客数(去重)
|
|
|
+ Set<Integer> visitors = (Set<Integer>) priceStat.get("visitors");
|
|
|
+ if (event.getUserId() != null) {
|
|
|
+ visitors.add(event.getUserId());
|
|
|
+ }
|
|
|
+ priceStat.put("visitorCount", visitors.size());
|
|
|
+ } else if ("PRICE_SHARE".equals(event.getEventType())) {
|
|
|
+ Integer shareCount = (Integer) priceStat.get("shareCount");
|
|
|
+ priceStat.put("shareCount", shareCount + 1);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 转换为列表并按浏览量排序
|
|
|
+ List<Map<String, Object>> result = priceMap.values().stream()
|
|
|
+ .sorted((a, b) -> Integer.compare((Integer) b.get("viewCount"), (Integer) a.get("viewCount")))
|
|
|
+ .limit(limit != null ? limit : 10)
|
|
|
+ .map(stat -> {
|
|
|
+ Map<String, Object> item = new HashMap<>();
|
|
|
+ item.put("priceId", stat.get("priceId"));
|
|
|
+ item.put("viewCount", stat.get("viewCount"));
|
|
|
+ item.put("visitorCount", stat.get("visitorCount"));
|
|
|
+ item.put("shareCount", stat.get("shareCount"));
|
|
|
+ return item;
|
|
|
+ })
|
|
|
+ .collect(Collectors.toList());
|
|
|
+
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取流量数据
|
|
|
+ */
|
|
|
+ private Map<String, Object> getTrafficData(Integer storeId, Date startDate, Date endDate) {
|
|
|
+ Map<String, Object> result = new HashMap<>();
|
|
|
+
|
|
|
+ LambdaQueryWrapper<StoreTrackEvent> queryWrapper = new LambdaQueryWrapper<>();
|
|
|
+ queryWrapper.eq(StoreTrackEvent::getStoreId, storeId)
|
|
|
+ .eq(StoreTrackEvent::getEventCategory, "TRAFFIC")
|
|
|
+ .ge(StoreTrackEvent::getEventTime, startDate)
|
|
|
+ .le(StoreTrackEvent::getEventTime, endDate)
|
|
|
+ .eq(StoreTrackEvent::getDeleteFlag, 0);
|
|
|
+
|
|
|
+ List<StoreTrackEvent> events = trackEventMapper.selectList(queryWrapper);
|
|
|
+
|
|
|
+ // 统计搜索量
|
|
|
+ long searchCount = events.stream()
|
|
|
+ .filter(e -> "SEARCH".equals(e.getEventType()))
|
|
|
+ .count();
|
|
|
+ result.put("searchCount", searchCount);
|
|
|
+
|
|
|
+ // 统计浏览量
|
|
|
+ long viewCount = events.stream()
|
|
|
+ .filter(e -> "VIEW".equals(e.getEventType()))
|
|
|
+ .count();
|
|
|
+ result.put("viewCount", viewCount);
|
|
|
+
|
|
|
+ // 统计访客数(去重)
|
|
|
+ Set<Integer> visitors = events.stream()
|
|
|
+ .filter(e -> "VIEW".equals(e.getEventType()) && e.getUserId() != null)
|
|
|
+ .map(StoreTrackEvent::getUserId)
|
|
|
+ .collect(Collectors.toSet());
|
|
|
+ result.put("visitorCount", visitors.size());
|
|
|
+
|
|
|
+ // 统计新增访客数(需要查询历史数据判断)
|
|
|
+ // TODO: 实现新增访客数统计逻辑
|
|
|
+
|
|
|
+ // 统计访问时长
|
|
|
+ long totalDuration = events.stream()
|
|
|
+ .filter(e -> "VIEW".equals(e.getEventType()) && e.getDuration() != null)
|
|
|
+ .mapToLong(StoreTrackEvent::getDuration)
|
|
|
+ .sum();
|
|
|
+ result.put("totalDuration", totalDuration);
|
|
|
+
|
|
|
+ // 计算平均访问时长
|
|
|
+ long viewCountWithDuration = events.stream()
|
|
|
+ .filter(e -> "VIEW".equals(e.getEventType()) && e.getDuration() != null)
|
|
|
+ .count();
|
|
|
+ long avgDuration = viewCountWithDuration > 0 ? totalDuration / viewCountWithDuration : 0;
|
|
|
+ result.put("avgDuration", avgDuration);
|
|
|
+
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取互动数据
|
|
|
+ */
|
|
|
+ private Map<String, Object> getInteractionData(Integer storeId, Date startDate, Date endDate) {
|
|
|
+ Map<String, Object> result = new HashMap<>();
|
|
|
+
|
|
|
+ LambdaQueryWrapper<StoreTrackEvent> queryWrapper = new LambdaQueryWrapper<>();
|
|
|
+ queryWrapper.eq(StoreTrackEvent::getStoreId, storeId)
|
|
|
+ .eq(StoreTrackEvent::getEventCategory, "INTERACTION")
|
|
|
+ .ge(StoreTrackEvent::getEventTime, startDate)
|
|
|
+ .le(StoreTrackEvent::getEventTime, endDate)
|
|
|
+ .eq(StoreTrackEvent::getDeleteFlag, 0);
|
|
|
+
|
|
|
+ List<StoreTrackEvent> events = trackEventMapper.selectList(queryWrapper);
|
|
|
+
|
|
|
+ // 统计各类互动数据
|
|
|
+ result.put("collectCount", events.stream().filter(e -> "COLLECT".equals(e.getEventType())).count());
|
|
|
+ result.put("shareCount", events.stream().filter(e -> "SHARE".equals(e.getEventType())).count());
|
|
|
+ result.put("checkinCount", events.stream().filter(e -> "CHECKIN".equals(e.getEventType())).count());
|
|
|
+ result.put("consultCount", events.stream().filter(e -> "CONSULT".equals(e.getEventType())).count());
|
|
|
+
|
|
|
+ // TODO: 实现好友、关注、粉丝、动态等数据的统计(需要从其他表查询)
|
|
|
+
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取优惠券数据
|
|
|
+ */
|
|
|
+ private Map<String, Object> getCouponData(Integer storeId, Date startDate, Date endDate) {
|
|
|
+ Map<String, Object> result = new HashMap<>();
|
|
|
+
|
|
|
+ // TODO: 实现优惠券数据统计(需要从优惠券相关表查询)
|
|
|
+
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取代金券数据
|
|
|
+ */
|
|
|
+ private Map<String, Object> getVoucherData(Integer storeId, Date startDate, Date endDate) {
|
|
|
+ Map<String, Object> result = new HashMap<>();
|
|
|
+
|
|
|
+ // TODO: 实现代金券数据统计(需要从代金券相关表查询)
|
|
|
+
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取服务质量数据
|
|
|
+ */
|
|
|
+ private Map<String, Object> getServiceData(Integer storeId, Date startDate, Date endDate) {
|
|
|
+ Map<String, Object> result = new HashMap<>();
|
|
|
+
|
|
|
+ // TODO: 实现服务质量数据统计(需要从评价表查询)
|
|
|
+
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算对比数据
|
|
|
+ */
|
|
|
+ private Map<String, Object> calculateCompareData(Map<String, Object> period1Data, Map<String, Object> period2Data) {
|
|
|
+ Map<String, Object> compareData = new HashMap<>();
|
|
|
+
|
|
|
+ // 对比流量数据
|
|
|
+ Map<String, Object> traffic1 = (Map<String, Object>) period1Data.get("trafficData");
|
|
|
+ Map<String, Object> traffic2 = (Map<String, Object>) period2Data.get("trafficData");
|
|
|
+
|
|
|
+ if (traffic1 != null && traffic2 != null) {
|
|
|
+ Map<String, Object> trafficCompare = new HashMap<>();
|
|
|
+
|
|
|
+ Long viewCount1 = getLongValue(traffic1, "viewCount");
|
|
|
+ Long viewCount2 = getLongValue(traffic2, "viewCount");
|
|
|
+ if (viewCount1 != null && viewCount2 != null) {
|
|
|
+ double changePercent = calculateChangePercent(viewCount1, viewCount2);
|
|
|
+ trafficCompare.put("viewCountChange", changePercent);
|
|
|
+ }
|
|
|
+
|
|
|
+ compareData.put("traffic", trafficCompare);
|
|
|
+ }
|
|
|
+
|
|
|
+ // TODO: 实现其他数据的对比
|
|
|
+
|
|
|
+ return compareData;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Long getLongValue(Map<String, Object> map, String key) {
|
|
|
+ Object value = map.get(key);
|
|
|
+ if (value == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ if (value instanceof Long) {
|
|
|
+ return (Long) value;
|
|
|
+ }
|
|
|
+ if (value instanceof Integer) {
|
|
|
+ return ((Integer) value).longValue();
|
|
|
+ }
|
|
|
+ if (value instanceof Number) {
|
|
|
+ return ((Number) value).longValue();
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private double calculateChangePercent(long value1, long value2) {
|
|
|
+ if (value2 == 0) {
|
|
|
+ return value1 > 0 ? 100.0 : 0.0;
|
|
|
+ }
|
|
|
+ return ((double) (value1 - value2) / value2) * 100.0;
|
|
|
+ }
|
|
|
+}
|