Explorar o código

fix:修改用户端埋点逻辑

penghao hai 3 meses
pai
achega
0768909893
Modificáronse 20 ficheiros con 1094 adicións e 1 borrados
  1. 55 0
      alien-store/doc/埋点接口清单.md
  2. 280 0
      alien-store/doc/埋点统计数据JSON格式说明.md
  3. 283 0
      alien-store/src/main/java/shop/alien/store/aspect/TrackEventAspect.java
  4. 7 0
      alien-store/src/main/java/shop/alien/store/controller/AiSearchController.java
  5. 7 0
      alien-store/src/main/java/shop/alien/store/controller/CommentAppealController.java
  6. 7 0
      alien-store/src/main/java/shop/alien/store/controller/CommonRatingController.java
  7. 7 0
      alien-store/src/main/java/shop/alien/store/controller/LifeCollectController.java
  8. 7 0
      alien-store/src/main/java/shop/alien/store/controller/LifeCouponController.java
  9. 7 0
      alien-store/src/main/java/shop/alien/store/controller/LifeDiscountCouponStoreFriendController.java
  10. 6 1
      alien-store/src/main/java/shop/alien/store/controller/LifeUserDynamicsController.java
  11. 7 0
      alien-store/src/main/java/shop/alien/store/controller/StoreClockInController.java
  12. 7 0
      alien-store/src/main/java/shop/alien/store/controller/StoreCuisineController.java
  13. 7 0
      alien-store/src/main/java/shop/alien/store/controller/StoreInfoController.java
  14. 7 0
      alien-store/src/main/java/shop/alien/store/controller/StorePriceController.java
  15. 7 0
      alien-store/src/main/java/shop/alien/store/controller/StoreRenovationRequirementController.java
  16. 108 0
      alien-store/src/main/java/shop/alien/store/controller/TrackEventController.java
  17. 7 0
      alien-store/src/main/java/shop/alien/store/controller/UserStoreController.java
  18. 28 0
      alien-store/src/main/java/shop/alien/store/service/TrackEventService.java
  19. 61 0
      alien-store/src/main/java/shop/alien/store/service/impl/TrackEventServiceImpl.java
  20. 189 0
      alien-store/src/main/java/shop/alien/store/util/UserAgentParserUtil.java

+ 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. **性能优化**:
+   - 统计数据已预计算并存储,查询时直接读取,无需实时计算
+   - 建议定期清理历史统计数据,避免数据表过大

+ 283 - 0
alien-store/src/main/java/shop/alien/store/aspect/TrackEventAspect.java

@@ -0,0 +1,283 @@
+package shop.alien.store.aspect;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Pointcut;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.core.DefaultParameterNameDiscoverer;
+import org.springframework.core.annotation.Order;
+import org.springframework.expression.EvaluationContext;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.expression.spel.support.StandardEvaluationContext;
+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.store.annotation.TrackEvent;
+import shop.alien.store.config.BaseRedisService;
+import shop.alien.util.common.JwtUtil;
+
+import javax.servlet.http.HttpServletRequest;
+import java.lang.reflect.Method;
+import java.util.Date;
+
+/**
+ * 埋点事件切面
+ * 拦截标注了@TrackEvent注解的方法,自动收集埋点数据并写入Redis List
+ *
+ * @author system
+ * @since 2026-01-14
+ */
+@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();
+    
+    /**
+     * Redis List Key
+     */
+    private static final String REDIS_QUEUE_KEY = "track:event:queue";
+
+    /**
+     * 定义切点:所有标注了@TrackEvent注解的方法
+     */
+    @Pointcut("@annotation(shop.alien.store.annotation.TrackEvent)")
+    public void trackEventPointcut() {
+    }
+
+    /**
+     * 环绕通知:收集埋点数据并写入Redis List
+     */
+    @Around("trackEventPointcut()")
+    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
+        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
+        Method method = signature.getMethod();
+        TrackEvent annotation = method.getAnnotation(TrackEvent.class);
+
+        if (annotation == null) {
+            return joinPoint.proceed();
+        }
+
+        // 执行目标方法
+        Object result = joinPoint.proceed();
+
+        try {
+            // 构建埋点事件对象
+            StoreTrackEvent trackEvent = buildTrackEvent(joinPoint, annotation, result);
+            
+            // 异步写入Redis List
+            if (annotation.async()) {
+                String eventJson = JSON.toJSONString(trackEvent);
+                baseRedisService.setListRight(REDIS_QUEUE_KEY, eventJson);
+                log.debug("埋点事件已写入Redis List: eventType={}, storeId={}", 
+                        trackEvent.getEventType(), trackEvent.getStoreId());
+            }
+        } catch (Exception e) {
+            // 埋点失败不应该影响主流程
+            log.error("收集埋点数据失败", e);
+        }
+
+        return result;
+    }
+
+    /**
+     * 构建埋点事件对象
+     */
+    private StoreTrackEvent buildTrackEvent(ProceedingJoinPoint joinPoint, TrackEvent annotation, Object result) {
+        StoreTrackEvent trackEvent = new StoreTrackEvent();
+        
+        // 设置基本事件信息
+        trackEvent.setEventType(annotation.eventType());
+        trackEvent.setEventCategory(annotation.eventCategory());
+        trackEvent.setTargetType(StringUtils.isNotBlank(annotation.targetType()) ? annotation.targetType() : null);
+        trackEvent.setEventTime(new Date());
+
+        // 创建SpEL表达式上下文
+        EvaluationContext context = createEvaluationContext(joinPoint, result);
+
+        // 解析SpEL表达式获取storeId
+        Integer storeId = parseSpEL(annotation.storeId(), context, Integer.class);
+        if (storeId == null && StringUtils.isBlank(annotation.storeId())) {
+            // 尝试从方法参数中查找storeId
+            storeId = extractStoreIdFromArgs(joinPoint);
+        }
+        trackEvent.setStoreId(storeId);
+
+        // 解析SpEL表达式获取userId
+        Integer userId = parseSpEL(annotation.userId(), context, Integer.class);
+        if (userId == null && StringUtils.isBlank(annotation.userId())) {
+            // 从JWT中获取用户ID
+            userId = getUserIdFromJWT();
+        }
+        trackEvent.setUserId(userId);
+
+        // 解析SpEL表达式获取targetId
+        Integer targetId = parseSpEL(annotation.targetId(), context, Integer.class);
+        trackEvent.setTargetId(targetId);
+
+        // 获取请求信息
+        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
+        if (attributes != null) {
+            HttpServletRequest request = attributes.getRequest();
+            trackEvent.setIpAddress(getIpAddress(request));
+            String userAgent = request.getHeader("User-Agent");
+            trackEvent.setUserAgent(userAgent);
+            // 根据User-Agent解析设备类型
+            trackEvent.setDeviceType(shop.alien.store.util.UserAgentParserUtil.parseDeviceType(userAgent));
+        }
+
+        return trackEvent;
+    }
+
+    /**
+     * 创建SpEL表达式上下文
+     */
+    private EvaluationContext createEvaluationContext(ProceedingJoinPoint joinPoint, Object result) {
+        StandardEvaluationContext context = new StandardEvaluationContext();
+        
+        // 添加方法参数
+        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
+        Method method = signature.getMethod();
+        Object[] args = joinPoint.getArgs();
+        String[] parameterNames = parameterNameDiscoverer.getParameterNames(method);
+        
+        if (parameterNames != null && args != null) {
+            for (int i = 0; i < parameterNames.length; i++) {
+                context.setVariable(parameterNames[i], args[i]);
+            }
+        }
+        
+        // 添加返回值
+        if (result != null) {
+            context.setVariable("result", result);
+        }
+
+        return context;
+    }
+
+    /**
+     * 解析SpEL表达式
+     */
+    private <T> T parseSpEL(String expression, EvaluationContext context, Class<T> clazz) {
+        if (StringUtils.isBlank(expression)) {
+            return null;
+        }
+
+        try {
+            // 如果表达式不包含#,直接作为字符串处理
+            if (!expression.contains("#")) {
+                if (clazz == String.class) {
+                    return clazz.cast(expression);
+                }
+                return null;
+            }
+
+            // 解析SpEL表达式
+            Object value = spelExpressionParser.parseExpression(expression).getValue(context);
+            if (value == null) {
+                return null;
+            }
+
+            if (clazz.isInstance(value)) {
+                return clazz.cast(value);
+            }
+            
+            // 类型转换
+            if (clazz == Integer.class && value instanceof Number) {
+                return clazz.cast(((Number) value).intValue());
+            }
+            
+            return null;
+        } catch (Exception e) {
+            log.debug("解析SpEL表达式失败: {}", expression, e);
+            return null;
+        }
+    }
+
+    /**
+     * 从方法参数中提取storeId
+     */
+    private Integer extractStoreIdFromArgs(ProceedingJoinPoint joinPoint) {
+        try {
+            Object[] args = joinPoint.getArgs();
+            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
+            String[] parameterNames = parameterNameDiscoverer.getParameterNames(signature.getMethod());
+
+            if (args != null && parameterNames != null) {
+                for (int i = 0; i < parameterNames.length; i++) {
+                    // 查找名为storeId的参数
+                    if ("storeId".equals(parameterNames[i]) && args[i] != null) {
+                        if (args[i] instanceof Integer) {
+                            return (Integer) args[i];
+                        } else if (args[i] instanceof String) {
+                            return Integer.parseInt((String) args[i]);
+                        }
+                    }
+                }
+            }
+        } catch (Exception e) {
+            log.debug("从方法参数中提取storeId失败", e);
+        }
+        return null;
+    }
+
+    /**
+     * 从JWT中获取用户ID
+     */
+    private Integer getUserIdFromJWT() {
+        try {
+            JSONObject userInfo = JwtUtil.getCurrentUserInfo();
+            if (userInfo != null && userInfo.get("userId") != null) {
+                Object userIdObj = userInfo.get("userId");
+                if (userIdObj instanceof Integer) {
+                    return (Integer) userIdObj;
+                } else if (userIdObj instanceof String) {
+                    return Integer.parseInt((String) userIdObj);
+                }
+            }
+        } catch (Exception e) {
+            log.debug("从JWT获取用户ID失败", e);
+        }
+        return null;
+    }
+
+    /**
+     * 获取客户端IP地址
+     */
+    private String getIpAddress(HttpServletRequest request) {
+        String ip = request.getHeader("X-Forwarded-For");
+        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getHeader("Proxy-Client-IP");
+        }
+        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getHeader("WL-Proxy-Client-IP");
+        }
+        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getHeader("HTTP_CLIENT_IP");
+        }
+        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getHeader("HTTP_X_FORWARDED_FOR");
+        }
+        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getRemoteAddr();
+        }
+        // 如果是多级代理,取第一个IP
+        if (ip != null && ip.contains(",")) {
+            ip = ip.split(",")[0].trim();
+        }
+        return ip;
+    }
+}

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

+ 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.dto.AuditAppealRequestDto;
 import shop.alien.entity.store.vo.CommentAppealVo;
+import shop.alien.store.annotation.TrackEvent;
 import shop.alien.store.service.CommentAppealService;
 
 import java.text.SimpleDateFormat;
@@ -33,6 +34,12 @@ public class CommentAppealController {
 
     private final CommentAppealService commentAppealService;
 
+    @TrackEvent(
+            eventType = "APPEAL",
+            eventCategory = "SERVICE",
+            storeId = "#{#commentAppeal.storeId}",
+            targetType = "COMMENT"
+    )
     @ApiOperation(value = "提交申诉", notes = "用户提交评论申诉")
     @ApiOperationSupport(order = 1)
     @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 shop.alien.entity.result.R;
 import shop.alien.entity.store.CommonRating;
+import shop.alien.store.annotation.TrackEvent;
 import shop.alien.store.service.CommonCommentService;
 import shop.alien.store.service.CommonRatingService;
 
@@ -70,6 +71,12 @@ public class CommonRatingController {
 //        return R.data(commonRatingService.getById(id));
 //    }
 //
+    @TrackEvent(
+            eventType = "RATING_ADD",
+            eventCategory = "SERVICE",
+            storeId = "#{#commonRating.businessId}",
+            targetType = "STORE"
+    )
     @ApiOperation(value = "新增评价", notes = "0:成功, 1:失败, 2:文本内容异常, 3:图片内容异常")
     @PostMapping("/addRating")
     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.mapper.*;
 import shop.alien.mapper.second.SecondGoodsMapper;
+import shop.alien.store.annotation.TrackEvent;
 import shop.alien.store.config.GaoDeMapUtil;
 import shop.alien.util.common.ListToPage;
 
@@ -203,6 +204,12 @@ public class LifeCollectController {
         return R.data(ListToPage.setPage(lifeCoupons, page, size));
     }
 
+    @TrackEvent(
+            eventType = "COLLECT",
+            eventCategory = "INTERACTION",
+            storeId = "#{#lifeCollect.storeId}",
+            targetType = "STORE"
+    )
     @ApiOperation("添加收藏")
     @ApiOperationSupport(order = 2)
     @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.vo.LifeCouponStatusVo;
 import shop.alien.entity.store.vo.LifeCouponVo;
+import shop.alien.store.annotation.TrackEvent;
 import shop.alien.store.service.LifeCouponService;
 
 import java.util.Map;
@@ -96,6 +97,12 @@ public class LifeCouponController {
         }
     }
 
+    @TrackEvent(
+            eventType = "COUPON_USE",
+            eventCategory = "COUPON",
+            storeId = "#{#storeId}",
+            targetType = "COUPON"
+    )
     @ApiOperation("旧 核销订单")
     @ApiImplicitParams({@ApiImplicitParam(name = "storeId", value = "门店id", dataType = "Integer", paramType = "query", required = true), @ApiImplicitParam(name = "quanCode", value = "券码", dataType = "Integer", paramType = "query", required = true)})
     @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.LifeDiscountCouponStoreFriendVo;
 import shop.alien.entity.store.vo.LifeGroupBuyCountDateVo;
+import shop.alien.store.annotation.TrackEvent;
 import shop.alien.store.service.LifeDiscountCouponStoreFriendService;
 import shop.alien.util.common.TokenInfo;
 import springfox.documentation.annotations.ApiIgnore;
@@ -53,6 +54,12 @@ public class LifeDiscountCouponStoreFriendController {
         }
     }
 
+    @TrackEvent(
+            eventType = "COUPON_GIVE",
+            eventCategory = "COUPON",
+            storeId = "#{#lifeDiscountCouponStoreFriendDto.storeId}",
+            targetType = "COUPON"
+    )
     @ApiOperation("给好友发放优惠券")
     @ApiOperationSupport(order = 2)
     @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.vo.LifePinglunVo;
 import shop.alien.entity.store.vo.LifeUserDynamicsVo;
+import shop.alien.store.annotation.TrackEvent;
 import shop.alien.store.service.LifeUserDynamicsService;
 import shop.alien.util.common.ListToPage;
 import shop.alien.util.common.safe.*;
@@ -73,7 +74,11 @@ public class LifeUserDynamicsController {
         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:图片内容异常")
     @ApiOperationSupport(order = 2)
     @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.store.StoreClockIn;
 import shop.alien.entity.store.vo.StoreClockInVo;
+import shop.alien.store.annotation.TrackEvent;
 import shop.alien.store.service.StoreClockInService;
 
 @Slf4j
@@ -20,6 +21,12 @@ public class StoreClockInController {
 
     private final StoreClockInService storeClockInService;
 
+    @TrackEvent(
+            eventType = "CHECKIN",
+            eventCategory = "INTERACTION",
+            storeId = "#{#storeClockIn.storeId}",
+            targetType = "STORE"
+    )
     @ApiOperation("打卡")
     @ApiOperationSupport(order = 1)
     @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.CuisineTypeResponseDto;
 import shop.alien.entity.store.vo.PriceListVo;
+import shop.alien.store.annotation.TrackEvent;
 import shop.alien.store.service.StoreCuisineService;
 import shop.alien.store.service.StorePriceService;
 import shop.alien.store.util.ai.AiGetPriceUtil;
@@ -125,6 +126,12 @@ public class StoreCuisineController {
         return R.fail("操作失败");
     }
 
+    @TrackEvent(
+            eventType = "PRICE_VIEW",
+            eventCategory = "PRICE",
+            storeId = "#{#storeId}",
+            targetType = "PRICE"
+    )
     @ApiOperation("分页查询美食价目/通用价目")
     @ApiOperationSupport(order = 7)
     @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.mapper.*;
 import shop.alien.mapper.storePlantform.StoreLicenseHistoryMapper;
+import shop.alien.store.annotation.TrackEvent;
 import shop.alien.store.config.BaseRedisService;
 import shop.alien.store.service.StoreInfoService;
 import shop.alien.store.service.StoreQualificationService;
@@ -1027,6 +1028,12 @@ public class StoreInfoController {
         return R.data(ocrData);
     }
 
+    @TrackEvent(
+            eventType = "VIEW",
+            eventCategory = "TRAFFIC",
+            storeId = "#{#id}",
+            targetType = "STORE"
+    )
     @ApiOperation(value = "获取店铺详情(用户端)")
     @ApiOperationSupport(order = 17)
     @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.StorePrice;
 import shop.alien.mapper.StoreInfoMapper;
+import shop.alien.store.annotation.TrackEvent;
 import shop.alien.store.service.StorePriceService;
 import shop.alien.util.encryption.Decrypt;
 import shop.alien.util.encryption.Encrypt;
@@ -167,6 +168,12 @@ public class StorePriceController {
         return R.fail("批量删除失败");
     }
 
+    @TrackEvent(
+            eventType = "PRICE_VIEW",
+            eventCategory = "PRICE",
+            storeId = "#{#storeId}",
+            targetType = "PRICE"
+    )
     @ApiOperation("分页查询通用价目")
     @ApiOperationSupport(order = 6)
     @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.store.dto.StoreRenovationBrowseRequirementDto;
 import shop.alien.entity.store.dto.StoreRenovationRequirementDto;
+import shop.alien.store.annotation.TrackEvent;
 import shop.alien.store.service.StoreRenovationBrowseRecordService;
 import shop.alien.store.service.StoreRenovationRequirementService;
 
@@ -209,6 +210,12 @@ public class StoreRenovationRequirementController {
         }
     }
 
+    @TrackEvent(
+            eventType = "CONSULT",
+            eventCategory = "INTERACTION",
+            targetId = "#{#requirementId}",
+            targetType = "REQUIREMENT"
+    )
     @ApiOperation("装修商铺咨询装修需求(记录浏览和咨询历史)")
     @ApiOperationSupport(order = 9)
     @ApiImplicitParams({

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

@@ -0,0 +1,108 @@
+package shop.alien.store.controller;
+
+import com.alibaba.fastjson.JSONObject;
+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.entity.store.StoreTrackEvent;
+import shop.alien.store.service.TrackEventService;
+import shop.alien.util.common.JwtUtil;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.Date;
+
+/**
+ * 埋点事件Controller
+ *
+ * @author system
+ * @since 2026-01-14
+ */
+@Slf4j
+@Api(tags = {"埋点事件管理"})
+@ApiSort(20)
+@CrossOrigin
+@RestController
+@RequestMapping("/track")
+@RequiredArgsConstructor
+public class TrackEventController {
+
+    private final TrackEventService trackEventService;
+
+    @ApiOperation("上报埋点事件")
+    @ApiOperationSupport(order = 1)
+    @PostMapping("/event")
+    public R<String> reportEvent(@RequestBody StoreTrackEvent trackEvent, HttpServletRequest request) {
+        log.info("上报埋点事件: {}", JSONObject.toJSONString(trackEvent));
+
+        try {
+            // 设置默认值
+            if (trackEvent.getEventTime() == null) {
+                trackEvent.setEventTime(new Date());
+            }
+
+            // 从JWT中获取用户ID(如果未指定)
+            if (trackEvent.getUserId() == null) {
+                try {
+                    JSONObject userInfo = JwtUtil.getCurrentUserInfo();
+                    if (userInfo != null && userInfo.get("userId") != null) {
+                        trackEvent.setUserId(userInfo.getInteger("userId"));
+                    }
+                } catch (Exception e) {
+                    log.debug("无法从JWT获取用户ID", e);
+                }
+            }
+
+            // 获取IP地址
+            if (trackEvent.getIpAddress() == null) {
+                trackEvent.setIpAddress(getClientIpAddress(request));
+            }
+
+            // 获取User-Agent
+            if (trackEvent.getUserAgent() == null) {
+                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)
+            trackEventService.saveTrackEvent(trackEvent);
+
+            return R.success("上报成功");
+        } catch (Exception e) {
+            log.error("上报埋点事件失败", e);
+            return R.fail("上报失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 获取客户端IP地址
+     */
+    private String getClientIpAddress(HttpServletRequest request) {
+        String ip = request.getHeader("X-Forwarded-For");
+        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getHeader("Proxy-Client-IP");
+        }
+        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getHeader("WL-Proxy-Client-IP");
+        }
+        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getHeader("HTTP_CLIENT_IP");
+        }
+        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getHeader("HTTP_X_FORWARDED_FOR");
+        }
+        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getRemoteAddr();
+        }
+        // 如果是多级代理,取第一个IP
+        if (ip != null && ip.contains(",")) {
+            ip = ip.split(",")[0].trim();
+        }
+        return ip;
+    }
+}

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

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

@@ -0,0 +1,28 @@
+package shop.alien.store.service;
+
+import shop.alien.entity.store.StoreTrackEvent;
+
+import java.util.List;
+
+/**
+ * 埋点事件服务接口
+ *
+ * @author system
+ * @since 2026-01-14
+ */
+public interface TrackEventService {
+
+    /**
+     * 保存埋点事件(异步写入Redis)
+     *
+     * @param trackEvent 埋点事件
+     */
+    void saveTrackEvent(StoreTrackEvent trackEvent);
+
+    /**
+     * 批量保存埋点事件(写入数据库)
+     *
+     * @param trackEvents 埋点事件列表
+     */
+    void batchSaveTrackEvents(List<StoreTrackEvent> trackEvents);
+}

+ 61 - 0
alien-store/src/main/java/shop/alien/store/service/impl/TrackEventServiceImpl.java

@@ -0,0 +1,61 @@
+package shop.alien.store.service.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import shop.alien.entity.store.StoreTrackEvent;
+import shop.alien.mapper.StoreTrackEventMapper;
+import shop.alien.store.config.BaseRedisService;
+import shop.alien.store.service.TrackEventService;
+
+import java.util.List;
+
+/**
+ * 埋点事件服务实现类
+ *
+ * @author system
+ * @since 2026-01-14
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+@Transactional
+public class TrackEventServiceImpl extends ServiceImpl<StoreTrackEventMapper, StoreTrackEvent> implements TrackEventService {
+
+    private final BaseRedisService baseRedisService;
+    
+    private static final String REDIS_QUEUE_KEY = "track:event:queue";
+
+    @Override
+    public void saveTrackEvent(StoreTrackEvent trackEvent) {
+        // 异步写入Redis List
+        try {
+            String eventJson = JSON.toJSONString(trackEvent);
+            baseRedisService.setListRight(REDIS_QUEUE_KEY, eventJson);
+            log.debug("埋点事件已写入Redis List: eventType={}, storeId={}", 
+                    trackEvent.getEventType(), trackEvent.getStoreId());
+        } catch (Exception e) {
+            log.error("写入Redis List失败", e);
+            throw new RuntimeException("写入Redis List失败", e);
+        }
+    }
+
+    @Override
+    public void batchSaveTrackEvents(List<StoreTrackEvent> trackEvents) {
+        if (trackEvents == null || trackEvents.isEmpty()) {
+            return;
+        }
+
+        try {
+            // 批量保存到数据库
+            boolean result = this.saveBatch(trackEvents, 100);
+            log.info("批量保存埋点事件: 总数={}, 成功={}", trackEvents.size(), result);
+        } catch (Exception e) {
+            log.error("批量保存埋点事件失败", e);
+            throw new RuntimeException("批量保存埋点事件失败", e);
+        }
+    }
+}

+ 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";
+    }
+}