Просмотр исходного кода

fix:优化用户端埋点,增加需要埋点的方法,设计定时任务定时统计埋点数据

penghao 2 месяцев назад
Родитель
Сommit
1fdffa7e49
19 измененных файлов с 922 добавлено и 998 удалено
  1. 1 1
      alien-entity/src/main/java/shop/alien/entity/store/StoreTrackEvent.java
  2. 0 297
      alien-store/doc/埋点实现代码清单.md
  3. 0 217
      alien-store/doc/埋点实现总结.md
  4. 13 9
      alien-store/doc/埋点接口清单.md
  5. 0 454
      alien-store/doc/埋点测试指南.md
  6. 22 0
      alien-store/doc/埋点统计数据JSON格式说明.md
  7. 282 8
      alien-store/src/main/java/shop/alien/store/aspect/TrackEventAspect.java
  8. 10 1
      alien-store/src/main/java/shop/alien/store/controller/CommonCommentController.java
  9. 9 0
      alien-store/src/main/java/shop/alien/store/controller/LifeBlacklistController.java
  10. 9 0
      alien-store/src/main/java/shop/alien/store/controller/LifeCommentController.java
  11. 2 0
      alien-store/src/main/java/shop/alien/store/controller/LifeUserDynamicsController.java
  12. 1 1
      alien-store/src/main/java/shop/alien/store/controller/StoreRenovationRequirementController.java
  13. 35 2
      alien-store/src/main/java/shop/alien/store/controller/TrackEventController.java
  14. 9 0
      alien-store/src/main/java/shop/alien/store/controller/UserViolationController.java
  15. 3 2
      alien-store/src/main/java/shop/alien/store/service/TrackEventConsumer.java
  16. 10 0
      alien-store/src/main/java/shop/alien/store/service/TrackEventService.java
  17. 171 0
      alien-store/src/main/java/shop/alien/store/service/TrackStatisticsScheduler.java
  18. 9 0
      alien-store/src/main/java/shop/alien/store/service/impl/StoreInfoServiceImpl.java
  19. 336 6
      alien-store/src/main/java/shop/alien/store/service/impl/TrackEventServiceImpl.java

+ 1 - 1
alien-entity/src/main/java/shop/alien/entity/store/StoreTrackEvent.java

@@ -59,7 +59,7 @@ public class StoreTrackEvent {
     private BigDecimal amount;
 
     @ApiModelProperty(value = "时长(毫秒)")
-    @TableField("duration")
+    @TableField(value = "duration", insertStrategy = com.baomidou.mybatisplus.annotation.FieldStrategy.IGNORED, updateStrategy = com.baomidou.mybatisplus.annotation.FieldStrategy.IGNORED)
     private Long duration;
 
     @ApiModelProperty(value = "IP地址")

+ 0 - 297
alien-store/doc/埋点实现代码清单.md

@@ -1,297 +0,0 @@
-# 埋点实现代码清单
-
-## 一、已创建的文件
-
-### 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. 埋点数据量大,需要定期清理历史数据

+ 0 - 217
alien-store/doc/埋点实现总结.md

@@ -1,217 +0,0 @@
-# 埋点系统实现总结
-
-## ✅ 已完成的工作
-
-### 1. 数据库表设计 ✅
-- **store_track_event** - 埋点事件表
-- **store_track_statistics** - 埋点统计表
-
-> ⚠️ **注意**:需要执行SQL建表语句,详见 `埋点需求完整方案.md`
-
-### 2. 实体类 ✅
-- ✅ `StoreTrackEvent.java` - 埋点事件实体
-- ✅ `StoreTrackStatistics.java` - 统计数据实体
-
-### 3. Mapper接口 ✅
-- ✅ `StoreTrackEventMapper.java`
-- ✅ `StoreTrackStatisticsMapper.java`
-
-### 4. 注解 ✅
-- ✅ `@TrackEvent` - 埋点注解
-
-### 5. AOP切面 ✅
-- ✅ `TrackEventAspect.java` - 自动埋点切面
-  - 拦截标注了`@TrackEvent`的方法
-  - 自动收集数据并写入Redis List
-
-### 6. Service层 ✅
-- ✅ `TrackEventService.java` - 服务接口
-- ✅ `TrackEventServiceImpl.java` - 服务实现
-  - 埋点数据保存(写入Redis List)
-  - 批量保存到数据库
-  - 经营数据统计查询
-  - 数据对比
-  - 价目表排名查询
-
-### 7. 数据消费服务 ✅
-- ✅ `TrackEventConsumer.java` - 定时消费服务
-  - 每10秒执行一次
-  - 从Redis List批量取出数据
-  - 批量写入数据库
-  - 使用分布式锁防止重复消费
-
-### 8. Controller层 ✅
-- ✅ `TrackEventController.java` - 埋点上报接口
-  - `POST /track/event` - 前端主动上报埋点
-  
-- ✅ `BusinessDataController.java` - 经营数据查询接口
-  - `GET /business/data` - 查询经营数据
-  - `GET /business/data/compare` - 数据对比
-  - `GET /business/data/history` - 历史数据查询
-
-- ✅ `AIRecoveryController.java` - AI推荐接口
-  - `GET /business/ai/recommendation` - 获取AI推荐
-
-## 📋 待完成的工作
-
-### 1. 执行数据库建表SQL ⚠️
-需要执行以下SQL创建表结构:
-- `store_track_event` 表
-- `store_track_statistics` 表
-
-详见:`埋点需求完整方案.md` 第 四、数据库表设计 章节
-
-### 2. Service实现类的TODO ⚠️
-在 `TrackEventServiceImpl.java` 中有以下TODO需要实现:
-
-1. **新增访客数统计** - `getTrafficData()` 方法
-   - 需要查询历史数据判断是否为新增访客
-
-2. **互动数据统计** - `getInteractionData()` 方法
-   - 好友数量、关注数量、粉丝数量
-   - 发布动态数量、动态点赞/评论/转发数量
-   - 被举报/拉黑次数
-   - 这些数据需要从其他表查询
-
-3. **优惠券数据统计** - `getCouponData()` 方法
-   - 需要从优惠券相关表查询数据
-   - 实现赠送、使用等统计
-
-4. **代金券数据统计** - `getVoucherData()` 方法
-   - 需要从代金券相关表查询数据
-   - 实现赠送、使用等统计
-
-5. **服务质量数据统计** - `getServiceData()` 方法
-   - 需要从评价表查询数据
-   - 店铺评分、口味评分、环境评分、服务评分
-   - 评价数量、好评/中评/差评统计
-   - 差评申诉相关统计
-
-### 3. AI推荐功能完善 ⚠️
-在 `AIRecoveryController.java` 中:
-- 需要实现 `callAIService()` 方法
-- 根据实际的AI服务接口调用
-- 解析AI返回结果并生成推荐
-
-## 🚀 使用指南
-
-### 1. 在后端方法上使用注解埋点
-
-```java
-@TrackEvent(
-    eventType = "VIEW",
-    eventCategory = "TRAFFIC",
-    storeId = "#{#storeId}",
-    targetType = "STORE"
-)
-@GetMapping("/store/detail")
-public R<StoreInfo> getStoreDetail(@RequestParam Integer storeId) {
-    // 业务逻辑
-}
-```
-
-### 2. 前端上报埋点
-
-```javascript
-// 上报浏览事件
-axios.post('/track/event', {
-  eventType: 'VIEW',
-  eventCategory: 'TRAFFIC',
-  storeId: 1001,
-  targetType: 'STORE',
-  duration: 3000
-});
-```
-
-### 3. 查询经营数据
-
-```javascript
-// 查询经营数据
-axios.get('/business/data', {
-  params: {
-    storeId: 1001,
-    startDate: '2026-01-08',
-    endDate: '2026-01-14'
-  }
-});
-
-// 对比数据
-axios.get('/business/data/compare', {
-  params: {
-    storeId: 1001,
-    startDate1: '2026-01-08',
-    endDate1: '2026-01-14',
-    startDate2: '2026-01-01',
-    endDate2: '2026-01-07'
-  }
-});
-```
-
-### 4. 获取AI推荐
-
-```javascript
-// 获取AI推荐
-axios.get('/business/ai/recommendation', {
-  params: {
-    storeId: 1001
-  }
-});
-```
-
-## 📊 技术架构
-
-```
-前端上报埋点
-    ↓
-TrackEventController (POST /track/event)
-    ↓
-TrackEventService.saveTrackEvent()
-    ↓
-Redis List (异步队列)
-    ↓
-TrackEventConsumer (定时任务,每10秒)
-    ↓
-TrackEventService.batchSaveTrackEvents()
-    ↓
-MySQL (store_track_event 表)
-    ↓
-统计查询 (BusinessDataController)
-    ↓
-返回前端
-```
-
-## 🔧 配置说明
-
-### 1. 定时任务已启用
-应用已配置 `@EnableScheduling`,定时任务会自动执行。
-
-### 2. Redis配置
-确保Redis连接正常,Redis List Key为:`track:event:queue`
-
-### 3. 消费频率
-当前配置为每10秒消费一次,可在 `TrackEventConsumer.java` 中修改 `@Scheduled` 注解。
-
-## ⚠️ 注意事项
-
-1. **数据量控制**:Redis List需要设置最大长度,防止内存溢出
-2. **异常处理**:消费服务已实现异常处理,失败不影响主流程
-3. **性能优化**:统计数据建议使用定时任务预计算,存入 `store_track_statistics` 表
-4. **数据清理**:定期清理历史埋点数据,避免数据表过大
-5. **AI推荐缓存**:AI推荐结果建议缓存,避免频繁调用AI服务
-
-## 📝 后续优化建议
-
-1. **统计数据预计算**:使用定时任务(如每天凌晨)计算前一天的统计数据
-2. **缓存优化**:对常用查询结果进行缓存
-3. **监控告警**:监控Redis List长度,设置告警阈值
-4. **数据分表**:如果数据量很大,可以考虑按时间分表
-
-## 📚 相关文档
-
-- `埋点需求完整方案.md` - 完整方案文档
-- `埋点实现代码清单.md` - 代码清单和关键代码示例
-
----
-
-**实现完成时间**:2026-01-14
-**下一步**:执行建表SQL,完善Service中的TODO,测试联调

+ 13 - 9
alien-store/doc/埋点接口清单.md

@@ -18,38 +18,42 @@
 | 5 | `/storeClockIn/addStoreClockIn` | POST | `addStoreClockIn` | `StoreClockInController` | CHECKIN |
 | 6 | `/renovation/requirement/consultRequirement` | POST | `consultRequirement` | `StoreRenovationRequirementController` | CONSULT |
 | 7 | `/userDynamics/addOrUpdate` | POST | `addOrUpdate` | `LifeUserDynamicsController` | POST_PUBLISH |
+| 8 | `/comment/like` | POST | `like` | `LifeCommentController` | POST_LIKE |
+| 9 | `/commonComment/addComment` | POST | `addComment` | `CommonCommentController` | POST_COMMENT |
+| 10 | `/user-violation/reporting` | POST | `reporting` | `UserViolationController` | REPORT |
+| 11 | `/life-blacklist/blackList` | POST | `blackList` | `LifeBlacklistController` | BLOCK |
 
 ## 三、服务质量(SERVICE)
 
 | 序号 | 接口路径 | HTTP方法 | 方法名 | 所在类 | 事件类型 |
 |------|---------|---------|--------|--------|---------|
-| 8 | `/commonRating/addRating` | POST | `add` | `CommonRatingController` | RATING_ADD |
-| 9 | `/commentAppeal/submit` | POST | `submitAppeal` | `CommentAppealController` | APPEAL |
+| 12 | `/commonRating/addRating` | POST | `add` | `CommonRatingController` | RATING_ADD |
+| 13 | `/commentAppeal/submit` | POST | `submitAppeal` | `CommentAppealController` | APPEAL |
 
 ## 四、价目表(PRICE)
 
 | 序号 | 接口路径 | HTTP方法 | 方法名 | 所在类 | 事件类型 |
 |------|---------|---------|--------|--------|---------|
-| 10 | `/cuisine/getPage` | GET | `getPage` | `StoreCuisineController` | PRICE_VIEW |
-| 11 | `/price/getPage` | GET | `getPage` | `StorePriceController` | PRICE_VIEW |
+| 14 | `/cuisine/getPage` | GET | `getPage` | `StoreCuisineController` | PRICE_VIEW |
+| 15 | `/price/getPage` | GET | `getPage` | `StorePriceController` | PRICE_VIEW |
 
 ## 五、优惠券(COUPON)
 
 | 序号 | 接口路径 | HTTP方法 | 方法名 | 所在类 | 事件类型 |
 |------|---------|---------|--------|--------|---------|
-| 12 | `/life-discount-coupon-store-friend/setFriendCoupon` | POST | `setFriendCoupon` | `LifeDiscountCouponStoreFriendController` | COUPON_GIVE |
-| 13 | `/coupon/verify` | GET | `verify` | `LifeCouponController` | COUPON_USE |
+| 16 | `/life-discount-coupon-store-friend/setFriendCoupon` | POST | `setFriendCoupon` | `LifeDiscountCouponStoreFriendController` | COUPON_GIVE |
+| 17 | `/coupon/verify` | GET | `verify` | `LifeCouponController` | COUPON_USE |
 
 ## 统计汇总
 
-- **总接口数**:13
+- **总接口数**:17
 - **流量数据**:3个
-- **互动数据**:4
+- **互动数据**:8
 - **服务质量**:2个
 - **价目表**:2个
 - **优惠券**:2个
 
 ---
 
-**文档版本**:v1.0  
+**文档版本**:v1.1  
 **最后更新**:2026-01-14

+ 0 - 454
alien-store/doc/埋点测试指南.md

@@ -1,454 +0,0 @@
-# 埋点系统测试指南
-
-## 📋 测试前准备
-
-### 1. 执行数据库建表SQL(必需)
-
-**⚠️ 重要:在执行任何测试前,必须先创建数据库表!**
-
-执行以下SQL创建表结构:
-
-```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='埋点事件表';
-
--- 埋点统计表
-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='埋点统计表';
-```
-
-### 2. 确认服务配置
-
-- ✅ Redis连接正常
-- ✅ MySQL连接正常
-- ✅ 定时任务已启用(应用已配置 `@EnableScheduling`)
-
----
-
-## 🧪 测试步骤
-
-### 测试1:前端埋点上报测试
-
-#### 1.1 使用Postman/curl测试上报接口
-
-**接口地址**: `POST http://localhost:8080/track/event`
-
-**请求头**:
-```http
-Content-Type: application/json
-Authorization: Bearer {your_token}  # 如果需要认证
-```
-
-**请求体**:
-```json
-{
-  "eventType": "VIEW",
-  "eventCategory": "TRAFFIC",
-  "storeId": 1001,
-  "targetType": "STORE",
-  "duration": 3000
-}
-```
-
-**预期结果**:
-```json
-{
-  "code": 200,
-  "success": true,
-  "msg": "上报成功",
-  "data": null
-}
-```
-
-**验证步骤**:
-1. 调用接口后,检查Redis List中是否有数据
-   ```bash
-   # 连接到Redis,查看队列长度
-   LLEN track:event:queue
-   
-   # 查看队列中的数据
-   LRANGE track:event:queue 0 -1
-   ```
-
-2. 等待10秒(定时任务消费间隔),检查数据库
-   ```sql
-   -- 查询最新埋点数据
-   SELECT * FROM store_track_event 
-   ORDER BY created_time DESC 
-   LIMIT 10;
-   ```
-
-#### 1.2 测试不同类型的事件
-
-```json
-// 测试搜索事件
-{
-  "eventType": "SEARCH",
-  "eventCategory": "TRAFFIC",
-  "storeId": 1001,
-  "targetType": "STORE"
-}
-
-// 测试收藏事件
-{
-  "eventType": "COLLECT",
-  "eventCategory": "INTERACTION",
-  "storeId": 1001,
-  "targetType": "STORE"
-}
-
-// 测试分享事件
-{
-  "eventType": "SHARE",
-  "eventCategory": "INTERACTION",
-  "storeId": 1001,
-  "targetType": "STORE"
-}
-
-// 测试价目表浏览
-{
-  "eventType": "PRICE_VIEW",
-  "eventCategory": "PRICE",
-  "storeId": 1001,
-  "targetId": 2001,
-  "targetType": "PRICE"
-}
-```
-
----
-
-### 测试2:AOP自动埋点测试
-
-#### 2.1 在Controller方法上添加注解
-
-在需要埋点的Controller方法上添加 `@TrackEvent` 注解:
-
-```java
-@TrackEvent(
-    eventType = "VIEW",
-    eventCategory = "TRAFFIC",
-    storeId = "#{#storeId}",
-    targetType = "STORE"
-)
-@GetMapping("/store/detail")
-public R<StoreInfo> getStoreDetail(@RequestParam Integer storeId) {
-    // 业务逻辑
-    return R.data(storeInfo);
-}
-```
-
-#### 2.2 测试步骤
-
-1. 调用业务接口(如:`GET /store/detail?storeId=1001`)
-2. 接口正常返回
-3. 检查Redis List中是否自动生成了埋点数据
-4. 等待10秒,检查数据库是否写入
-
----
-
-### 测试3:数据统计查询测试
-
-#### 3.1 查询经营数据
-
-**接口地址**: `GET http://localhost:8080/business/data`
-
-**请求参数**:
-```
-storeId=1001
-startDate=2026-01-08
-endDate=2026-01-14
-category=TRAFFIC  # 可选:TRAFFIC, INTERACTION, COUPON, VOUCHER, SERVICE, PRICE
-```
-
-**完整URL示例**:
-```
-GET http://localhost:8080/business/data?storeId=1001&startDate=2026-01-08&endDate=2026-01-14
-```
-
-**预期响应**:
-```json
-{
-  "code": 200,
-  "success": true,
-  "msg": "查询成功",
-  "data": {
-    "trafficData": {
-      "searchCount": 10,
-      "viewCount": 50,
-      "visitorCount": 30,
-      "totalDuration": 150000,
-      "avgDuration": 3000
-    },
-    "interactionData": {
-      "collectCount": 5,
-      "shareCount": 3,
-      "checkinCount": 2,
-      "consultCount": 1
-    }
-  }
-}
-```
-
-#### 3.2 数据对比测试
-
-**接口地址**: `GET http://localhost:8080/business/data/compare`
-
-**请求参数**:
-```
-storeId=1001
-startDate1=2026-01-08
-endDate1=2026-01-14
-startDate2=2026-01-01
-endDate2=2026-01-07
-```
-
-**完整URL示例**:
-```
-GET http://localhost:8080/business/data/compare?storeId=1001&startDate1=2026-01-08&endDate1=2026-01-14&startDate2=2026-01-01&endDate2=2026-01-07
-```
-
-**预期响应**:
-```json
-{
-  "code": 200,
-  "success": true,
-  "msg": "查询成功",
-  "data": {
-    "period1": {
-      "trafficData": {
-        "viewCount": 100
-      }
-    },
-    "period2": {
-      "trafficData": {
-        "viewCount": 80
-      }
-    },
-    "compare": {
-      "traffic": {
-        "viewCountChange": 25.00
-      }
-    }
-  }
-}
-```
-
-#### 3.3 AI推荐测试
-
-**接口地址**: `GET http://localhost:8080/business/ai/recommendation`
-
-**请求参数**:
-```
-storeId=1001
-```
-
-**完整URL示例**:
-```
-GET http://localhost:8080/business/ai/recommendation?storeId=1001
-```
-
-**预期响应**:
-```json
-{
-  "code": 200,
-  "success": true,
-  "msg": "查询成功",
-  "data": {
-    "summary": "相较于其他同星级的店铺,您价目表中的锅包肉和烤羊腿价格远高于其他商家",
-    "recommendations": [
-      {
-        "type": "PRICING",
-        "title": "价格优化建议",
-        "content": "寻找原材料更便宜的菜场、菜量降低、菜名突出特色,如锡林郭勒盟羔羊烤羊腿"
-      }
-    ]
-  }
-}
-```
-
----
-
-### 测试4:定时消费任务测试
-
-#### 4.1 验证消费任务是否正常运行
-
-1. **查看日志**
-   - 每10秒应该看到日志:`成功消费X条埋点数据`
-   - 如果Redis List为空,不会有日志输出(这是正常的)
-
-2. **手动触发测试**
-   - 先上报多条埋点数据(10-20条)
-   - 检查Redis List长度:`LLEN track:event:queue`
-   - 等待10秒,再次检查:`LLEN track:event:queue`(应该为0或减少)
-   - 查询数据库确认数据已写入
-
-#### 4.2 分布式锁测试
-
-如果有多个服务实例,验证分布式锁是否生效:
-- 只有一个实例能消费数据
-- 不会出现重复消费
-
----
-
-## 📊 完整测试流程示例
-
-### 流程1:完整的埋点到统计流程
-
-```bash
-# 步骤1: 上报多条埋点数据
-curl -X POST http://localhost:8080/track/event \
-  -H "Content-Type: application/json" \
-  -d '{"eventType":"VIEW","eventCategory":"TRAFFIC","storeId":1001,"targetType":"STORE","duration":3000}'
-
-curl -X POST http://localhost:8080/track/event \
-  -H "Content-Type: application/json" \
-  -d '{"eventType":"SEARCH","eventCategory":"TRAFFIC","storeId":1001,"targetType":"STORE"}'
-
-curl -X POST http://localhost:8080/track/event \
-  -H "Content-Type: application/json" \
-  -d '{"eventType":"COLLECT","eventCategory":"INTERACTION","storeId":1001,"targetType":"STORE"}'
-
-# 步骤2: 检查Redis List
-# 连接到Redis执行: LLEN track:event:queue
-
-# 步骤3: 等待10秒,定时任务会自动消费
-
-# 步骤4: 查询数据库验证数据已写入
-# SELECT COUNT(*) FROM store_track_event WHERE store_id = 1001;
-
-# 步骤5: 查询统计数据
-curl "http://localhost:8080/business/data?storeId=1001&startDate=2026-01-08&endDate=2026-01-14"
-```
-
----
-
-## 🔍 测试检查清单
-
-### ✅ 功能测试
-
-- [ ] 前端埋点上报接口正常
-- [ ] AOP自动埋点正常工作
-- [ ] Redis List正常写入
-- [ ] 定时任务正常消费
-- [ ] 数据库正常写入
-- [ ] 统计数据查询正常
-- [ ] 数据对比功能正常
-- [ ] AI推荐接口正常(如已实现)
-
-### ✅ 性能测试
-
-- [ ] 批量上报100条数据,验证性能
-- [ ] 定时任务消费速度是否正常
-- [ ] 统计查询响应时间是否可接受
-
-### ✅ 异常测试
-
-- [ ] Redis不可用时,系统是否降级
-- [ ] 数据库不可用时,是否有异常处理
-- [ ] 异常数据是否会被正确处理
-
----
-
-## 🐛 常见问题排查
-
-### 问题1:埋点数据没有写入数据库
-
-**检查项**:
-1. 检查Redis List是否有数据:`LLEN track:event:queue`
-2. 查看定时任务日志,是否有错误
-3. 检查数据库连接是否正常
-4. 检查表结构是否正确创建
-
-### 问题2:定时任务没有执行
-
-**检查项**:
-1. 确认应用已启用 `@EnableScheduling`
-2. 检查 `TrackEventConsumer` 类是否被Spring管理(`@Component`)
-3. 查看应用启动日志,确认定时任务配置加载
-
-### 问题3:统计数据为空
-
-**检查项**:
-1. 确认有埋点数据写入数据库
-2. 确认查询的时间范围正确
-3. 检查 `getTrafficData()` 等方法是否有TODO未实现
-
-### 问题4:AOP切面不生效
-
-**检查项**:
-1. 确认 `@TrackEvent` 注解在正确的方法上
-2. 确认 `TrackEventAspect` 类被Spring管理
-3. 检查切面执行顺序(`@Order`)
-
----
-
-## 📝 测试报告模板
-
-```markdown
-### 测试日期: 2026-01-14
-### 测试人员: [姓名]
-
-#### 测试结果
-- ✅ 前端埋点上报: 通过
-- ✅ AOP自动埋点: 通过
-- ✅ Redis队列: 正常
-- ✅ 定时消费: 正常
-- ✅ 统计查询: 通过
-- ⚠️ AI推荐: 待完善
-
-#### 发现问题
-1. [问题描述]
-
-#### 建议
-1. [建议内容]
-```
-
----
-
-## 🚀 下一步
-
-1. **完善统计逻辑**:实现Service中的TODO(优惠券、代金券、服务质量统计)
-2. **优化性能**:对统计数据添加缓存
-3. **监控告警**:添加Redis List长度监控
-4. **数据清理**:实现历史数据清理策略

+ 22 - 0
alien-store/doc/埋点统计数据JSON格式说明.md

@@ -4,6 +4,28 @@
 
 `store_track_statistics` 表中的各个数据字段存储的是JSON格式的统计数据。本文档详细说明各个字段的JSON格式。
 
+````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 '统计类型(DAILY-日统计,WEEKLY-周统计,MONTHLY-月统计)',
+  `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='埋点统计表';
+```
+
 ## 二、各字段JSON格式
 
 ### 2.1 traffic_data(流量数据)

+ 282 - 8
alien-store/src/main/java/shop/alien/store/aspect/TrackEventAspect.java

@@ -1,7 +1,8 @@
 package shop.alien.store.aspect;
 
-import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.aspectj.lang.ProceedingJoinPoint;
@@ -18,7 +19,8 @@ import org.springframework.stereotype.Component;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.web.context.request.RequestContextHolder;
 import org.springframework.web.context.request.ServletRequestAttributes;
-import shop.alien.entity.store.StoreTrackEvent;
+import shop.alien.entity.store.*;
+import shop.alien.mapper.*;
 import shop.alien.store.annotation.TrackEvent;
 import shop.alien.store.config.BaseRedisService;
 import shop.alien.util.common.JwtUtil;
@@ -42,6 +44,10 @@ import java.util.Date;
 public class TrackEventAspect {
 
     private final BaseRedisService baseRedisService;
+    private final LifeUserDynamicsMapper lifeUserDynamicsMapper;
+    private final CommonRatingMapper commonRatingMapper;
+    private final StoreUserMapper storeUserMapper;
+    private final ObjectMapper objectMapper;
     
     private final SpelExpressionParser spelExpressionParser = new SpelExpressionParser();
     private final DefaultParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
@@ -80,10 +86,8 @@ public class TrackEventAspect {
             
             // 异步写入Redis List
             if (annotation.async()) {
-                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) {
             // 埋点失败不应该影响主流程
@@ -114,6 +118,20 @@ public class TrackEventAspect {
             // 尝试从方法参数中查找storeId
             storeId = extractStoreIdFromArgs(joinPoint);
         }
+        // 如果storeId仍为空,根据接口类型和参数查询店铺ID
+        if (storeId == null) {
+            storeId = queryStoreIdByBusinessLogic(joinPoint, annotation, context);
+        }
+        
+        // 如果storeId仍为空,记录警告日志
+        if (storeId == null) {
+            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
+            String methodName = signature.getMethod().getName();
+            String className = joinPoint.getTarget().getClass().getSimpleName();
+            log.warn("无法获取storeId: className={}, methodName={}, storeId表达式={}", 
+                    className, methodName, annotation.storeId());
+        }
+        
         trackEvent.setStoreId(storeId);
 
         // 解析SpEL表达式获取userId
@@ -185,8 +203,14 @@ public class TrackEventAspect {
                 return null;
             }
 
+            // 处理 #{#xxx} 格式,转换为 #xxx
+            String normalizedExpression = expression;
+            if (expression.startsWith("#{#") && expression.endsWith("}")) {
+                normalizedExpression = expression.substring(2, expression.length() - 1);
+            }
+            
             // 解析SpEL表达式
-            Object value = spelExpressionParser.parseExpression(expression).getValue(context);
+            Object value = spelExpressionParser.parseExpression(normalizedExpression).getValue(context);
             if (value == null) {
                 return null;
             }
@@ -196,8 +220,17 @@ public class TrackEventAspect {
             }
             
             // 类型转换
-            if (clazz == Integer.class && value instanceof Number) {
-                return clazz.cast(((Number) value).intValue());
+            if (clazz == Integer.class) {
+                if (value instanceof Number) {
+                    return clazz.cast(((Number) value).intValue());
+                } else if (value instanceof String) {
+                    try {
+                        return clazz.cast(Integer.parseInt((String) value));
+                    } catch (NumberFormatException e) {
+                        log.debug("无法将字符串转换为Integer: {}", value);
+                        return null;
+                    }
+                }
             }
             
             return null;
@@ -255,6 +288,247 @@ public class TrackEventAspect {
     }
 
     /**
+     * 根据业务逻辑查询店铺ID
+     */
+    private Integer queryStoreIdByBusinessLogic(ProceedingJoinPoint joinPoint, TrackEvent annotation, EvaluationContext context) {
+        try {
+            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
+            String methodName = signature.getMethod().getName();
+            String className = joinPoint.getTarget().getClass().getSimpleName();
+            
+            // 处理 /comment/like 接口(动态点赞)
+            if ("LifeCommentController".equals(className) && "like".equals(methodName)) {
+                return queryStoreIdForLike(context);
+            }
+            
+            // 处理 /commonComment/addComment 接口(动态回复)
+            if ("CommonCommentController".equals(className) && "addComment".equals(methodName)) {
+                return queryStoreIdForAddComment(context);
+            }
+            
+            // 处理 /user-violation/reporting 接口(举报)
+            if ("UserViolationController".equals(className) && "reporting".equals(methodName)) {
+                return queryStoreIdForReporting(context);
+            }
+            
+            // 处理 /life-blacklist/blackList 接口(拉黑)
+            if ("LifeBlacklistController".equals(className) && "blackList".equals(methodName)) {
+                return queryStoreIdForBlackList(context);
+            }
+            
+            // 处理 /userDynamics/addOrUpdate 接口(发布动态)
+            if ("LifeUserDynamicsController".equals(className) && "addOrUpdate".equals(methodName)) {
+                return queryStoreIdForAddOrUpdate(context);
+            }
+        } catch (Exception e) {
+            log.debug("根据业务逻辑查询店铺ID失败", e);
+        }
+        return null;
+    }
+    
+    /**
+     * 为点赞接口查询店铺ID
+     */
+    private Integer queryStoreIdForLike(EvaluationContext context) {
+        try {
+            // 获取type和huifuId参数
+            Object typeObj = context.lookupVariable("type");
+            Object huifuIdObj = context.lookupVariable("huifuId");
+            
+            if (typeObj == null || huifuIdObj == null) {
+                return null;
+            }
+            
+            String type = String.valueOf(typeObj);
+            String huifuId = String.valueOf(huifuIdObj);
+            
+            // type=2 表示社区动态
+            if ("2".equals(type)) {
+                LifeUserDynamics dynamics = lifeUserDynamicsMapper.selectById(Integer.parseInt(huifuId));
+                if (dynamics != null && dynamics.getPhoneId() != null) {
+                    // phoneId格式:store_手机号 或 user_手机号
+                    if (dynamics.getPhoneId().startsWith("store_")) {
+                        String phone = dynamics.getPhoneId().substring(6);
+                        LambdaQueryWrapper<StoreUser> wrapper = new LambdaQueryWrapper<>();
+                        wrapper.eq(StoreUser::getPhone, phone)
+                                .eq(StoreUser::getDeleteFlag, 0);
+                        StoreUser storeUser = storeUserMapper.selectOne(wrapper);
+                        if (storeUser != null && storeUser.getStoreId() != null) {
+                            return storeUser.getStoreId();
+                        }
+                    }
+                }
+            }
+            // type=1 表示评论,需要查询评论表获取店铺ID
+            else if ("1".equals(type)) {
+                // 这里可以根据评论ID查询评论表,但需要知道评论表的结构
+                // 暂时返回null,后续可以根据实际表结构补充
+            }
+        } catch (Exception e) {
+            log.debug("查询点赞接口的店铺ID失败", e);
+        }
+        return null;
+    }
+    
+    /**
+     * 为添加评论接口查询店铺ID
+     */
+    private Integer queryStoreIdForAddComment(EvaluationContext context) {
+        try {
+            // 获取commonComment参数
+            Object commonCommentObj = context.lookupVariable("commonComment");
+            if (commonCommentObj == null || !(commonCommentObj instanceof CommonComment)) {
+                return null;
+            }
+            
+            CommonComment commonComment = (CommonComment) commonCommentObj;
+            
+            // sourceType=1 表示评价的评论
+            if (commonComment.getSourceType() != null && commonComment.getSourceType() == 1) {
+                // sourceId是rating.id,通过CommonRating查询businessId(店铺ID)
+                if (commonComment.getSourceId() != null) {
+                    CommonRating rating = commonRatingMapper.selectById(commonComment.getSourceId());
+                    if (rating != null && rating.getBusinessId() != null) {
+                        return rating.getBusinessId();
+                    }
+                }
+            }
+            // sourceType=2 表示社区动态
+            else if (commonComment.getSourceType() != null && commonComment.getSourceType() == 2) {
+                // sourceId是动态ID,通过LifeUserDynamics查询店铺ID
+                if (commonComment.getSourceId() != null) {
+                    LifeUserDynamics dynamics = lifeUserDynamicsMapper.selectById(commonComment.getSourceId().intValue());
+                    if (dynamics != null && dynamics.getPhoneId() != null) {
+                        // phoneId格式:store_手机号 或 user_手机号
+                        if (dynamics.getPhoneId().startsWith("store_")) {
+                            String phone = dynamics.getPhoneId().substring(6);
+                            LambdaQueryWrapper<StoreUser> wrapper = new LambdaQueryWrapper<>();
+                            wrapper.eq(StoreUser::getPhone, phone)
+                                    .eq(StoreUser::getDeleteFlag, 0);
+                            StoreUser storeUser = storeUserMapper.selectOne(wrapper);
+                            if (storeUser != null && storeUser.getStoreId() != null) {
+                                return storeUser.getStoreId();
+                            }
+                        }
+                    }
+                }
+            }
+        } catch (Exception e) {
+            log.debug("查询添加评论接口的店铺ID失败", e);
+        }
+        return null;
+    }
+
+    /**
+     * 为举报接口查询店铺ID
+     */
+    private Integer queryStoreIdForReporting(EvaluationContext context) {
+        try {
+            // 获取lifeUserViolation参数
+            Object violationObj = context.lookupVariable("lifeUserViolation");
+            if (violationObj == null || !(violationObj instanceof LifeUserViolation)) {
+                return null;
+            }
+            
+            LifeUserViolation violation = (LifeUserViolation) violationObj;
+            
+            // 根据reportContextType查询店铺ID
+            String reportContextType = violation.getReportContextType();
+            
+            // reportContextType=2 表示动态
+            if ("2".equals(reportContextType) && violation.getDynamicsId() != null) {
+                LifeUserDynamics dynamics = lifeUserDynamicsMapper.selectById(Integer.parseInt(violation.getDynamicsId()));
+                if (dynamics != null && dynamics.getPhoneId() != null) {
+                    if (dynamics.getPhoneId().startsWith("store_")) {
+                        String phone = dynamics.getPhoneId().substring(6);
+                        LambdaQueryWrapper<StoreUser> wrapper = new LambdaQueryWrapper<>();
+                        wrapper.eq(StoreUser::getPhone, phone)
+                                .eq(StoreUser::getDeleteFlag, 0);
+                        StoreUser storeUser = storeUserMapper.selectOne(wrapper);
+                        if (storeUser != null && storeUser.getStoreId() != null) {
+                            return storeUser.getStoreId();
+                        }
+                    }
+                }
+            }
+            // reportContextType=3 表示评论
+            else if ("3".equals(reportContextType) && violation.getCommentId() != null) {
+                // 可以通过评论ID查询评论表,然后获取店铺ID
+                // 暂时返回null,后续可以根据实际表结构补充
+            }
+            // reportContextType=0 表示商户
+            else if ("0".equals(reportContextType) && violation.getBusinessId() != null) {
+                // businessId就是店铺ID
+                return violation.getBusinessId();
+            }
+        } catch (Exception e) {
+            log.debug("查询举报接口的店铺ID失败", e);
+        }
+        return null;
+    }
+    
+    /**
+     * 为发布动态接口查询店铺ID
+     */
+    private Integer queryStoreIdForAddOrUpdate(EvaluationContext context) {
+        try {
+            // 获取lifeUserDynamics参数
+            Object dynamicsObj = context.lookupVariable("lifeUserDynamics");
+            if (dynamicsObj == null || !(dynamicsObj instanceof LifeUserDynamics)) {
+                return null;
+            }
+            
+            LifeUserDynamics dynamics = (LifeUserDynamics) dynamicsObj;
+            
+            // 如果phoneId以store_开头,通过phoneId查询店铺ID
+            if (dynamics.getPhoneId() != null && dynamics.getPhoneId().startsWith("store_")) {
+                String phone = dynamics.getPhoneId().substring(6);
+                LambdaQueryWrapper<StoreUser> wrapper = new LambdaQueryWrapper<>();
+                wrapper.eq(StoreUser::getPhone, phone)
+                        .eq(StoreUser::getDeleteFlag, 0);
+                StoreUser storeUser = storeUserMapper.selectOne(wrapper);
+                if (storeUser != null && storeUser.getStoreId() != null) {
+                    return storeUser.getStoreId();
+                }
+            }
+        } catch (Exception e) {
+            log.debug("查询发布动态接口的店铺ID失败", e);
+        }
+        return null;
+    }
+    
+    /**
+     * 为拉黑接口查询店铺ID
+     */
+    private Integer queryStoreIdForBlackList(EvaluationContext context) {
+        try {
+            // 获取lifeBlacklist参数
+            Object blacklistObj = context.lookupVariable("lifeBlacklist");
+            if (blacklistObj == null || !(blacklistObj instanceof LifeBlacklist)) {
+                return null;
+            }
+            
+            LifeBlacklist blacklist = (LifeBlacklist) blacklistObj;
+            
+            // 如果拉黑方是商户(blockerType=1),通过blockerId查询店铺ID
+            if ("1".equals(blacklist.getBlockerType()) && blacklist.getBlockerId() != null) {
+                try {
+                    Integer blockerId = Integer.parseInt(blacklist.getBlockerId());
+                    StoreUser storeUser = storeUserMapper.selectById(blockerId);
+                    if (storeUser != null && storeUser.getStoreId() != null) {
+                        return storeUser.getStoreId();
+                    }
+                } catch (NumberFormatException e) {
+                    log.debug("blockerId无法转换为Integer: {}", blacklist.getBlockerId());
+                }
+            }
+        } catch (Exception e) {
+            log.debug("查询拉黑接口的店铺ID失败", e);
+        }
+        return null;
+    }
+
+    /**
      * 获取客户端IP地址
      */
     private String getIpAddress(HttpServletRequest request) {

+ 10 - 1
alien-store/src/main/java/shop/alien/store/controller/CommonCommentController.java

@@ -1,4 +1,4 @@
-  package shop.alien.store.controller;
+package shop.alien.store.controller;
 
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
@@ -8,6 +8,7 @@ import lombok.extern.slf4j.Slf4j;
 import org.springframework.web.bind.annotation.*;
 import shop.alien.entity.result.R;
 import shop.alien.entity.store.CommonComment;
+import shop.alien.store.annotation.TrackEvent;
 import shop.alien.store.service.CommonCommentService;
 
 /**
@@ -64,6 +65,14 @@ public class CommonCommentController {
      * @param commonComment 评论对象
      * @return 0:成功, 1:失败, 2:文本内容异常, 4:字数超限(超过300字)
      */
+    @TrackEvent(
+            eventType = "POST_COMMENT",
+            eventCategory = "INTERACTION",
+            storeId = "",
+            userId = "#commonComment.userId",
+            targetId = "#commonComment.sourceId",
+            targetType = "POST"
+    )
     @ApiOperation(value = "新增评论", notes = "0:成功, 1:失败, 2:文本内容异常, 4:字数超限(超过300字)")
     @PostMapping("/addComment")
     public R addComment(@RequestBody CommonComment commonComment) {

+ 9 - 0
alien-store/src/main/java/shop/alien/store/controller/LifeBlacklistController.java

@@ -10,6 +10,7 @@ import org.springframework.web.bind.annotation.*;
 import shop.alien.entity.result.R;
 import shop.alien.entity.store.LifeBlacklist;
 import shop.alien.entity.store.vo.LifeBlacklistVo;
+import shop.alien.store.annotation.TrackEvent;
 import shop.alien.store.service.LifeBlacklistService;
 
 import java.util.List;
@@ -33,6 +34,14 @@ public class LifeBlacklistController {
 
     private final LifeBlacklistService lifeBlacklistService;
 
+    @TrackEvent(
+            eventType = "BLOCK",
+            eventCategory = "INTERACTION",
+            storeId = "",
+            userId = "#lifeBlacklist.blockerId",
+            targetId = "#lifeBlacklist.blockedId",
+            targetType = "STORE"
+    )
     @ApiOperation("拉黑")
     @ApiOperationSupport(order = 1)
     @PostMapping("/blackList")

+ 9 - 0
alien-store/src/main/java/shop/alien/store/controller/LifeCommentController.java

@@ -10,6 +10,7 @@ import org.springframework.web.multipart.MultipartFile;
 import shop.alien.entity.result.R;
 import shop.alien.entity.store.LifeComment;
 import shop.alien.entity.store.vo.LifePinglunVo;
+import shop.alien.store.annotation.TrackEvent;
 import shop.alien.store.service.LifeCommentService;
 import shop.alien.util.common.FileUpload;
 import shop.alien.util.common.ListToPage;
@@ -42,6 +43,14 @@ public class LifeCommentController {
      * @param type    点赞类型(1-评论 2-社区动态 3-活动 4-推荐菜 5-店铺打卡 6-二手商品 7-律师评分 8-点赞员工)
      * @return 点赞结果
      */
+    @TrackEvent(
+            eventType = "POST_LIKE",
+            eventCategory = "INTERACTION",
+            storeId = "",
+            userId = "#userId",
+            targetId = "#huifuId",
+            targetType = "POST"
+    )
     @ApiOperation("点赞")
     @ApiOperationSupport(order = 1)
     @ApiImplicitParams({

+ 2 - 0
alien-store/src/main/java/shop/alien/store/controller/LifeUserDynamicsController.java

@@ -77,8 +77,10 @@ public class LifeUserDynamicsController {
     @TrackEvent(
             eventType = "POST_PUBLISH",
             eventCategory = "INTERACTION",
+            storeId = "",
             targetType = "POST"
     )
+
     @ApiOperation(value = "发布动态社区", notes = "0:成功, 1:失败, 2:文本内容异常, 3:图片内容异常")
     @ApiOperationSupport(order = 2)
     @PostMapping("/addOrUpdate")

+ 1 - 1
alien-store/src/main/java/shop/alien/store/controller/StoreRenovationRequirementController.java

@@ -214,7 +214,7 @@ public class StoreRenovationRequirementController {
             eventType = "CONSULT",
             eventCategory = "INTERACTION",
             targetId = "#{#requirementId}",
-            targetType = "REQUIREMENT"
+            targetType = "STORE"
     )
     @ApiOperation("装修商铺咨询装修需求(记录浏览和咨询历史)")
     @ApiOperationSupport(order = 9)

+ 35 - 2
alien-store/src/main/java/shop/alien/store/controller/TrackEventController.java

@@ -34,8 +34,6 @@ public class TrackEventController {
     @ApiOperationSupport(order = 1)
     @PostMapping("/event")
     public R<String> reportEvent(@RequestBody StoreTrackEvent trackEvent, HttpServletRequest request) {
-        log.info("上报埋点事件: {}", JSONObject.toJSONString(trackEvent));
-
         try {
             // 设置默认值
             if (trackEvent.getEventTime() == null) {
@@ -79,6 +77,41 @@ public class TrackEventController {
         }
     }
 
+    @ApiOperation("手动触发统计数据计算(测试用)")
+    @ApiOperationSupport(order = 2)
+    @PostMapping("/statistics/calculate")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "storeId", value = "店铺ID", dataType = "Integer", paramType = "query", required = true),
+            @ApiImplicitParam(name = "statDate", value = "统计日期(格式:yyyy-MM-dd)", dataType = "String", paramType = "query", required = true),
+            @ApiImplicitParam(name = "statType", value = "统计类型(DAILY/WEEKLY/MONTHLY)", dataType = "String", paramType = "query", required = true)
+    })
+    public R<String> calculateStatistics(
+            @RequestParam("storeId") Integer storeId,
+            @RequestParam("statDate") String statDateStr,
+            @RequestParam("statType") String statType) {
+        try {
+            // 解析日期
+            java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd");
+            Date statDate = sdf.parse(statDateStr);
+            
+            // 验证统计类型
+            if (!"DAILY".equals(statType) && !"WEEKLY".equals(statType) && !"MONTHLY".equals(statType)) {
+                return R.fail("统计类型必须是 DAILY、WEEKLY 或 MONTHLY");
+            }
+            
+            // 调用统计方法
+            trackEventService.calculateAndSaveStatistics(storeId, statDate, statType);
+            
+            return R.success("统计数据计算成功");
+        } catch (java.text.ParseException e) {
+            log.error("日期格式错误: {}", statDateStr, e);
+            return R.fail("日期格式错误,请使用 yyyy-MM-dd 格式");
+        } catch (Exception e) {
+            log.error("计算统计数据失败", e);
+            return R.fail("计算统计数据失败: " + e.getMessage());
+        }
+    }
+
     /**
      * 获取客户端IP地址
      */

+ 9 - 0
alien-store/src/main/java/shop/alien/store/controller/UserViolationController.java

@@ -14,6 +14,7 @@ import shop.alien.entity.store.LifeUserViolation;
 import shop.alien.entity.store.UserLoginInfo;
 import shop.alien.entity.store.dto.LifeUserViolationDto;
 import shop.alien.mapper.LifeNoticeMapper;
+import shop.alien.store.annotation.TrackEvent;
 import shop.alien.store.service.LifeUserViolationService;
 import shop.alien.util.common.JwtUtil;
 import shop.alien.util.common.TokenInfo;
@@ -42,6 +43,14 @@ public class UserViolationController {
 
     private final LifeNoticeMapper lifeNoticeMapper;
 
+    @TrackEvent(
+            eventType = "REPORT",
+            eventCategory = "INTERACTION",
+            storeId = "",
+            userId = "#lifeUserViolation.reportingUserId",
+            targetId = "#lifeUserViolation.dynamicsId != null ? #lifeUserViolation.dynamicsId : #lifeUserViolation.commentId",
+            targetType = "STORE"
+    )
     @ApiOperation("举报")
     @ApiOperationSupport(order = 1)
     @PostMapping("/reporting")

+ 3 - 2
alien-store/src/main/java/shop/alien/store/service/TrackEventConsumer.java

@@ -1,6 +1,6 @@
 package shop.alien.store.service;
 
-import com.alibaba.fastjson.JSON;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.scheduling.annotation.Scheduled;
@@ -25,6 +25,7 @@ public class TrackEventConsumer {
 
     private final BaseRedisService baseRedisService;
     private final TrackEventService trackEventService;
+    private final ObjectMapper objectMapper;
     
     private static final String REDIS_QUEUE_KEY = "track:event:queue";
     private static final String CONSUMER_LOCK_KEY = "track:event:consumer:lock";
@@ -56,7 +57,7 @@ public class TrackEventConsumer {
             List<StoreTrackEvent> events = eventList.stream()
                     .map(json -> {
                         try {
-                            return JSON.parseObject(json, StoreTrackEvent.class);
+                            return objectMapper.readValue(json, StoreTrackEvent.class);
                         } catch (Exception e) {
                             log.error("解析埋点数据JSON失败: {}", json, e);
                             return null;

+ 10 - 0
alien-store/src/main/java/shop/alien/store/service/TrackEventService.java

@@ -2,6 +2,7 @@ package shop.alien.store.service;
 
 import shop.alien.entity.store.StoreTrackEvent;
 
+import java.util.Date;
 import java.util.List;
 
 /**
@@ -25,4 +26,13 @@ public interface TrackEventService {
      * @param trackEvents 埋点事件列表
      */
     void batchSaveTrackEvents(List<StoreTrackEvent> trackEvents);
+
+    /**
+     * 计算并保存统计数据
+     *
+     * @param id              店铺ID
+     * @param lastWeekMonday  上周一的日期
+     * @param weekly          统计类型(DAILY/WEEKLY/MONTHLY)
+     */
+    void calculateAndSaveStatistics(Integer id, Date lastWeekMonday, String weekly);
 }

+ 171 - 0
alien-store/src/main/java/shop/alien/store/service/TrackStatisticsScheduler.java

@@ -0,0 +1,171 @@
+package shop.alien.store.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+import shop.alien.mapper.StoreInfoMapper;
+import shop.alien.entity.store.StoreInfo;
+
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 统计数据定时任务
+ * 定时计算并保存统计数据到store_track_statistics表
+ *
+ * @author system
+ * @since 2026-01-14
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class TrackStatisticsScheduler {
+    
+    private final TrackEventService trackEventService;
+    private final StoreInfoMapper storeInfoMapper;
+
+    /**
+     * 每天凌晨1点执行,计算前一天的统计数据
+     * cron表达式: 秒 分 时 日 月 周
+     */
+    @Scheduled(cron = "0 0 1 * * ?")
+    public void calculateDailyStatistics() {
+        log.info("开始执行日统计数据计算任务");
+        
+        try {
+            // 计算前一天的日期
+            Calendar calendar = Calendar.getInstance();
+            calendar.add(Calendar.DAY_OF_MONTH, -1);
+            calendar.set(Calendar.HOUR_OF_DAY, 0);
+            calendar.set(Calendar.MINUTE, 0);
+            calendar.set(Calendar.SECOND, 0);
+            calendar.set(Calendar.MILLISECOND, 0);
+            Date yesterday = calendar.getTime();
+            
+            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+            log.info("计算日期: {}", sdf.format(yesterday));
+            
+            // 查询所有店铺(只查询未删除的)
+            LambdaQueryWrapper<StoreInfo> wrapper = new LambdaQueryWrapper<>();
+            wrapper.eq(StoreInfo::getDeleteFlag, 0);
+            List<StoreInfo> stores = storeInfoMapper.selectList(wrapper);
+            log.info("共{}个店铺需要计算统计数据", stores.size());
+            
+            int successCount = 0;
+            int failCount = 0;
+            
+            for (StoreInfo store : stores) {
+                try {
+                    trackEventService.calculateAndSaveStatistics(store.getId(), yesterday, "DAILY");
+                    successCount++;
+                    log.debug("店铺{}的日统计数据计算成功", store.getId());
+                } catch (Exception e) {
+                    failCount++;
+                    log.error("店铺{}的日统计数据计算失败", store.getId(), e);
+                }
+            }
+            
+            log.info("日统计数据计算任务完成: 成功{}, 失败{}", successCount, failCount);
+        } catch (Exception e) {
+            log.error("执行日统计数据计算任务失败", e);
+        }
+    }
+
+    /**
+     * 每周一凌晨2点执行,计算上一周的统计数据
+     */
+    @Scheduled(cron = "0 0 2 ? * MON")
+    public void calculateWeeklyStatistics() {
+        log.info("开始执行周统计数据计算任务");
+        
+        try {
+            // 计算上一周的日期(上周一)
+            Calendar calendar = Calendar.getInstance();
+            calendar.add(Calendar.WEEK_OF_YEAR, -1);
+            calendar.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY);
+            calendar.set(Calendar.HOUR_OF_DAY, 0);
+            calendar.set(Calendar.MINUTE, 0);
+            calendar.set(Calendar.SECOND, 0);
+            calendar.set(Calendar.MILLISECOND, 0);
+            Date lastWeekMonday = calendar.getTime();
+            
+            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+            log.info("计算日期: {}", sdf.format(lastWeekMonday));
+            
+            // 查询所有店铺(只查询未删除的)
+            LambdaQueryWrapper<StoreInfo> wrapper = new LambdaQueryWrapper<>();
+            wrapper.eq(StoreInfo::getDeleteFlag, 0);
+            List<StoreInfo> stores = storeInfoMapper.selectList(wrapper);
+            log.info("共{}个店铺需要计算统计数据", stores.size());
+            
+            int successCount = 0;
+            int failCount = 0;
+            
+            for (StoreInfo store : stores) {
+                try {
+                    trackEventService.calculateAndSaveStatistics(store.getId(), lastWeekMonday, "WEEKLY");
+                    successCount++;
+                    log.debug("店铺{}的周统计数据计算成功", store.getId());
+                } catch (Exception e) {
+                    failCount++;
+                    log.error("店铺{}的周统计数据计算失败", store.getId(), e);
+                }
+            }
+            
+            log.info("周统计数据计算任务完成: 成功{}, 失败{}", successCount, failCount);
+        } catch (Exception e) {
+            log.error("执行周统计数据计算任务失败", e);
+        }
+    }
+
+    /**
+     * 每月1号凌晨3点执行,计算上一个月的统计数据
+     */
+    @Scheduled(cron = "0 0 3 1 * ?")
+    public void calculateMonthlyStatistics() {
+        log.info("开始执行月统计数据计算任务");
+        
+        try {
+            // 计算上一个月的日期(上月1号)
+            Calendar calendar = Calendar.getInstance();
+            calendar.add(Calendar.MONTH, -1);
+            calendar.set(Calendar.DAY_OF_MONTH, 1);
+            calendar.set(Calendar.HOUR_OF_DAY, 0);
+            calendar.set(Calendar.MINUTE, 0);
+            calendar.set(Calendar.SECOND, 0);
+            calendar.set(Calendar.MILLISECOND, 0);
+            Date lastMonthFirstDay = calendar.getTime();
+            
+            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+            log.info("计算日期: {}", sdf.format(lastMonthFirstDay));
+            
+            // 查询所有店铺(只查询未删除的)
+            LambdaQueryWrapper<StoreInfo> wrapper = new LambdaQueryWrapper<>();
+            wrapper.eq(StoreInfo::getDeleteFlag, 0);
+            List<StoreInfo> stores = storeInfoMapper.selectList(wrapper);
+            log.info("共{}个店铺需要计算统计数据", stores.size());
+            
+            int successCount = 0;
+            int failCount = 0;
+            
+            for (StoreInfo store : stores) {
+                try {
+                    trackEventService.calculateAndSaveStatistics(store.getId(), lastMonthFirstDay, "MONTHLY");
+                    successCount++;
+                    log.debug("店铺{}的月统计数据计算成功", store.getId());
+                } catch (Exception e) {
+                    failCount++;
+                    log.error("店铺{}的月统计数据计算失败", store.getId(), e);
+                }
+            }
+            
+            log.info("月统计数据计算任务完成: 成功{}, 失败{}", successCount, failCount);
+        } catch (Exception e) {
+            log.error("执行月统计数据计算任务失败", e);
+        }
+    }
+}

+ 9 - 0
alien-store/src/main/java/shop/alien/store/service/impl/StoreInfoServiceImpl.java

@@ -1676,6 +1676,9 @@ public class StoreInfoServiceImpl extends ServiceImpl<StoreInfoMapper, StoreInfo
     public StoreInfoVo getStoreDetail(String storeId, String userId, String jingdu, String weidu) {
         StoreInfoVo result = new StoreInfoVo();
         StoreInfo storeInfo = storeInfoMapper.selectById(storeId);
+        if (storeInfo == null) {
+            return null;
+        }
         BeanUtils.copyProperties(storeInfo, result);
         //将经营板块和种类拆分成集合
         String businessTypes = storeInfo.getBusinessTypes();
@@ -5486,6 +5489,9 @@ public class StoreInfoServiceImpl extends ServiceImpl<StoreInfoMapper, StoreInfo
     public StoreInfoVo getClientStoreDetail(String storeId, String userId, String jingdu, String weidu) {
         StoreInfoVo result = new StoreInfoVo();
         StoreInfo storeInfo = storeInfoMapper.selectById(storeId);
+        if (storeInfo == null) {
+            return null;
+        }
         BeanUtils.copyProperties(storeInfo, result);
         //将经营板块和种类拆分成集合
         String businessTypes = storeInfo.getBusinessTypes();
@@ -6118,6 +6124,9 @@ public class StoreInfoServiceImpl extends ServiceImpl<StoreInfoMapper, StoreInfo
 
         StoreInfoVo result = new StoreInfoVo();
         StoreInfo storeInfo = storeInfoMapper.selectById(storeId);
+        if (storeInfo == null) {
+            return null;
+        }
         BeanUtils.copyProperties(storeInfo, result);
         //将经营板块和种类拆分成集合
         String businessTypes = storeInfo.getBusinessTypes();

+ 336 - 6
alien-store/src/main/java/shop/alien/store/service/impl/TrackEventServiceImpl.java

@@ -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();
+    }
 }