|
|
@@ -0,0 +1,297 @@
|
|
|
+# 埋点实现代码清单
|
|
|
+
|
|
|
+## 一、已创建的文件
|
|
|
+
|
|
|
+### 1. 实体类(Entity)
|
|
|
+- ✅ `alien-entity/src/main/java/shop/alien/entity/store/StoreTrackEvent.java` - 埋点事件实体类
|
|
|
+- ✅ `alien-entity/src/main/java/shop/alien/entity/store/StoreTrackStatistics.java` - 埋点统计数据实体类
|
|
|
+
|
|
|
+### 2. 注解(Annotation)
|
|
|
+- ✅ `alien-store/src/main/java/shop/alien/store/annotation/TrackEvent.java` - 埋点注解
|
|
|
+
|
|
|
+### 3. 服务接口(Service Interface)
|
|
|
+- ✅ `alien-store/src/main/java/shop/alien/store/service/TrackEventService.java` - 埋点事件服务接口
|
|
|
+
|
|
|
+### 4. Controller
|
|
|
+- ✅ `alien-store/src/main/java/shop/alien/store/controller/TrackEventController.java` - 埋点上报接口
|
|
|
+
|
|
|
+### 5. 文档
|
|
|
+- ✅ `alien-store/doc/埋点需求完整方案.md` - 完整方案文档
|
|
|
+
|
|
|
+## 二、待创建的文件(核心代码)
|
|
|
+
|
|
|
+### 1. AOP切面(重要)
|
|
|
+**文件路径**: `alien-store/src/main/java/shop/alien/store/aspect/TrackEventAspect.java`
|
|
|
+
|
|
|
+**功能**: 拦截标注了`@TrackEvent`注解的方法,自动收集埋点数据并写入Redis List
|
|
|
+
|
|
|
+**关键代码要点**:
|
|
|
+- 使用`@Around`环绕通知
|
|
|
+- 解析SpEL表达式获取`storeId`、`userId`等参数
|
|
|
+- 调用`BaseRedisService.setListRight()`写入Redis List
|
|
|
+- 使用`@Order`注解设置切面执行顺序
|
|
|
+
|
|
|
+### 2. Service实现类(重要)
|
|
|
+**文件路径**: `alien-store/src/main/java/shop/alien/store/service/impl/TrackEventServiceImpl.java`
|
|
|
+
|
|
|
+**功能**:
|
|
|
+- 实现`saveTrackEvent()`方法,将埋点数据写入Redis List
|
|
|
+- 实现`batchSaveTrackEvents()`方法,批量保存到数据库
|
|
|
+- 实现`getBusinessData()`方法,统计查询经营数据
|
|
|
+- 实现`compareBusinessData()`方法,对比数据
|
|
|
+- 实现`calculateAndSaveStatistics()`方法,计算统计数据
|
|
|
+- 实现`getPriceRankingData()`方法,获取价目表排名
|
|
|
+
|
|
|
+### 3. 数据消费服务(重要)
|
|
|
+**文件路径**: `alien-store/src/main/java/shop/alien/store/service/TrackEventConsumer.java`
|
|
|
+
|
|
|
+**功能**: 定时任务,从Redis List批量消费数据并写入数据库
|
|
|
+
|
|
|
+**关键代码要点**:
|
|
|
+- 使用`@Scheduled(cron = "0/10 * * * * ?")`每10秒执行一次
|
|
|
+- 使用分布式锁防止多实例重复消费
|
|
|
+- 每次从Redis List取出100条数据
|
|
|
+- 批量保存到数据库
|
|
|
+
|
|
|
+### 4. Mapper接口
|
|
|
+**文件路径**: `alien-store/src/main/java/shop/alien/mapper/StoreTrackEventMapper.java`
|
|
|
+**文件路径**: `alien-store/src/main/java/shop/alien/mapper/StoreTrackStatisticsMapper.java`
|
|
|
+
|
|
|
+### 5. 统计查询Controller
|
|
|
+**文件路径**: `alien-store/src/main/java/shop/alien/store/controller/BusinessDataController.java`
|
|
|
+
|
|
|
+**接口列表**:
|
|
|
+- `GET /business/data` - 查询经营数据
|
|
|
+- `GET /business/data/compare` - 数据对比
|
|
|
+- `GET /business/data/history` - 历史数据查询
|
|
|
+
|
|
|
+### 6. AI推荐Controller
|
|
|
+**文件路径**: `alien-store/src/main/java/shop/alien/store/controller/AIRecoveryController.java`
|
|
|
+
|
|
|
+**接口列表**:
|
|
|
+- `GET /business/ai/recommendation` - 获取AI推荐
|
|
|
+
|
|
|
+## 三、关键代码示例
|
|
|
+
|
|
|
+### 3.1 AOP切面核心代码
|
|
|
+
|
|
|
+```java
|
|
|
+@Slf4j
|
|
|
+@Aspect
|
|
|
+@Component
|
|
|
+@Order(2)
|
|
|
+@RequiredArgsConstructor
|
|
|
+public class TrackEventAspect {
|
|
|
+
|
|
|
+ private final BaseRedisService baseRedisService;
|
|
|
+ private final SpelExpressionParser spelExpressionParser = new SpelExpressionParser();
|
|
|
+ private final DefaultParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
|
|
|
+ private static final String REDIS_QUEUE_KEY = "track:event:queue";
|
|
|
+
|
|
|
+ @Around("@annotation(trackEvent)")
|
|
|
+ public Object around(ProceedingJoinPoint joinPoint, TrackEvent trackEvent) throws Throwable {
|
|
|
+ Object result = joinPoint.proceed();
|
|
|
+
|
|
|
+ // 构建埋点事件对象
|
|
|
+ StoreTrackEvent event = buildTrackEvent(joinPoint, trackEvent);
|
|
|
+
|
|
|
+ // 异步写入Redis List
|
|
|
+ if (trackEvent.async()) {
|
|
|
+ baseRedisService.setListRight(REDIS_QUEUE_KEY, JSON.toJSONString(event));
|
|
|
+ } else {
|
|
|
+ // 同步写入
|
|
|
+ trackEventService.batchSaveTrackEvents(Collections.singletonList(event));
|
|
|
+ }
|
|
|
+
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ private StoreTrackEvent buildTrackEvent(ProceedingJoinPoint joinPoint, TrackEvent annotation) {
|
|
|
+ // 解析SpEL表达式获取参数值
|
|
|
+ // 获取用户ID、店铺ID等
|
|
|
+ // 设置IP、User-Agent等
|
|
|
+ // ...
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 3.2 Redis List消费核心代码
|
|
|
+
|
|
|
+```java
|
|
|
+@Component
|
|
|
+@RequiredArgsConstructor
|
|
|
+@Slf4j
|
|
|
+public class TrackEventConsumer {
|
|
|
+
|
|
|
+ private final BaseRedisService baseRedisService;
|
|
|
+ private final TrackEventService trackEventService;
|
|
|
+ private static final String REDIS_QUEUE_KEY = "track:event:queue";
|
|
|
+ private static final String CONSUMER_LOCK_KEY = "track:event:consumer:lock";
|
|
|
+ private static final int BATCH_SIZE = 100;
|
|
|
+
|
|
|
+ @Scheduled(cron = "0/10 * * * * ?") // 每10秒执行一次
|
|
|
+ public void consumeTrackEvents() {
|
|
|
+ // 获取分布式锁
|
|
|
+ String lockId = baseRedisService.lock(CONSUMER_LOCK_KEY, 5000, 1000);
|
|
|
+ if (lockId == null) {
|
|
|
+ log.debug("获取消费锁失败,跳过本次消费");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 批量从Redis List取出数据
|
|
|
+ List<String> eventList = baseRedisService.popBatchFromList(REDIS_QUEUE_KEY);
|
|
|
+
|
|
|
+ if (eventList == null || eventList.isEmpty()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 转换为实体对象
|
|
|
+ List<StoreTrackEvent> events = eventList.stream()
|
|
|
+ .map(json -> JSON.parseObject(json, StoreTrackEvent.class))
|
|
|
+ .collect(Collectors.toList());
|
|
|
+
|
|
|
+ // 批量保存到数据库
|
|
|
+ trackEventService.batchSaveTrackEvents(events);
|
|
|
+
|
|
|
+ log.info("成功消费{}条埋点数据", events.size());
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("消费埋点数据失败", e);
|
|
|
+ } finally {
|
|
|
+ // 释放锁
|
|
|
+ baseRedisService.unlock(CONSUMER_LOCK_KEY, lockId);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 3.3 统计数据查询核心代码
|
|
|
+
|
|
|
+```java
|
|
|
+@Override
|
|
|
+public Map<String, Object> getBusinessData(Integer storeId, Date startDate, Date endDate, String category) {
|
|
|
+ Map<String, Object> result = new HashMap<>();
|
|
|
+
|
|
|
+ if (category == null || category.equals("TRAFFIC")) {
|
|
|
+ // 流量数据统计
|
|
|
+ Map<String, Object> trafficData = new HashMap<>();
|
|
|
+ // 查询浏览量、访客数等
|
|
|
+ result.put("trafficData", trafficData);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (category == null || category.equals("INTERACTION")) {
|
|
|
+ // 互动数据统计
|
|
|
+ Map<String, Object> interactionData = new HashMap<>();
|
|
|
+ // 查询收藏、分享等
|
|
|
+ result.put("interactionData", interactionData);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ... 其他分类数据
|
|
|
+
|
|
|
+ return result;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+## 四、数据库表SQL
|
|
|
+
|
|
|
+### 4.1 埋点事件表
|
|
|
+
|
|
|
+```sql
|
|
|
+CREATE TABLE `store_track_event` (
|
|
|
+ `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
|
|
+ `event_type` varchar(50) NOT NULL COMMENT '事件类型',
|
|
|
+ `event_category` varchar(50) NOT NULL COMMENT '事件分类',
|
|
|
+ `user_id` int(11) DEFAULT NULL COMMENT '用户ID',
|
|
|
+ `store_id` int(11) DEFAULT NULL COMMENT '店铺ID',
|
|
|
+ `target_id` int(11) DEFAULT NULL COMMENT '目标对象ID',
|
|
|
+ `target_type` varchar(50) DEFAULT NULL COMMENT '目标对象类型',
|
|
|
+ `event_data` text COMMENT '事件附加数据(JSON格式)',
|
|
|
+ `amount` decimal(10,2) DEFAULT NULL COMMENT '金额',
|
|
|
+ `duration` bigint(20) DEFAULT NULL COMMENT '时长(毫秒)',
|
|
|
+ `ip_address` varchar(50) DEFAULT NULL COMMENT 'IP地址',
|
|
|
+ `user_agent` varchar(500) DEFAULT NULL COMMENT '用户代理',
|
|
|
+ `device_type` varchar(20) DEFAULT NULL COMMENT '设备类型',
|
|
|
+ `app_version` varchar(20) DEFAULT NULL COMMENT 'APP版本号',
|
|
|
+ `event_time` datetime NOT NULL COMMENT '事件发生时间',
|
|
|
+ `created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
|
|
+ `delete_flag` int(1) NOT NULL DEFAULT '0' COMMENT '删除标记',
|
|
|
+ PRIMARY KEY (`id`),
|
|
|
+ KEY `idx_store_id` (`store_id`),
|
|
|
+ KEY `idx_user_id` (`user_id`),
|
|
|
+ KEY `idx_event_type` (`event_type`),
|
|
|
+ KEY `idx_event_category` (`event_category`),
|
|
|
+ KEY `idx_event_time` (`event_time`),
|
|
|
+ KEY `idx_store_event_time` (`store_id`,`event_time`)
|
|
|
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='埋点事件表';
|
|
|
+```
|
|
|
+
|
|
|
+### 4.2 埋点统计表
|
|
|
+
|
|
|
+```sql
|
|
|
+CREATE TABLE `store_track_statistics` (
|
|
|
+ `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
|
|
+ `store_id` int(11) NOT NULL COMMENT '店铺ID',
|
|
|
+ `stat_date` date NOT NULL COMMENT '统计日期',
|
|
|
+ `stat_type` varchar(50) NOT NULL COMMENT '统计类型',
|
|
|
+ `traffic_data` text COMMENT '流量数据(JSON格式)',
|
|
|
+ `interaction_data` text COMMENT '互动数据(JSON格式)',
|
|
|
+ `coupon_data` text COMMENT '优惠券数据(JSON格式)',
|
|
|
+ `voucher_data` text COMMENT '代金券数据(JSON格式)',
|
|
|
+ `service_data` text COMMENT '服务质量数据(JSON格式)',
|
|
|
+ `price_ranking_data` text COMMENT '价目表排名数据(JSON格式)',
|
|
|
+ `created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
|
|
+ `updated_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
|
|
+ PRIMARY KEY (`id`),
|
|
|
+ UNIQUE KEY `uk_store_date_type` (`store_id`,`stat_date`,`stat_type`),
|
|
|
+ KEY `idx_store_id` (`store_id`),
|
|
|
+ KEY `idx_stat_date` (`stat_date`)
|
|
|
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='埋点统计表';
|
|
|
+```
|
|
|
+
|
|
|
+## 五、前端调用示例
|
|
|
+
|
|
|
+### 5.1 上报埋点事件
|
|
|
+
|
|
|
+```javascript
|
|
|
+// 上报浏览事件
|
|
|
+axios.post('/track/event', {
|
|
|
+ eventType: 'VIEW',
|
|
|
+ eventCategory: 'TRAFFIC',
|
|
|
+ storeId: 1001,
|
|
|
+ targetType: 'STORE',
|
|
|
+ duration: 3000
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+### 5.2 查询经营数据
|
|
|
+
|
|
|
+```javascript
|
|
|
+axios.get('/business/data', {
|
|
|
+ params: {
|
|
|
+ storeId: 1001,
|
|
|
+ startDate: '2026-01-08',
|
|
|
+ endDate: '2026-01-14'
|
|
|
+ }
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+## 六、后续开发步骤
|
|
|
+
|
|
|
+1. ✅ 创建实体类和注解(已完成)
|
|
|
+2. ⬜ 实现AOP切面(TrackEventAspect)
|
|
|
+3. ⬜ 实现Service实现类(TrackEventServiceImpl)
|
|
|
+4. ⬜ 实现数据消费服务(TrackEventConsumer)
|
|
|
+5. ⬜ 创建Mapper接口
|
|
|
+6. ⬜ 实现统计查询Controller(BusinessDataController)
|
|
|
+7. ⬜ 实现AI推荐Controller(AIRecoveryController)
|
|
|
+8. ⬜ 执行数据库建表SQL
|
|
|
+9. ⬜ 编写单元测试
|
|
|
+10. ⬜ 联调测试
|
|
|
+
|
|
|
+## 七、注意事项
|
|
|
+
|
|
|
+1. Redis List需要设置最大长度,防止内存溢出
|
|
|
+2. 消费服务需要异常处理和重试机制
|
|
|
+3. 统计数据建议使用定时任务预计算
|
|
|
+4. AI推荐接口需要缓存,避免频繁调用AI服务
|
|
|
+5. 埋点数据量大,需要定期清理历史数据
|