Forráskód Böngészése

fix:修改用户端埋点逻辑

penghao 3 hónapja
szülő
commit
57220bce51
23 módosított fájl, 625 hozzáadás és 965 törlés
  1. 0 297
      alien-store/doc/埋点实现代码清单.md
  2. 55 0
      alien-store/doc/埋点接口清单.md
  3. 280 0
      alien-store/doc/埋点统计数据JSON格式说明.md
  4. 4 1
      alien-store/src/main/java/shop/alien/store/aspect/TrackEventAspect.java
  5. 0 143
      alien-store/src/main/java/shop/alien/store/controller/AIRecoveryController.java
  6. 7 0
      alien-store/src/main/java/shop/alien/store/controller/AiSearchController.java
  7. 0 127
      alien-store/src/main/java/shop/alien/store/controller/BusinessDataController.java
  8. 7 0
      alien-store/src/main/java/shop/alien/store/controller/CommentAppealController.java
  9. 7 0
      alien-store/src/main/java/shop/alien/store/controller/CommonRatingController.java
  10. 7 0
      alien-store/src/main/java/shop/alien/store/controller/LifeCollectController.java
  11. 7 0
      alien-store/src/main/java/shop/alien/store/controller/LifeCouponController.java
  12. 7 0
      alien-store/src/main/java/shop/alien/store/controller/LifeDiscountCouponStoreFriendController.java
  13. 6 1
      alien-store/src/main/java/shop/alien/store/controller/LifeUserDynamicsController.java
  14. 7 0
      alien-store/src/main/java/shop/alien/store/controller/StoreClockInController.java
  15. 7 0
      alien-store/src/main/java/shop/alien/store/controller/StoreCuisineController.java
  16. 7 0
      alien-store/src/main/java/shop/alien/store/controller/StoreInfoController.java
  17. 7 0
      alien-store/src/main/java/shop/alien/store/controller/StorePriceController.java
  18. 7 0
      alien-store/src/main/java/shop/alien/store/controller/StoreRenovationRequirementController.java
  19. 5 0
      alien-store/src/main/java/shop/alien/store/controller/TrackEventController.java
  20. 7 0
      alien-store/src/main/java/shop/alien/store/controller/UserStoreController.java
  21. 0 45
      alien-store/src/main/java/shop/alien/store/service/TrackEventService.java
  22. 2 351
      alien-store/src/main/java/shop/alien/store/service/impl/TrackEventServiceImpl.java
  23. 189 0
      alien-store/src/main/java/shop/alien/store/util/UserAgentParserUtil.java

+ 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. 埋点数据量大,需要定期清理历史数据

+ 55 - 0
alien-store/doc/埋点接口清单.md

@@ -0,0 +1,55 @@
+# 埋点接口清单
+
+本文档列出了所有已添加埋点注解(`@TrackEvent`)的接口方法。
+
+## 一、流量数据(TRAFFIC)
+
+| 序号 | 接口路径 | HTTP方法 | 方法名 | 所在类 | 事件类型 |
+|------|---------|---------|--------|--------|---------|
+| 1 | `/aiSearch/search` | POST | `search` | `AiSearchController` | SEARCH |
+| 2 | `/store/info/getClientStoreDetail` | GET | `getClientStoreDetail` | `StoreInfoController` | VIEW |
+| 3 | `/userStore/getStoreDetailById` | GET | `getStoreDetailById` | `UserStoreController` | VIEW |
+
+## 二、互动数据(INTERACTION)
+
+| 序号 | 接口路径 | HTTP方法 | 方法名 | 所在类 | 事件类型 |
+|------|---------|---------|--------|--------|---------|
+| 4 | `/collect/addCollect` | POST | `addCollect` | `LifeCollectController` | COLLECT |
+| 5 | `/storeClockIn/addStoreClockIn` | POST | `addStoreClockIn` | `StoreClockInController` | CHECKIN |
+| 6 | `/renovation/requirement/consultRequirement` | POST | `consultRequirement` | `StoreRenovationRequirementController` | CONSULT |
+| 7 | `/userDynamics/addOrUpdate` | POST | `addOrUpdate` | `LifeUserDynamicsController` | POST_PUBLISH |
+
+## 三、服务质量(SERVICE)
+
+| 序号 | 接口路径 | HTTP方法 | 方法名 | 所在类 | 事件类型 |
+|------|---------|---------|--------|--------|---------|
+| 8 | `/commonRating/addRating` | POST | `add` | `CommonRatingController` | RATING_ADD |
+| 9 | `/commentAppeal/submit` | POST | `submitAppeal` | `CommentAppealController` | APPEAL |
+
+## 四、价目表(PRICE)
+
+| 序号 | 接口路径 | HTTP方法 | 方法名 | 所在类 | 事件类型 |
+|------|---------|---------|--------|--------|---------|
+| 10 | `/cuisine/getPage` | GET | `getPage` | `StoreCuisineController` | PRICE_VIEW |
+| 11 | `/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 |
+
+## 统计汇总
+
+- **总接口数**:13个
+- **流量数据**:3个
+- **互动数据**:4个
+- **服务质量**:2个
+- **价目表**:2个
+- **优惠券**:2个
+
+---
+
+**文档版本**:v1.0  
+**最后更新**:2026-01-14

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

@@ -0,0 +1,280 @@
+# 埋点统计数据JSON格式说明
+
+## 一、概述
+
+`store_track_statistics` 表中的各个数据字段存储的是JSON格式的统计数据。本文档详细说明各个字段的JSON格式。
+
+## 二、各字段JSON格式
+
+### 2.1 traffic_data(流量数据)
+
+**字段说明**:存储流量相关的统计数据
+
+**JSON格式**:
+```json
+{
+  "searchCount": 100,           // 搜索量(Long类型)
+  "viewCount": 500,             // 浏览量(Long类型)
+  "visitorCount": 300,          // 访客数(去重后的用户数,Long类型)
+  "newVisitorCount": 50,        // 新增访客数(Long类型)
+  "totalDuration": 1500000,     // 总访问时长(毫秒,Long类型)
+  "avgDuration": 5000           // 平均访问时长(毫秒,Long类型)
+}
+```
+
+**字段说明**:
+- `searchCount`: 店铺搜索次数
+- `viewCount`: 店铺浏览次数
+- `visitorCount`: 访客数(去重后的用户ID数量)
+- `newVisitorCount`: 新增访客数(在统计日期之前没有访问记录的用户)
+- `totalDuration`: 总访问时长(所有浏览事件duration字段的总和,单位:毫秒)
+- `avgDuration`: 平均访问时长(totalDuration / 有duration的浏览事件数,单位:毫秒)
+
+---
+
+### 2.2 interaction_data(互动数据)
+
+**字段说明**:存储用户互动相关的统计数据
+
+**JSON格式**:
+```json
+{
+  "collectCount": 50,           // 收藏次数(Long类型)
+  "shareCount": 30,             // 分享次数(Long类型)
+  "checkinCount": 20,           // 打卡次数(Long类型)
+  "consultCount": 10,           // 咨询次数(Long类型)
+  "friendCount": 100,           // 好友数量(互相关注的用户数,Long类型)
+  "followCount": 200,           // 关注数量(店铺用户关注的人数,Long类型)
+  "fansCount": 150,             // 粉丝数量(关注店铺用户的人数,Long类型)
+  "postCount": 25,              // 发布动态数量(Long类型)
+  "postLikeCount": 200,         // 动态点赞数量(Long类型)
+  "postCommentCount": 80,      // 动态评论数量(Long类型)
+  "postRepostCount": 15,        // 动态转发数量(Long类型)
+  "reportCount": 5,             // 被举报次数(Long类型)
+  "blockCount": 2               // 被拉黑次数(Long类型)
+}
+```
+
+**字段说明**:
+- `collectCount`: 店铺收藏次数(从埋点事件中统计COLLECT类型)
+- `shareCount`: 店铺分享次数(从埋点事件中统计SHARE类型)
+- `checkinCount`: 店铺打卡次数(从埋点事件中统计CHECKIN类型)
+- `consultCount`: 咨询商家次数(从埋点事件中统计CONSULT类型)
+- `friendCount`: 好友数量(通过life_fans表查询互相关注的用户数)
+- `followCount`: 关注数量(店铺用户关注的人数)
+- `fansCount`: 粉丝数量(关注店铺用户的人数)
+- `postCount`: 发布动态数量(从life_user_dynamics表查询,type=2商家社区)
+- `postLikeCount`: 动态点赞数量(从埋点事件中统计POST_LIKE类型)
+- `postCommentCount`: 动态评论数量(从埋点事件中统计POST_COMMENT类型)
+- `postRepostCount`: 动态转发数量(从埋点事件中统计POST_REPOST类型)
+- `reportCount`: 被举报次数(从埋点事件中统计REPORT类型)
+- `blockCount`: 被拉黑次数(从埋点事件中统计BLOCK类型)
+
+---
+
+### 2.3 coupon_data(优惠券数据)
+
+**字段说明**:存储优惠券相关的统计数据
+
+**JSON格式**:
+```json
+{
+  "giveToFriendCount": 50,                    // 赠送好友数量(Long类型)
+  "giveToFriendAmount": 5000.00,             // 赠送好友金额合计(BigDecimal类型,单位:元)
+  "giveToFriendUseCount": 30,                // 赠送好友使用数量(Long类型)
+  "giveToFriendUseAmount": 3000.00,          // 赠送好友使用金额合计(BigDecimal类型,单位:元)
+  "giveToFriendUseAmountPercent": 60.00,     // 赠送好友使用金额占比(Double类型,单位:%)
+  "friendGiveCount": 20,                     // 好友赠送数量(Long类型)
+  "friendGiveAmount": 2000.00,               // 好友赠送金额合计(BigDecimal类型,单位:元)
+  "friendGiveUseCount": 15,                  // 好友赠送使用数量(Long类型)
+  "friendGiveUseAmount": 1500.00,            // 好友赠送使用金额合计(BigDecimal类型,单位:元)
+  "friendGiveUseAmountPercent": 75.00        // 好友赠送使用金额占比(Double类型,单位:%)
+}
+```
+
+**字段说明**:
+- `giveToFriendCount`: 赠送好友数量(从life_discount_coupon_user表统计,关联店铺的优惠券)
+- `giveToFriendAmount`: 赠送好友金额合计(优惠券面值的总和)
+- `giveToFriendUseCount`: 赠送好友使用数量(status=1已使用的数量)
+- `giveToFriendUseAmount`: 赠送好友使用金额合计(已使用优惠券的面值总和)
+- `giveToFriendUseAmountPercent`: 赠送好友使用金额占比(giveToFriendUseAmount / giveToFriendAmount * 100)
+- `friendGiveCount`: 好友赠送数量(好友赠送给店铺用户的优惠券数量,从life_discount_coupon_store_friend表统计)
+- `friendGiveAmount`: 好友赠送金额合计(好友赠送的优惠券面值总和)
+- `friendGiveUseCount`: 好友赠送使用数量(已使用的数量)
+- `friendGiveUseAmount`: 好友赠送使用金额合计(已使用的优惠券面值总和)
+- `friendGiveUseAmountPercent`: 好友赠送使用金额占比(friendGiveUseAmount / friendGiveAmount * 100)
+
+---
+
+### 2.4 voucher_data(代金券数据)
+
+**字段说明**:存储代金券相关的统计数据
+
+**JSON格式**:
+```json
+{
+  "giveToFriendCount": 30,                    // 赠送好友数量(Long类型)
+  "giveToFriendAmount": 3000.00,             // 赠送好友金额合计(BigDecimal类型,单位:元)
+  "giveToFriendUseCount": 20,               // 赠送好友使用数量(Long类型)
+  "giveToFriendUseAmount": 2000.00,         // 赠送好友使用金额合计(BigDecimal类型,单位:元)
+  "giveToFriendUseAmountPercent": 66.67,    // 赠送好友使用金额占比(Double类型,单位:%)
+  "friendGiveCount": 10,                     // 好友赠送数量(Long类型)
+  "friendGiveAmount": 1000.00,               // 好友赠送金额合计(BigDecimal类型,单位:元)
+  "friendGiveUseCount": 8,                   // 好友赠送使用数量(Long类型)
+  "friendGiveUseAmount": 800.00,             // 好友赠送使用金额合计(BigDecimal类型,单位:元)
+  "friendGiveUseAmountPercent": 80.00        // 好友赠送使用金额占比(Double类型,单位:%)
+}
+```
+
+**字段说明**:
+- `giveToFriendCount`: 赠送好友数量(从埋点事件中统计VOUCHER_GIVE类型)
+- `giveToFriendAmount`: 赠送好友金额合计(从埋点事件的amount字段汇总)
+- `giveToFriendUseCount`: 赠送好友使用数量(从埋点事件中统计VOUCHER_USE类型)
+- `giveToFriendUseAmount`: 赠送好友使用金额合计(从埋点事件的amount字段汇总)
+- `giveToFriendUseAmountPercent`: 赠送好友使用金额占比(giveToFriendUseAmount / giveToFriendAmount * 100)
+- `friendGiveCount`: 好友赠送数量(好友赠送给店铺用户的代金券数量,从life_discount_coupon_store_friend表统计,type=1代金券)
+- `friendGiveAmount`: 好友赠送金额合计(好友赠送的代金券面值总和)
+- `friendGiveUseCount`: 好友赠送使用数量(已使用的数量)
+- `friendGiveUseAmount`: 好友赠送使用金额合计(已使用的代金券面值总和)
+- `friendGiveUseAmountPercent`: 好友赠送使用金额占比(friendGiveUseAmount / friendGiveAmount * 100)
+
+---
+
+### 2.5 service_data(服务质量数据)
+
+**字段说明**:存储服务质量相关的统计数据
+
+**JSON格式**:
+```json
+{
+  "storeScore": 4.5,              // 店铺评分(Double类型,0-5分)
+  "tasteScore": 4.3,              // 口味评分(Double类型,0-5分)
+  "environmentScore": 4.2,        // 环境评分(Double类型,0-5分)
+  "serviceScore": 4.4,            // 服务评分(Double类型,0-5分)
+  "ratingCount": 100,             // 评价数量(Long类型)
+  "goodRatingCount": 60,          // 好评数量(score >= 4.5,Long类型)
+  "midRatingCount": 30,           // 中评数量(3.0 <= score <= 4.0,Long类型)
+  "badRatingCount": 10,           // 差评数量(0.5 <= score <= 2.5,Long类型)
+  "badRatingPercent": 10.00,      // 差评占比(Double类型,单位:%)
+  "appealCount": 5,               // 差评申诉次数(Long类型)
+  "appealSuccessCount": 3,        // 差评申诉成功次数(Long类型)
+  "appealSuccessPercent": 60.00   // 差评申诉成功占比(Double类型,单位:%)
+}
+```
+
+**字段说明**:
+- `storeScore`: 店铺评分(从common_rating表统计,businessType=1,计算平均分)
+- `tasteScore`: 口味评分(从common_rating表的otherScore字段解析"口味"评分,计算平均分)
+- `environmentScore`: 环境评分(从common_rating表的otherScore字段解析"环境"评分,计算平均分)
+- `serviceScore`: 服务评分(从common_rating表的otherScore字段解析"服务"评分,计算平均分)
+- `ratingCount`: 评价数量(common_rating表中该店铺的评价总数)
+- `goodRatingCount`: 好评数量(score >= 4.5的评价数)
+- `midRatingCount`: 中评数量(3.0 <= score <= 4.0的评价数)
+- `badRatingCount`: 差评数量(0.5 <= score <= 2.5的评价数)
+- `badRatingPercent`: 差评占比(badRatingCount / ratingCount * 100)
+- `appealCount`: 差评申诉次数(从埋点事件中统计APPEAL类型)
+- `appealSuccessCount`: 差评申诉成功次数(从store_comment_appeal表统计,appealStatus=2已同意)
+- `appealSuccessPercent`: 差评申诉成功占比(appealSuccessCount / appealCount * 100)
+
+**otherScore字段格式示例**:
+```json
+{
+  "口味": 5.0,
+  "环境": 4.5,
+  "服务": 4.0
+}
+```
+
+---
+
+### 2.6 price_ranking_data(价目表排名数据)
+
+**字段说明**:存储价目表排名相关的统计数据(数组格式)
+
+**JSON格式**:
+```json
+[
+  {
+    "priceId": 1001,        // 价目表ID(Integer类型)
+    "viewCount": 500,       // 浏览量(Integer类型)
+    "visitorCount": 300,    // 访客数(Integer类型)
+    "shareCount": 50        // 分享数(Integer类型)
+  },
+  {
+    "priceId": 1002,
+    "viewCount": 400,
+    "visitorCount": 250,
+    "shareCount": 40
+  },
+  {
+    "priceId": 1003,
+    "viewCount": 300,
+    "visitorCount": 200,
+    "shareCount": 30
+  }
+]
+```
+
+**字段说明**:
+- 数组按`viewCount`(浏览量)降序排列
+- 每个元素包含一个价目表的统计数据
+- `priceId`: 价目表ID(targetId字段)
+- `viewCount`: 浏览量(PRICE_VIEW事件数量)
+- `visitorCount`: 访客数(去重后的用户ID数量)
+- `shareCount`: 分享数(PRICE_SHARE事件数量)
+
+---
+
+## 三、数据保存说明
+
+### 3.1 统计数据如何保存
+
+统计数据通过定时任务自动计算并保存:
+
+1. **日统计任务**:每天凌晨1点执行,计算前一天的统计数据(statType=DAILY)
+2. **周统计任务**:每周一凌晨2点执行,计算上一周的统计数据(statType=WEEKLY)
+3. **月统计任务**:每月1号凌晨3点执行,计算上一个月的统计数据(statType=MONTHLY)
+
+**定时任务类**:`TrackStatisticsScheduler.java`
+
+### 3.2 手动触发统计
+
+如果需要手动触发统计数据计算,可以调用:
+
+```java
+trackEventService.calculateAndSaveStatistics(storeId, statDate, statType);
+```
+
+**参数说明**:
+- `storeId`: 店铺ID
+- `statDate`: 统计日期
+- `statType`: 统计类型(DAILY/WEEKLY/MONTHLY)
+
+### 3.3 数据更新策略
+
+- 如果该店铺、该日期、该统计类型的记录已存在,则更新数据
+- 如果不存在,则创建新记录
+- 使用唯一索引 `uk_store_date_type` 保证唯一性
+
+---
+
+## 四、注意事项
+
+1. **数据类型**:
+   - Long类型字段在JSON中显示为数字(如:100)
+   - BigDecimal类型字段在JSON中显示为数字(如:5000.00)
+   - Double类型字段在JSON中显示为数字(如:4.5)
+   - 数组类型字段在JSON中显示为数组(如:[])
+
+2. **空值处理**:
+   - 如果某个分类没有数据,对应的字段可能为null或空对象
+   - 建议在查询时进行null判断
+
+3. **数据精度**:
+   - 金额字段使用BigDecimal保证精度
+   - 百分比字段保留2位小数
+   - 评分字段保留1位小数
+
+4. **性能优化**:
+   - 统计数据已预计算并存储,查询时直接读取,无需实时计算
+   - 建议定期清理历史统计数据,避免数据表过大

+ 4 - 1
alien-store/src/main/java/shop/alien/store/aspect/TrackEventAspect.java

@@ -133,7 +133,10 @@ public class TrackEventAspect {
         if (attributes != null) {
         if (attributes != null) {
             HttpServletRequest request = attributes.getRequest();
             HttpServletRequest request = attributes.getRequest();
             trackEvent.setIpAddress(getIpAddress(request));
             trackEvent.setIpAddress(getIpAddress(request));
-            trackEvent.setUserAgent(request.getHeader("User-Agent"));
+            String userAgent = request.getHeader("User-Agent");
+            trackEvent.setUserAgent(userAgent);
+            // 根据User-Agent解析设备类型
+            trackEvent.setDeviceType(shop.alien.store.util.UserAgentParserUtil.parseDeviceType(userAgent));
         }
         }
 
 
         return trackEvent;
         return trackEvent;

+ 0 - 143
alien-store/src/main/java/shop/alien/store/controller/AIRecoveryController.java

@@ -1,143 +0,0 @@
-package shop.alien.store.controller;
-
-import com.alibaba.fastjson.JSONObject;
-import com.fasterxml.jackson.databind.JsonNode;
-import io.swagger.annotations.*;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.web.bind.annotation.*;
-import shop.alien.entity.result.R;
-import shop.alien.store.feign.AlienAIFeign;
-import shop.alien.store.service.TrackEventService;
-
-import java.util.*;
-
-/**
- * AI推荐Controller
- *
- * @author system
- * @since 2026-01-14
- */
-@Slf4j
-@Api(tags = {"AI智能推荐"})
-@ApiSort(22)
-@CrossOrigin
-@RestController
-@RequestMapping("/business/ai")
-@RequiredArgsConstructor
-public class AIRecoveryController {
-
-    private final TrackEventService trackEventService;
-    private final AlienAIFeign alienAIFeign;
-
-    @ApiOperation("获取AI推荐")
-    @ApiOperationSupport(order = 1)
-    @ApiImplicitParams({
-            @ApiImplicitParam(name = "storeId", value = "店铺ID", dataType = "Integer", paramType = "query", required = true)
-    })
-    @GetMapping("/recommendation")
-    public R<Map<String, Object>> getRecommendation(@RequestParam("storeId") Integer storeId) {
-        log.info("获取AI推荐: storeId={}", storeId);
-
-        try {
-            // 获取最近30天的经营数据
-            Date endDate = new Date();
-            Date startDate = new Date(endDate.getTime() - 30L * 24 * 60 * 60 * 1000);
-            
-            Map<String, Object> businessData = trackEventService.getBusinessData(storeId, startDate, endDate, null);
-            
-            // 构建AI分析请求数据
-            Map<String, Object> aiRequestData = buildAIRequestData(storeId, businessData);
-            
-            // TODO: 调用AI服务进行分析
-            // JsonNode aiResponse = callAIService(aiRequestData);
-            
-            // 构建推荐结果
-            Map<String, Object> recommendation = buildRecommendation(businessData);
-            
-            return R.data(recommendation);
-        } catch (Exception e) {
-            log.error("获取AI推荐失败", e);
-            return R.fail("获取推荐失败: " + e.getMessage());
-        }
-    }
-
-    /**
-     * 构建AI请求数据
-     */
-    private Map<String, Object> buildAIRequestData(Integer storeId, Map<String, Object> businessData) {
-        Map<String, Object> requestData = new HashMap<>();
-        requestData.put("storeId", storeId);
-        requestData.put("businessData", businessData);
-        requestData.put("analysisType", "BUSINESS_RECOMMENDATION");
-        
-        // 构建提示词
-        StringBuilder prompt = new StringBuilder();
-        prompt.append("请分析以下店铺的经营数据,并提供优化建议:\n");
-        prompt.append("店铺ID: ").append(storeId).append("\n");
-        
-        Map<String, Object> trafficData = (Map<String, Object>) businessData.get("trafficData");
-        if (trafficData != null) {
-            prompt.append("流量数据: ").append(JSONObject.toJSONString(trafficData)).append("\n");
-        }
-        
-        Map<String, Object> priceRankingData = (Map<String, Object>) businessData.get("priceRankingData");
-        if (priceRankingData != null) {
-            prompt.append("价目表排名数据: ").append(JSONObject.toJSONString(priceRankingData)).append("\n");
-        }
-        
-        requestData.put("prompt", prompt.toString());
-        
-        return requestData;
-    }
-
-    /**
-     * 调用AI服务
-     */
-    private JsonNode callAIService(Map<String, Object> requestData) {
-        try {
-            // TODO: 实现AI服务调用逻辑
-            // 根据AlienAIFeign的接口定义调用相应的AI服务
-            // 注意:需要先登录获取token
-            
-            // 示例代码(需要根据实际的AI服务接口调整):
-            // JSONObject loginResult = alienAIFeign.login(...);
-            // String token = loginResult.getString("access_token");
-            // JsonNode response = alienAIFeign.generateRecommendation("Bearer " + token, requestData);
-            
-            return null;
-        } catch (Exception e) {
-            log.error("调用AI服务失败", e);
-            return null;
-        }
-    }
-
-    /**
-     * 构建推荐结果(示例实现,实际应该从AI服务返回)
-     */
-    private Map<String, Object> buildRecommendation(Map<String, Object> businessData) {
-        Map<String, Object> recommendation = new HashMap<>();
-        
-        // 示例:基于数据的简单推荐逻辑
-        Map<String, Object> priceRankingData = (Map<String, Object>) businessData.get("priceRankingData");
-        
-        StringBuilder summary = new StringBuilder();
-        List<Map<String, Object>> recommendations = new ArrayList<>();
-        
-        // 示例推荐逻辑:如果价目表价格高于平均值,给出建议
-        if (priceRankingData != null) {
-            summary.append("相较于其他同星级的店铺,您价目表中的锅包肉和烤羊腿价格远高于其他商家");
-            
-            Map<String, Object> rec1 = new HashMap<>();
-            rec1.put("type", "PRICING");
-            rec1.put("title", "价格优化建议");
-            rec1.put("content", "寻找原材料更便宜的菜场、菜量降低、菜名突出特色,如锡林郭勒盟羔羊烤羊腿");
-            recommendations.add(rec1);
-        }
-        
-        recommendation.put("summary", summary.toString());
-        recommendation.put("recommendations", recommendations);
-        
-        return recommendation;
-    }
-}

+ 7 - 0
alien-store/src/main/java/shop/alien/store/controller/AiSearchController.java

@@ -24,6 +24,7 @@ import shop.alien.entity.store.StoreUser;
 import shop.alien.entity.store.vo.StoreInfoVo;
 import shop.alien.entity.store.vo.StoreInfoVo;
 import shop.alien.mapper.StoreImgMapper;
 import shop.alien.mapper.StoreImgMapper;
 import shop.alien.mapper.StoreUserMapper;
 import shop.alien.mapper.StoreUserMapper;
+import shop.alien.store.annotation.TrackEvent;
 import shop.alien.store.service.CommonRatingService;
 import shop.alien.store.service.CommonRatingService;
 import shop.alien.store.service.StoreImgService;
 import shop.alien.store.service.StoreImgService;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
@@ -57,6 +58,12 @@ public class AiSearchController {
     private final StoreImgService storeImgService;
     private final StoreImgService storeImgService;
     private final CommonRatingService commonRatingService;
     private final CommonRatingService commonRatingService;
 
 
+    @TrackEvent(
+            eventType = "SEARCH",
+            eventCategory = "TRAFFIC",
+            storeId = "#{#storeId}",
+            targetType = "STORE"
+    )
     @RequestMapping("/search")
     @RequestMapping("/search")
     public R search(@RequestBody Map<String,String> map) {
     public R search(@RequestBody Map<String,String> map) {
 
 

+ 0 - 127
alien-store/src/main/java/shop/alien/store/controller/BusinessDataController.java

@@ -1,127 +0,0 @@
-package shop.alien.store.controller;
-
-import io.swagger.annotations.*;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.format.annotation.DateTimeFormat;
-import org.springframework.web.bind.annotation.*;
-import shop.alien.entity.result.R;
-import shop.alien.store.service.TrackEventService;
-
-import java.util.Date;
-import java.util.Map;
-
-/**
- * 经营数据Controller
- *
- * @author system
- * @since 2026-01-14
- */
-@Slf4j
-@Api(tags = {"经营数据统计"})
-@ApiSort(21)
-@CrossOrigin
-@RestController
-@RequestMapping("/business")
-@RequiredArgsConstructor
-public class BusinessDataController {
-
-    private final TrackEventService trackEventService;
-
-    @ApiOperation("查询经营数据")
-    @ApiOperationSupport(order = 1)
-    @ApiImplicitParams({
-            @ApiImplicitParam(name = "storeId", value = "店铺ID", dataType = "Integer", paramType = "query", required = true),
-            @ApiImplicitParam(name = "startDate", value = "开始日期", dataType = "String", paramType = "query", required = false, example = "2026-01-08"),
-            @ApiImplicitParam(name = "endDate", value = "结束日期", dataType = "String", paramType = "query", required = false, example = "2026-01-14"),
-            @ApiImplicitParam(name = "category", value = "数据分类(TRAFFIC,INTERACTION,COUPON,VOUCHER,SERVICE,PRICE)", dataType = "String", paramType = "query", required = false)
-    })
-    @GetMapping("/data")
-    public R<Map<String, Object>> getBusinessData(
-            @RequestParam("storeId") Integer storeId,
-            @RequestParam(value = "startDate", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date startDate,
-            @RequestParam(value = "endDate", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date endDate,
-            @RequestParam(value = "category", required = false) String category
-    ) {
-        log.info("查询经营数据: storeId={}, startDate={}, endDate={}, category={}", 
-                storeId, startDate, endDate, category);
-
-        try {
-            // 如果没有指定日期,默认查询最近7天的数据
-            if (startDate == null || endDate == null) {
-                Date now = new Date();
-                if (endDate == null) {
-                    endDate = now;
-                }
-                if (startDate == null) {
-                    long sevenDaysAgo = endDate.getTime() - 7 * 24 * 60 * 60 * 1000L;
-                    startDate = new Date(sevenDaysAgo);
-                }
-            }
-
-            Map<String, Object> data = trackEventService.getBusinessData(storeId, startDate, endDate, category);
-            return R.data(data);
-        } catch (Exception e) {
-            log.error("查询经营数据失败", e);
-            return R.fail("查询失败: " + e.getMessage());
-        }
-    }
-
-    @ApiOperation("对比两个时间段的数据")
-    @ApiOperationSupport(order = 2)
-    @ApiImplicitParams({
-            @ApiImplicitParam(name = "storeId", value = "店铺ID", dataType = "Integer", paramType = "query", required = true),
-            @ApiImplicitParam(name = "startDate1", value = "时间段1开始日期", dataType = "String", paramType = "query", required = true, example = "2026-01-08"),
-            @ApiImplicitParam(name = "endDate1", value = "时间段1结束日期", dataType = "String", paramType = "query", required = true, example = "2026-01-14"),
-            @ApiImplicitParam(name = "startDate2", value = "时间段2开始日期", dataType = "String", paramType = "query", required = true, example = "2026-01-01"),
-            @ApiImplicitParam(name = "endDate2", value = "时间段2结束日期", dataType = "String", paramType = "query", required = true, example = "2026-01-07")
-    })
-    @GetMapping("/data/compare")
-    public R<Map<String, Object>> compareBusinessData(
-            @RequestParam("storeId") Integer storeId,
-            @RequestParam("startDate1") @DateTimeFormat(pattern = "yyyy-MM-dd") Date startDate1,
-            @RequestParam("endDate1") @DateTimeFormat(pattern = "yyyy-MM-dd") Date endDate1,
-            @RequestParam("startDate2") @DateTimeFormat(pattern = "yyyy-MM-dd") Date startDate2,
-            @RequestParam("endDate2") @DateTimeFormat(pattern = "yyyy-MM-dd") Date endDate2
-    ) {
-        log.info("对比经营数据: storeId={}, period1=[{}, {}], period2=[{}, {}]", 
-                storeId, startDate1, endDate1, startDate2, endDate2);
-
-        try {
-            Map<String, Object> data = trackEventService.compareBusinessData(
-                    storeId, startDate1, endDate1, startDate2, endDate2);
-            return R.data(data);
-        } catch (Exception e) {
-            log.error("对比经营数据失败", e);
-            return R.fail("对比失败: " + e.getMessage());
-        }
-    }
-
-    @ApiOperation("查询历史数据")
-    @ApiOperationSupport(order = 3)
-    @ApiImplicitParams({
-            @ApiImplicitParam(name = "storeId", value = "店铺ID", dataType = "Integer", paramType = "query", required = true),
-            @ApiImplicitParam(name = "startDate", value = "开始日期", dataType = "String", paramType = "query", required = true, example = "2026-01-01"),
-            @ApiImplicitParam(name = "endDate", value = "结束日期", dataType = "String", paramType = "query", required = true, example = "2026-01-31"),
-            @ApiImplicitParam(name = "statType", value = "统计类型(DAILY,WEEKLY,MONTHLY)", dataType = "String", paramType = "query", required = false)
-    })
-    @GetMapping("/data/history")
-    public R<Map<String, Object>> getHistoryData(
-            @RequestParam("storeId") Integer storeId,
-            @RequestParam("startDate") @DateTimeFormat(pattern = "yyyy-MM-dd") Date startDate,
-            @RequestParam("endDate") @DateTimeFormat(pattern = "yyyy-MM-dd") Date endDate,
-            @RequestParam(value = "statType", required = false, defaultValue = "DAILY") String statType
-    ) {
-        log.info("查询历史数据: storeId={}, startDate={}, endDate={}, statType={}", 
-                storeId, startDate, endDate, statType);
-
-        try {
-            // TODO: 从统计表中查询历史数据
-            Map<String, Object> data = trackEventService.getBusinessData(storeId, startDate, endDate, null);
-            return R.data(data);
-        } catch (Exception e) {
-            log.error("查询历史数据失败", e);
-            return R.fail("查询失败: " + e.getMessage());
-        }
-    }
-}

+ 7 - 0
alien-store/src/main/java/shop/alien/store/controller/CommentAppealController.java

@@ -10,6 +10,7 @@ import shop.alien.entity.result.R;
 import shop.alien.entity.store.CommentAppeal;
 import shop.alien.entity.store.CommentAppeal;
 import shop.alien.entity.store.dto.AuditAppealRequestDto;
 import shop.alien.entity.store.dto.AuditAppealRequestDto;
 import shop.alien.entity.store.vo.CommentAppealVo;
 import shop.alien.entity.store.vo.CommentAppealVo;
+import shop.alien.store.annotation.TrackEvent;
 import shop.alien.store.service.CommentAppealService;
 import shop.alien.store.service.CommentAppealService;
 
 
 import java.text.SimpleDateFormat;
 import java.text.SimpleDateFormat;
@@ -33,6 +34,12 @@ public class CommentAppealController {
 
 
     private final CommentAppealService commentAppealService;
     private final CommentAppealService commentAppealService;
 
 
+    @TrackEvent(
+            eventType = "APPEAL",
+            eventCategory = "SERVICE",
+            storeId = "#{#commentAppeal.storeId}",
+            targetType = "COMMENT"
+    )
     @ApiOperation(value = "提交申诉", notes = "用户提交评论申诉")
     @ApiOperation(value = "提交申诉", notes = "用户提交评论申诉")
     @ApiOperationSupport(order = 1)
     @ApiOperationSupport(order = 1)
     @ApiImplicitParams({
     @ApiImplicitParams({

+ 7 - 0
alien-store/src/main/java/shop/alien/store/controller/CommonRatingController.java

@@ -6,6 +6,7 @@ import lombok.extern.slf4j.Slf4j;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.bind.annotation.*;
 import shop.alien.entity.result.R;
 import shop.alien.entity.result.R;
 import shop.alien.entity.store.CommonRating;
 import shop.alien.entity.store.CommonRating;
+import shop.alien.store.annotation.TrackEvent;
 import shop.alien.store.service.CommonCommentService;
 import shop.alien.store.service.CommonCommentService;
 import shop.alien.store.service.CommonRatingService;
 import shop.alien.store.service.CommonRatingService;
 
 
@@ -70,6 +71,12 @@ public class CommonRatingController {
 //        return R.data(commonRatingService.getById(id));
 //        return R.data(commonRatingService.getById(id));
 //    }
 //    }
 //
 //
+    @TrackEvent(
+            eventType = "RATING_ADD",
+            eventCategory = "SERVICE",
+            storeId = "#{#commonRating.businessId}",
+            targetType = "STORE"
+    )
     @ApiOperation(value = "新增评价", notes = "0:成功, 1:失败, 2:文本内容异常, 3:图片内容异常")
     @ApiOperation(value = "新增评价", notes = "0:成功, 1:失败, 2:文本内容异常, 3:图片内容异常")
     @PostMapping("/addRating")
     @PostMapping("/addRating")
     public R<Integer> add(@RequestBody CommonRating commonRating) {
     public R<Integer> add(@RequestBody CommonRating commonRating) {

+ 7 - 0
alien-store/src/main/java/shop/alien/store/controller/LifeCollectController.java

@@ -17,6 +17,7 @@ import shop.alien.entity.store.*;
 import shop.alien.entity.store.vo.StoreInfoVo;
 import shop.alien.entity.store.vo.StoreInfoVo;
 import shop.alien.mapper.*;
 import shop.alien.mapper.*;
 import shop.alien.mapper.second.SecondGoodsMapper;
 import shop.alien.mapper.second.SecondGoodsMapper;
+import shop.alien.store.annotation.TrackEvent;
 import shop.alien.store.config.GaoDeMapUtil;
 import shop.alien.store.config.GaoDeMapUtil;
 import shop.alien.util.common.ListToPage;
 import shop.alien.util.common.ListToPage;
 
 
@@ -203,6 +204,12 @@ public class LifeCollectController {
         return R.data(ListToPage.setPage(lifeCoupons, page, size));
         return R.data(ListToPage.setPage(lifeCoupons, page, size));
     }
     }
 
 
+    @TrackEvent(
+            eventType = "COLLECT",
+            eventCategory = "INTERACTION",
+            storeId = "#{#lifeCollect.storeId}",
+            targetType = "STORE"
+    )
     @ApiOperation("添加收藏")
     @ApiOperation("添加收藏")
     @ApiOperationSupport(order = 2)
     @ApiOperationSupport(order = 2)
     @PostMapping("addCollect")
     @PostMapping("addCollect")

+ 7 - 0
alien-store/src/main/java/shop/alien/store/controller/LifeCouponController.java

@@ -12,6 +12,7 @@ import shop.alien.entity.store.EssentialHolidayComparison;
 import shop.alien.entity.store.LifeCoupon;
 import shop.alien.entity.store.LifeCoupon;
 import shop.alien.entity.store.vo.LifeCouponStatusVo;
 import shop.alien.entity.store.vo.LifeCouponStatusVo;
 import shop.alien.entity.store.vo.LifeCouponVo;
 import shop.alien.entity.store.vo.LifeCouponVo;
+import shop.alien.store.annotation.TrackEvent;
 import shop.alien.store.service.LifeCouponService;
 import shop.alien.store.service.LifeCouponService;
 
 
 import java.util.Map;
 import java.util.Map;
@@ -96,6 +97,12 @@ public class LifeCouponController {
         }
         }
     }
     }
 
 
+    @TrackEvent(
+            eventType = "COUPON_USE",
+            eventCategory = "COUPON",
+            storeId = "#{#storeId}",
+            targetType = "COUPON"
+    )
     @ApiOperation("旧 核销订单")
     @ApiOperation("旧 核销订单")
     @ApiImplicitParams({@ApiImplicitParam(name = "storeId", value = "门店id", dataType = "Integer", paramType = "query", required = true), @ApiImplicitParam(name = "quanCode", value = "券码", dataType = "Integer", paramType = "query", required = true)})
     @ApiImplicitParams({@ApiImplicitParam(name = "storeId", value = "门店id", dataType = "Integer", paramType = "query", required = true), @ApiImplicitParam(name = "quanCode", value = "券码", dataType = "Integer", paramType = "query", required = true)})
     @GetMapping("/verify")
     @GetMapping("/verify")

+ 7 - 0
alien-store/src/main/java/shop/alien/store/controller/LifeDiscountCouponStoreFriendController.java

@@ -15,6 +15,7 @@ import shop.alien.entity.store.vo.LifeDiscountCouponFriendRuleDetailVo;
 import shop.alien.entity.store.vo.LifeDiscountCouponFriendRuleVo;
 import shop.alien.entity.store.vo.LifeDiscountCouponFriendRuleVo;
 import shop.alien.entity.store.vo.LifeDiscountCouponStoreFriendVo;
 import shop.alien.entity.store.vo.LifeDiscountCouponStoreFriendVo;
 import shop.alien.entity.store.vo.LifeGroupBuyCountDateVo;
 import shop.alien.entity.store.vo.LifeGroupBuyCountDateVo;
+import shop.alien.store.annotation.TrackEvent;
 import shop.alien.store.service.LifeDiscountCouponStoreFriendService;
 import shop.alien.store.service.LifeDiscountCouponStoreFriendService;
 import shop.alien.util.common.TokenInfo;
 import shop.alien.util.common.TokenInfo;
 import springfox.documentation.annotations.ApiIgnore;
 import springfox.documentation.annotations.ApiIgnore;
@@ -53,6 +54,12 @@ public class LifeDiscountCouponStoreFriendController {
         }
         }
     }
     }
 
 
+    @TrackEvent(
+            eventType = "COUPON_GIVE",
+            eventCategory = "COUPON",
+            storeId = "#{#lifeDiscountCouponStoreFriendDto.storeId}",
+            targetType = "COUPON"
+    )
     @ApiOperation("给好友发放优惠券")
     @ApiOperation("给好友发放优惠券")
     @ApiOperationSupport(order = 2)
     @ApiOperationSupport(order = 2)
     @PostMapping("/setFriendCoupon")
     @PostMapping("/setFriendCoupon")

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

@@ -13,6 +13,7 @@ import shop.alien.entity.result.R;
 import shop.alien.entity.store.LifeUserDynamics;
 import shop.alien.entity.store.LifeUserDynamics;
 import shop.alien.entity.store.vo.LifePinglunVo;
 import shop.alien.entity.store.vo.LifePinglunVo;
 import shop.alien.entity.store.vo.LifeUserDynamicsVo;
 import shop.alien.entity.store.vo.LifeUserDynamicsVo;
+import shop.alien.store.annotation.TrackEvent;
 import shop.alien.store.service.LifeUserDynamicsService;
 import shop.alien.store.service.LifeUserDynamicsService;
 import shop.alien.util.common.ListToPage;
 import shop.alien.util.common.ListToPage;
 import shop.alien.util.common.safe.*;
 import shop.alien.util.common.safe.*;
@@ -73,7 +74,11 @@ public class LifeUserDynamicsController {
         return R.data(lifeUserDynamicsService.getUserDynamics(myselfPhoneId, phoneId, type, page, size));
         return R.data(lifeUserDynamicsService.getUserDynamics(myselfPhoneId, phoneId, type, page, size));
     }
     }
 
 
-
+    @TrackEvent(
+            eventType = "POST_PUBLISH",
+            eventCategory = "INTERACTION",
+            targetType = "POST"
+    )
     @ApiOperation(value = "发布动态社区", notes = "0:成功, 1:失败, 2:文本内容异常, 3:图片内容异常")
     @ApiOperation(value = "发布动态社区", notes = "0:成功, 1:失败, 2:文本内容异常, 3:图片内容异常")
     @ApiOperationSupport(order = 2)
     @ApiOperationSupport(order = 2)
     @PostMapping("/addOrUpdate")
     @PostMapping("/addOrUpdate")

+ 7 - 0
alien-store/src/main/java/shop/alien/store/controller/StoreClockInController.java

@@ -8,6 +8,7 @@ import org.springframework.web.bind.annotation.*;
 import shop.alien.entity.result.R;
 import shop.alien.entity.result.R;
 import shop.alien.entity.store.StoreClockIn;
 import shop.alien.entity.store.StoreClockIn;
 import shop.alien.entity.store.vo.StoreClockInVo;
 import shop.alien.entity.store.vo.StoreClockInVo;
+import shop.alien.store.annotation.TrackEvent;
 import shop.alien.store.service.StoreClockInService;
 import shop.alien.store.service.StoreClockInService;
 
 
 @Slf4j
 @Slf4j
@@ -20,6 +21,12 @@ public class StoreClockInController {
 
 
     private final StoreClockInService storeClockInService;
     private final StoreClockInService storeClockInService;
 
 
+    @TrackEvent(
+            eventType = "CHECKIN",
+            eventCategory = "INTERACTION",
+            storeId = "#{#storeClockIn.storeId}",
+            targetType = "STORE"
+    )
     @ApiOperation("打卡")
     @ApiOperation("打卡")
     @ApiOperationSupport(order = 1)
     @ApiOperationSupport(order = 1)
     @PostMapping("/addStoreClockIn")
     @PostMapping("/addStoreClockIn")

+ 7 - 0
alien-store/src/main/java/shop/alien/store/controller/StoreCuisineController.java

@@ -15,6 +15,7 @@ import shop.alien.entity.store.StorePrice;
 import shop.alien.entity.store.dto.CuisineComboDto;
 import shop.alien.entity.store.dto.CuisineComboDto;
 import shop.alien.entity.store.dto.CuisineTypeResponseDto;
 import shop.alien.entity.store.dto.CuisineTypeResponseDto;
 import shop.alien.entity.store.vo.PriceListVo;
 import shop.alien.entity.store.vo.PriceListVo;
+import shop.alien.store.annotation.TrackEvent;
 import shop.alien.store.service.StoreCuisineService;
 import shop.alien.store.service.StoreCuisineService;
 import shop.alien.store.service.StorePriceService;
 import shop.alien.store.service.StorePriceService;
 import shop.alien.store.util.ai.AiGetPriceUtil;
 import shop.alien.store.util.ai.AiGetPriceUtil;
@@ -125,6 +126,12 @@ public class StoreCuisineController {
         return R.fail("操作失败");
         return R.fail("操作失败");
     }
     }
 
 
+    @TrackEvent(
+            eventType = "PRICE_VIEW",
+            eventCategory = "PRICE",
+            storeId = "#{#storeId}",
+            targetType = "PRICE"
+    )
     @ApiOperation("分页查询美食价目/通用价目")
     @ApiOperation("分页查询美食价目/通用价目")
     @ApiOperationSupport(order = 7)
     @ApiOperationSupport(order = 7)
     @ApiImplicitParams({
     @ApiImplicitParams({

+ 7 - 0
alien-store/src/main/java/shop/alien/store/controller/StoreInfoController.java

@@ -19,6 +19,7 @@ import shop.alien.entity.store.vo.*;
 import shop.alien.entity.storePlatform.StoreLicenseHistory;
 import shop.alien.entity.storePlatform.StoreLicenseHistory;
 import shop.alien.mapper.*;
 import shop.alien.mapper.*;
 import shop.alien.mapper.storePlantform.StoreLicenseHistoryMapper;
 import shop.alien.mapper.storePlantform.StoreLicenseHistoryMapper;
+import shop.alien.store.annotation.TrackEvent;
 import shop.alien.store.config.BaseRedisService;
 import shop.alien.store.config.BaseRedisService;
 import shop.alien.store.service.StoreInfoService;
 import shop.alien.store.service.StoreInfoService;
 import shop.alien.store.service.StoreQualificationService;
 import shop.alien.store.service.StoreQualificationService;
@@ -1027,6 +1028,12 @@ public class StoreInfoController {
         return R.data(ocrData);
         return R.data(ocrData);
     }
     }
 
 
+    @TrackEvent(
+            eventType = "VIEW",
+            eventCategory = "TRAFFIC",
+            storeId = "#{#id}",
+            targetType = "STORE"
+    )
     @ApiOperation(value = "获取店铺详情(用户端)")
     @ApiOperation(value = "获取店铺详情(用户端)")
     @ApiOperationSupport(order = 17)
     @ApiOperationSupport(order = 17)
     @GetMapping("/getClientStoreDetail")
     @GetMapping("/getClientStoreDetail")

+ 7 - 0
alien-store/src/main/java/shop/alien/store/controller/StorePriceController.java

@@ -12,6 +12,7 @@ import shop.alien.entity.result.R;
 import shop.alien.entity.store.StoreInfo;
 import shop.alien.entity.store.StoreInfo;
 import shop.alien.entity.store.StorePrice;
 import shop.alien.entity.store.StorePrice;
 import shop.alien.mapper.StoreInfoMapper;
 import shop.alien.mapper.StoreInfoMapper;
+import shop.alien.store.annotation.TrackEvent;
 import shop.alien.store.service.StorePriceService;
 import shop.alien.store.service.StorePriceService;
 import shop.alien.util.encryption.Decrypt;
 import shop.alien.util.encryption.Decrypt;
 import shop.alien.util.encryption.Encrypt;
 import shop.alien.util.encryption.Encrypt;
@@ -167,6 +168,12 @@ public class StorePriceController {
         return R.fail("批量删除失败");
         return R.fail("批量删除失败");
     }
     }
 
 
+    @TrackEvent(
+            eventType = "PRICE_VIEW",
+            eventCategory = "PRICE",
+            storeId = "#{#storeId}",
+            targetType = "PRICE"
+    )
     @ApiOperation("分页查询通用价目")
     @ApiOperation("分页查询通用价目")
     @ApiOperationSupport(order = 6)
     @ApiOperationSupport(order = 6)
     @ApiImplicitParams({
     @ApiImplicitParams({

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

@@ -9,6 +9,7 @@ import org.springframework.web.bind.annotation.*;
 import shop.alien.entity.result.R;
 import shop.alien.entity.result.R;
 import shop.alien.entity.store.dto.StoreRenovationBrowseRequirementDto;
 import shop.alien.entity.store.dto.StoreRenovationBrowseRequirementDto;
 import shop.alien.entity.store.dto.StoreRenovationRequirementDto;
 import shop.alien.entity.store.dto.StoreRenovationRequirementDto;
+import shop.alien.store.annotation.TrackEvent;
 import shop.alien.store.service.StoreRenovationBrowseRecordService;
 import shop.alien.store.service.StoreRenovationBrowseRecordService;
 import shop.alien.store.service.StoreRenovationRequirementService;
 import shop.alien.store.service.StoreRenovationRequirementService;
 
 
@@ -209,6 +210,12 @@ public class StoreRenovationRequirementController {
         }
         }
     }
     }
 
 
+    @TrackEvent(
+            eventType = "CONSULT",
+            eventCategory = "INTERACTION",
+            targetId = "#{#requirementId}",
+            targetType = "REQUIREMENT"
+    )
     @ApiOperation("装修商铺咨询装修需求(记录浏览和咨询历史)")
     @ApiOperation("装修商铺咨询装修需求(记录浏览和咨询历史)")
     @ApiOperationSupport(order = 9)
     @ApiOperationSupport(order = 9)
     @ApiImplicitParams({
     @ApiImplicitParams({

+ 5 - 0
alien-store/src/main/java/shop/alien/store/controller/TrackEventController.java

@@ -64,6 +64,11 @@ public class TrackEventController {
                 trackEvent.setUserAgent(request.getHeader("User-Agent"));
                 trackEvent.setUserAgent(request.getHeader("User-Agent"));
             }
             }
 
 
+            // 根据User-Agent解析设备类型(如果未设置)
+            if (trackEvent.getDeviceType() == null && trackEvent.getUserAgent() != null) {
+                trackEvent.setDeviceType(shop.alien.store.util.UserAgentParserUtil.parseDeviceType(trackEvent.getUserAgent()));
+            }
+
             // 异步保存埋点事件(写入Redis List)
             // 异步保存埋点事件(写入Redis List)
             trackEventService.saveTrackEvent(trackEvent);
             trackEventService.saveTrackEvent(trackEvent);
 
 

+ 7 - 0
alien-store/src/main/java/shop/alien/store/controller/UserStoreController.java

@@ -26,6 +26,7 @@ import shop.alien.entity.store.StoreCommentAppeal;
 import shop.alien.entity.store.vo.StoreCommentAppealVo;
 import shop.alien.entity.store.vo.StoreCommentAppealVo;
 import shop.alien.mapper.StoreCommentAppealMapper;
 import shop.alien.mapper.StoreCommentAppealMapper;
 import shop.alien.mapper.StoreCommentMapper;
 import shop.alien.mapper.StoreCommentMapper;
+import shop.alien.store.annotation.TrackEvent;
 import shop.alien.store.service.LifeUserStoreService;
 import shop.alien.store.service.LifeUserStoreService;
 import shop.alien.util.common.AlipayTradeAppPay;
 import shop.alien.util.common.AlipayTradeAppPay;
 import shop.alien.util.common.ListToPage;
 import shop.alien.util.common.ListToPage;
@@ -95,6 +96,12 @@ public class UserStoreController {
         return R.data(ListToPage.setPage(result, page, size));
         return R.data(ListToPage.setPage(result, page, size));
     }
     }
 
 
+    @TrackEvent(
+            eventType = "VIEW",
+            eventCategory = "TRAFFIC",
+            storeId = "#{#storeId}",
+            targetType = "STORE"
+    )
     @ApiOperation("查询商铺详情")
     @ApiOperation("查询商铺详情")
     @ApiOperationSupport(order = 2)
     @ApiOperationSupport(order = 2)
     @ApiImplicitParams({@ApiImplicitParam(name = "storeId", value = "商铺id", dataType = "String", paramType = "query"),
     @ApiImplicitParams({@ApiImplicitParam(name = "storeId", value = "商铺id", dataType = "String", paramType = "query"),

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

@@ -2,9 +2,7 @@ package shop.alien.store.service;
 
 
 import shop.alien.entity.store.StoreTrackEvent;
 import shop.alien.entity.store.StoreTrackEvent;
 
 
-import java.util.Date;
 import java.util.List;
 import java.util.List;
-import java.util.Map;
 
 
 /**
 /**
  * 埋点事件服务接口
  * 埋点事件服务接口
@@ -27,47 +25,4 @@ public interface TrackEventService {
      * @param trackEvents 埋点事件列表
      * @param trackEvents 埋点事件列表
      */
      */
     void batchSaveTrackEvents(List<StoreTrackEvent> trackEvents);
     void batchSaveTrackEvents(List<StoreTrackEvent> trackEvents);
-
-    /**
-     * 查询经营数据统计
-     *
-     * @param storeId   店铺ID
-     * @param startDate 开始日期
-     * @param endDate   结束日期
-     * @param category  数据分类
-     * @return 统计数据
-     */
-    Map<String, Object> getBusinessData(Integer storeId, Date startDate, Date endDate, String category);
-
-    /**
-     * 对比两个时间段的数据
-     *
-     * @param storeId    店铺ID
-     * @param startDate1 时间段1开始日期
-     * @param endDate1   时间段1结束日期
-     * @param startDate2 时间段2开始日期
-     * @param endDate2   时间段2结束日期
-     * @return 对比数据
-     */
-    Map<String, Object> compareBusinessData(Integer storeId, Date startDate1, Date endDate1, Date startDate2, Date endDate2);
-
-    /**
-     * 计算并保存统计数据
-     *
-     * @param storeId   店铺ID
-     * @param statDate  统计日期
-     * @param statType  统计类型
-     */
-    void calculateAndSaveStatistics(Integer storeId, Date statDate, String statType);
-
-    /**
-     * 获取价目表排名数据
-     *
-     * @param storeId   店铺ID
-     * @param startDate 开始日期
-     * @param endDate   结束日期
-     * @param limit     排名数量
-     * @return 排名数据
-     */
-    List<Map<String, Object>> getPriceRankingData(Integer storeId, Date startDate, Date endDate, Integer limit);
 }
 }

+ 2 - 351
alien-store/src/main/java/shop/alien/store/service/impl/TrackEventServiceImpl.java

@@ -1,21 +1,17 @@
 package shop.alien.store.service.impl;
 package shop.alien.store.service.impl;
 
 
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSON;
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import lombok.RequiredArgsConstructor;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.transaction.annotation.Transactional;
 import shop.alien.entity.store.StoreTrackEvent;
 import shop.alien.entity.store.StoreTrackEvent;
-import shop.alien.entity.store.StoreTrackStatistics;
 import shop.alien.mapper.StoreTrackEventMapper;
 import shop.alien.mapper.StoreTrackEventMapper;
-import shop.alien.mapper.StoreTrackStatisticsMapper;
 import shop.alien.store.config.BaseRedisService;
 import shop.alien.store.config.BaseRedisService;
 import shop.alien.store.service.TrackEventService;
 import shop.alien.store.service.TrackEventService;
 
 
-import java.util.*;
-import java.util.stream.Collectors;
+import java.util.List;
 
 
 /**
 /**
  * 埋点事件服务实现类
  * 埋点事件服务实现类
@@ -29,8 +25,6 @@ import java.util.stream.Collectors;
 @Transactional
 @Transactional
 public class TrackEventServiceImpl extends ServiceImpl<StoreTrackEventMapper, StoreTrackEvent> implements TrackEventService {
 public class TrackEventServiceImpl extends ServiceImpl<StoreTrackEventMapper, StoreTrackEvent> implements TrackEventService {
 
 
-    private final StoreTrackEventMapper trackEventMapper;
-    private final StoreTrackStatisticsMapper trackStatisticsMapper;
     private final BaseRedisService baseRedisService;
     private final BaseRedisService baseRedisService;
     
     
     private static final String REDIS_QUEUE_KEY = "track:event:queue";
     private static final String REDIS_QUEUE_KEY = "track:event:queue";
@@ -64,347 +58,4 @@ public class TrackEventServiceImpl extends ServiceImpl<StoreTrackEventMapper, St
             throw new RuntimeException("批量保存埋点事件失败", 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;
-    }
-}
+}

+ 189 - 0
alien-store/src/main/java/shop/alien/store/util/UserAgentParserUtil.java

@@ -0,0 +1,189 @@
+package shop.alien.store.util;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ * User-Agent解析工具类
+ * 用于从User-Agent字符串中解析设备类型
+ *
+ * @author system
+ * @since 2026-01-14
+ */
+@Slf4j
+public class UserAgentParserUtil {
+
+    /**
+     * 设备类型常量
+     */
+    public static final String DEVICE_TYPE_IOS = "iOS";
+    public static final String DEVICE_TYPE_ANDROID = "Android";
+    public static final String DEVICE_TYPE_HARMONY = "Harmony";
+    public static final String DEVICE_TYPE_PC = "PC";
+    public static final String DEVICE_TYPE_UNKNOWN = "Unknown";
+
+    /**
+     * 从User-Agent字符串中解析设备类型
+     * 优先返回移动端操作系统类型:iOS、Android、Harmony等
+     *
+     * @param userAgent User-Agent字符串
+     * @return 设备类型:iOS、Android、Harmony、PC、Unknown
+     */
+    public static String parseDeviceType(String userAgent) {
+        if (StringUtils.isBlank(userAgent)) {
+            return DEVICE_TYPE_UNKNOWN;
+        }
+
+        String ua = userAgent.toLowerCase();
+
+        // 判断是否为iOS设备(iPhone、iPad)
+        if (isIOS(ua)) {
+            return DEVICE_TYPE_IOS;
+        }
+
+        // 判断是否为HarmonyOS设备(鸿蒙)
+        if (isHarmonyOS(ua)) {
+            return DEVICE_TYPE_HARMONY;
+        }
+
+        // 判断是否为Android设备
+        if (isAndroid(ua)) {
+            return DEVICE_TYPE_ANDROID;
+        }
+
+        // 默认为PC
+        return DEVICE_TYPE_PC;
+    }
+
+    /**
+     * 判断是否为iOS设备(iPhone、iPad)
+     */
+    private static boolean isIOS(String ua) {
+        // iPhone
+        if (ua.contains("iphone")) {
+            return true;
+        }
+
+        // iPad
+        if (ua.contains("ipad")) {
+            return true;
+        }
+
+        // iOS标识
+        if (ua.contains("iphone os") || ua.contains("ios")) {
+            return true;
+        }
+
+        // iPod touch
+        if (ua.contains("ipod")) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * 判断是否为HarmonyOS设备(鸿蒙)
+     */
+    private static boolean isHarmonyOS(String ua) {
+        // HarmonyOS标识
+        if (ua.contains("harmonyos") || ua.contains("harmony os")) {
+            return true;
+        }
+
+        // 鸿蒙系统可能还包含其他标识
+        if (ua.contains("hmos")) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * 判断是否为Android设备
+     */
+    private static boolean isAndroid(String ua) {
+        // Android标识(包括手机和平板)
+        if (ua.contains("android")) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * 从User-Agent中提取浏览器信息(可选功能)
+     *
+     * @param userAgent User-Agent字符串
+     * @return 浏览器名称
+     */
+    public static String parseBrowser(String userAgent) {
+        if (StringUtils.isBlank(userAgent)) {
+            return "Unknown";
+        }
+
+        String ua = userAgent.toLowerCase();
+
+        if (ua.contains("chrome") && !ua.contains("edg")) {
+            return "Chrome";
+        } else if (ua.contains("safari") && !ua.contains("chrome")) {
+            return "Safari";
+        } else if (ua.contains("firefox")) {
+            return "Firefox";
+        } else if (ua.contains("edg")) {
+            return "Edge";
+        } else if (ua.contains("opera") || ua.contains("opr")) {
+            return "Opera";
+        } else if (ua.contains("msie") || ua.contains("trident")) {
+            return "IE";
+        } else if (ua.contains("micromessenger")) {
+            return "WeChat";
+        } else if (ua.contains("qqbrowser")) {
+            return "QQBrowser";
+        } else if (ua.contains("ucbrowser")) {
+            return "UCBrowser";
+        }
+
+        return "Unknown";
+    }
+
+    /**
+     * 从User-Agent中提取操作系统信息(可选功能)
+     *
+     * @param userAgent User-Agent字符串
+     * @return 操作系统名称
+     */
+    public static String parseOS(String userAgent) {
+        if (StringUtils.isBlank(userAgent)) {
+            return "Unknown";
+        }
+
+        String ua = userAgent.toLowerCase();
+
+        if (ua.contains("windows")) {
+            if (ua.contains("windows nt 10")) {
+                return "Windows 10";
+            } else if (ua.contains("windows nt 6.3")) {
+                return "Windows 8.1";
+            } else if (ua.contains("windows nt 6.2")) {
+                return "Windows 8";
+            } else if (ua.contains("windows nt 6.1")) {
+                return "Windows 7";
+            } else {
+                return "Windows";
+            }
+        } else if (ua.contains("mac os x") || ua.contains("macintosh")) {
+            return "macOS";
+        } else if (ua.contains("android")) {
+            return "Android";
+        } else if (ua.contains("iphone os") || ua.contains("ios")) {
+            return "iOS";
+        } else if (ua.contains("linux")) {
+            return "Linux";
+        } else if (ua.contains("unix")) {
+            return "Unix";
+        }
+
+        return "Unknown";
+    }
+}