Browse Source

直播实时埋点

xw 6 days ago
parent
commit
a280f057a4
19 changed files with 1488 additions and 11 deletions
  1. 1 1
      fs-common-api/src/main/resources/application.yml
  2. 2 0
      fs-live-app/src/main/java/com/fs/framework/config/ThreadPoolConfig.java
  3. 26 5
      fs-live-app/src/main/java/com/fs/live/controller/LiveDataController.java
  4. 25 0
      fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java
  5. 1 1
      fs-live-app/src/main/resources/application.yml
  6. 95 0
      fs-service/src/main/java/com/fs/live/domain/LiveUserBehaviorTrack.java
  7. 54 0
      fs-service/src/main/java/com/fs/live/enums/LiveBehaviorResourceTypeEnum.java
  8. 78 0
      fs-service/src/main/java/com/fs/live/enums/LiveBehaviorTypeEnum.java
  9. 110 0
      fs-service/src/main/java/com/fs/live/mapper/LiveUserBehaviorTrackMapper.java
  10. 115 0
      fs-service/src/main/java/com/fs/live/service/ILiveUserBehaviorTrackService.java
  11. 284 0
      fs-service/src/main/java/com/fs/live/service/impl/LiveUserBehaviorTrackServiceImpl.java
  12. 293 0
      fs-service/src/main/java/com/fs/live/util/LiveBehaviorTrackUtil.java
  13. 166 0
      fs-service/src/main/resources/mapper/live/LiveUserBehaviorTrackMapper.xml
  14. 1 0
      fs-user-app/src/main/java/com/fs/FsUserAppApplication.java
  15. 144 0
      fs-user-app/src/main/java/com/fs/app/controller/live/LiveBehaviorTrackController.java
  16. 23 2
      fs-user-app/src/main/java/com/fs/app/controller/live/LiveDataController.java
  17. 23 1
      fs-user-app/src/main/java/com/fs/app/controller/live/LiveGoodsController.java
  18. 45 1
      fs-user-app/src/main/java/com/fs/app/controller/live/LiveOrderController.java
  19. 2 0
      fs-user-app/src/main/java/com/fs/framework/config/ThreadPoolConfig.java

+ 1 - 1
fs-common-api/src/main/resources/application.yml

@@ -5,4 +5,4 @@ server:
 spring:
   profiles:
 #    active: dev
-    active: druid-ylrz
+    active: druid-bjczwh-test

+ 2 - 0
fs-live-app/src/main/java/com/fs/framework/config/ThreadPoolConfig.java

@@ -4,6 +4,7 @@ import com.fs.common.utils.Threads;
 import org.apache.commons.lang3.concurrent.BasicThreadFactory;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.annotation.EnableAsync;
 import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
 
 import java.util.concurrent.ScheduledExecutorService;
@@ -16,6 +17,7 @@ import java.util.concurrent.ThreadPoolExecutor;
 
  **/
 @Configuration
+@EnableAsync
 public class ThreadPoolConfig
 {
     // 核心线程池大小

+ 26 - 5
fs-live-app/src/main/java/com/fs/live/controller/LiveDataController.java

@@ -3,25 +3,46 @@ package com.fs.live.controller;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
+import com.fs.his.domain.FsUser;
+import com.fs.his.service.IFsUserService;
+import com.fs.live.domain.LiveUserBehaviorTrack;
+import com.fs.live.service.ILiveUserBehaviorTrackService;
+import com.fs.live.util.LiveBehaviorTrackUtil;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
 
+
+@Slf4j
 @RestController
 @RequestMapping("/app/live/liveData")
 public class LiveDataController extends BaseController {
 
     @Autowired
     private RedisCache redisCache;
+
+    @Autowired
+    private ILiveUserBehaviorTrackService behaviorTrackService;
     /**
      * 点赞
      * */
     @GetMapping("/like/{liveId}")
-    public R like(@PathVariable("liveId") Long liveId) {
+    public R like(@PathVariable("liveId") Long liveId, @RequestParam Long userId) {
+
         //直播间总点赞数
         Long increment = redisCache.incr("live:like:" + liveId, 1);
+
+        // 埋点:记录点赞行为
+        if (liveId != null && userId != null) {
+            try {
+                LiveUserBehaviorTrack track = LiveBehaviorTrackUtil.trackLike(userId, liveId);
+                behaviorTrackService.asyncInsertLiveUserBehaviorTrack(track);
+            } catch (Exception e) {
+                log.error("[埋点] 记录点赞行为失败, liveId={}, userId={}",
+                        liveId, userId, e);
+            }
+        }
+
         return R.ok().put("like",increment);
     }
 }

+ 25 - 0
fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java

@@ -16,6 +16,11 @@ import com.fs.live.mapper.LiveCouponMapper;
 import com.fs.live.vo.LiveWatchUserEntry;
 import com.fs.live.domain.LiveWatchLog;
 import com.fs.live.domain.LiveVideo;
+import com.fs.live.domain.LiveUserBehaviorTrack;
+import com.fs.live.service.ILiveUserBehaviorTrackService;
+import com.fs.live.util.LiveBehaviorTrackUtil;
+import com.fs.live.enums.LiveBehaviorTypeEnum;
+import com.fs.live.enums.LiveBehaviorResourceTypeEnum;
 import com.fs.live.websocket.auth.WebSocketConfigurator;
 import com.fs.live.websocket.bean.SendMsgVo;
 import com.fs.common.core.domain.R;
@@ -82,6 +87,7 @@ public class WebSocketServer {
     private final ILiveWatchLogService liveWatchLogService = SpringUtils.getBean(ILiveWatchLogService.class);
     private final ILiveVideoService liveVideoService = SpringUtils.getBean(ILiveVideoService.class);
     private final ILiveCompletionPointsRecordService completionPointsRecordService = SpringUtils.getBean(ILiveCompletionPointsRecordService.class);
+    private final ILiveUserBehaviorTrackService behaviorTrackService = SpringUtils.getBean(ILiveUserBehaviorTrackService.class);
     private static Random random = new Random();
     
     // Redis key 前缀:用户进入直播间时间
@@ -425,6 +431,15 @@ public class WebSocketServer {
                         liveMsg.setReplayFlag(replayFlag);
 
                         liveMsgService.insertLiveMsg(liveMsg);
+                        
+                        // 埋点:记录发送消息行为
+                        try {
+                            LiveUserBehaviorTrack track = LiveBehaviorTrackUtil.trackSendMessage(
+                                msg.getUserId(), msg.getLiveId(), liveMsg.getMsgId());
+                            behaviorTrackService.asyncInsertLiveUserBehaviorTrack(track);
+                        } catch (Exception e) {
+                            log.error("[埋点] 记录发送消息行为失败, liveId={}, userId={}", msg.getLiveId(), msg.getUserId(), e);
+                        }
                     }
 
                     msg.setOn(true);
@@ -461,6 +476,16 @@ public class WebSocketServer {
                     }
 
                     liveMsgService.insertLiveMsg(liveMsg);
+                    
+                    // 埋点:记录普通消息行为
+                    try {
+                        LiveUserBehaviorTrack track = LiveBehaviorTrackUtil.trackSendMessage(
+                            msg.getUserId(), msg.getLiveId(), liveMsg.getMsgId());
+                        behaviorTrackService.asyncInsertLiveUserBehaviorTrack(track);
+                    } catch (Exception e) {
+                        log.error("[埋点] 记录普通消息行为失败, liveId={}, userId={}", msg.getLiveId(), msg.getUserId(), e);
+                    }
+                    
                     msg.setOn(true);
                     msg.setData(JSONObject.toJSONString(liveMsg));
                     msg.setCmd("sendMsg");

+ 1 - 1
fs-live-app/src/main/resources/application.yml

@@ -6,4 +6,4 @@ server:
 # Spring配置
 spring:
   profiles:
-    active: druid-bjzm-test
+    active: druid-bjczwh-test

+ 95 - 0
fs-service/src/main/java/com/fs/live/domain/LiveUserBehaviorTrack.java

@@ -0,0 +1,95 @@
+package com.fs.live.domain;
+
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 直播用户行为轨迹对象 live_user_behavior_track
+ *
+ * @author xw
+ * @date 2026-01-05
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class LiveUserBehaviorTrack extends BaseEntity {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 主键ID */
+    private Long id;
+
+    /** 直播间ID */
+    @Excel(name = "直播间ID")
+    private Long liveId;
+
+    /** 用户ID */
+    @Excel(name = "用户ID")
+    private Long userId;
+
+    /** 企业ID */
+    @Excel(name = "企业ID")
+    private Long companyId;
+
+    /** 企业用户ID(销售ID) */
+    @Excel(name = "企业用户ID")
+    private Long companyUserId;
+
+    /**
+     * 行为类型:
+     * 1-进入直播间
+     * 2-退出直播间
+     * 3-点赞
+     * 4-发送消息
+     * 5-点击商品
+     * 6-加入购物车
+     * 7-购买商品
+     * 8-收藏商品
+     * 9-关注主播
+     * 10-领取优惠券
+     * 11-参与抽奖
+     * 12-领取红包
+     * 13-分享直播间
+     * 14-观看时长记录
+     * 15-完课记录
+     */
+    @Excel(name = "行为类型")
+    private Integer behaviorType;
+
+    /** 行为描述 */
+    @Excel(name = "行为描述")
+    private String behaviorDesc;
+
+    /** 关联资源ID(如商品ID、消息ID等) */
+    @Excel(name = "关联资源ID")
+    private Long resourceId;
+
+    /** 关联资源类型(1-商品 2-消息 3-优惠券 4-红包 5-抽奖等) */
+    @Excel(name = "关联资源类型")
+    private Integer resourceType;
+
+    /** 扩展数据(JSON格式,存储额外信息) */
+    @Excel(name = "扩展数据")
+    private String extData;
+
+    /** 行为发生时间戳 */
+    @Excel(name = "行为发生时间戳")
+    private Long behaviorTime;
+
+    /** IP地址 */
+    @Excel(name = "IP地址")
+    private String ipAddress;
+
+    /** 设备信息 */
+    @Excel(name = "设备信息")
+    private String deviceInfo;
+
+    /** 用户代理信息 */
+    @Excel(name = "用户代理")
+    private String userAgent;
+
+    /** 分表后缀(根据userId尾数计算) */
+    private String tableSuffix;
+
+}

+ 54 - 0
fs-service/src/main/java/com/fs/live/enums/LiveBehaviorResourceTypeEnum.java

@@ -0,0 +1,54 @@
+package com.fs.live.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 直播行为资源类型枚举
+ *
+ * @author xw
+ * @date 2026-01-05
+ */
+@Getter
+@AllArgsConstructor
+public enum LiveBehaviorResourceTypeEnum {
+
+    /** 商品 */
+    GOODS(1, "商品"),
+
+    /** 消息 */
+    MESSAGE(2, "消息"),
+
+    /** 优惠券 */
+    COUPON(3, "优惠券"),
+
+    /** 红包 */
+    RED_PACKET(4, "红包"),
+
+    /** 抽奖 */
+    LOTTERY(5, "抽奖"),
+
+    /** 订单 */
+    ORDER(6, "订单"),
+
+    /** 主播 */
+    ANCHOR(7, "主播");
+
+    private final Integer code;
+    private final String desc;
+
+    /**
+     * 根据code获取枚举
+     */
+    public static LiveBehaviorResourceTypeEnum getByCode(Integer code) {
+        if (code == null) {
+            return null;
+        }
+        for (LiveBehaviorResourceTypeEnum typeEnum : values()) {
+            if (typeEnum.getCode().equals(code)) {
+                return typeEnum;
+            }
+        }
+        return null;
+    }
+}

+ 78 - 0
fs-service/src/main/java/com/fs/live/enums/LiveBehaviorTypeEnum.java

@@ -0,0 +1,78 @@
+package com.fs.live.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 直播用户行为类型枚举
+ *
+ * @author xw
+ * @date 2026-01-05
+ */
+@Getter
+@AllArgsConstructor
+public enum LiveBehaviorTypeEnum {
+
+    /** 进入直播间 */
+    ENTER_LIVE(1, "进入直播间"),
+
+    /** 退出直播间 */
+    LEAVE_LIVE(2, "退出直播间"),
+
+    /** 点赞 */
+    LIKE(3, "点赞"),
+
+    /** 发送消息 */
+    SEND_MESSAGE(4, "发送消息"),
+
+    /** 点击商品 */
+    CLICK_GOODS(5, "点击商品"),
+
+    /** 加入购物车 */
+    ADD_CART(6, "加入购物车"),
+
+    /** 购买商品 */
+    PURCHASE(7, "购买商品"),
+
+    /** 收藏商品 */
+    COLLECT_GOODS(8, "收藏商品"),
+
+    /** 关注主播 */
+    FOLLOW_ANCHOR(9, "关注主播"),
+
+    /** 领取优惠券 */
+    RECEIVE_COUPON(10, "领取优惠券"),
+
+    /** 参与抽奖 */
+    JOIN_LOTTERY(11, "参与抽奖"),
+
+    /** 领取红包 */
+    RECEIVE_RED_PACKET(12, "领取红包"),
+
+    /** 分享直播间 */
+    SHARE_LIVE(13, "分享直播间"),
+
+    /** 观看时长记录 */
+    WATCH_DURATION(14, "观看时长记录"),
+
+    /** 完课记录 */
+    COMPLETION(15, "完课记录");
+
+    private final Integer code;
+    private final String desc;
+
+    /**
+     * 根据code获取枚举
+     */
+    public static LiveBehaviorTypeEnum getByCode(Integer code) {
+        if (code == null) {
+            return null;
+        }
+        for (LiveBehaviorTypeEnum typeEnum : values()) {
+            if (typeEnum.getCode().equals(code)) {
+                return typeEnum;
+            }
+        }
+        return null;
+    }
+}

+ 110 - 0
fs-service/src/main/java/com/fs/live/mapper/LiveUserBehaviorTrackMapper.java

@@ -0,0 +1,110 @@
+package com.fs.live.mapper;
+
+import com.fs.live.domain.LiveUserBehaviorTrack;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 直播用户行为轨迹Mapper接口
+ *
+ * @author xw
+ * @date 2026-01-05
+ */
+public interface LiveUserBehaviorTrackMapper {
+
+    /**
+     * 查询直播用户行为轨迹
+     *
+     * @param id 主键
+     * @param tableSuffix 表后缀
+     * @return 直播用户行为轨迹
+     */
+    public LiveUserBehaviorTrack selectLiveUserBehaviorTrackById(@Param("id") Long id,
+                                                                  @Param("tableSuffix") String tableSuffix);
+
+    /**
+     * 查询直播用户行为轨迹列表
+     *
+     * @param track 直播用户行为轨迹
+     * @return 直播用户行为轨迹集合
+     */
+    public List<LiveUserBehaviorTrack> selectLiveUserBehaviorTrackList(LiveUserBehaviorTrack track);
+
+    /**
+     * 新增直播用户行为轨迹
+     *
+     * @param track 直播用户行为轨迹
+     * @return 结果
+     */
+    public int insertLiveUserBehaviorTrack(LiveUserBehaviorTrack track);
+
+    /**
+     * 批量新增直播用户行为轨迹
+     *
+     * @param trackList 直播用户行为轨迹列表
+     * @return 结果
+     */
+    public int batchInsertLiveUserBehaviorTrack(@Param("list") List<LiveUserBehaviorTrack> trackList);
+
+    /**
+     * 修改直播用户行为轨迹
+     *
+     * @param track 直播用户行为轨迹
+     * @return 结果
+     */
+    public int updateLiveUserBehaviorTrack(LiveUserBehaviorTrack track);
+
+    /**
+     * 删除直播用户行为轨迹
+     *
+     * @param id 主键
+     * @param tableSuffix 表后缀
+     * @return 结果
+     */
+    public int deleteLiveUserBehaviorTrackById(@Param("id") Long id,
+                                                @Param("tableSuffix") String tableSuffix);
+
+    /**
+     * 批量删除直播用户行为轨迹
+     *
+     * @param ids 需要删除的数据主键集合
+     * @param tableSuffix 表后缀
+     * @return 结果
+     */
+    public int deleteLiveUserBehaviorTrackByIds(@Param("ids") Long[] ids,
+                                                 @Param("tableSuffix") String tableSuffix);
+
+    /**
+     * 根据用户ID和直播ID查询行为轨迹
+     *
+     * @param userId 用户ID
+     * @param liveId 直播ID
+     * @param tableSuffix 表后缀
+     * @return 行为轨迹列表
+     */
+    public List<LiveUserBehaviorTrack> selectByUserIdAndLiveId(@Param("userId") Long userId,
+                                                                @Param("liveId") Long liveId,
+                                                                @Param("tableSuffix") String tableSuffix);
+
+    /**
+     * 根据直播ID统计各类行为数量
+     *
+     * @param liveId 直播ID
+     * @param tableSuffix 表后缀(如果为null则查询所有表)
+     * @return 统计结果
+     */
+    public List<Map<String, Object>> statisticsByLiveId(@Param("liveId") Long liveId,
+                                                         @Param("tableSuffix") String tableSuffix);
+
+    /**
+     * 根据用户ID统计行为数量
+     *
+     * @param userId 用户ID
+     * @param tableSuffix 表后缀
+     * @return 统计结果
+     */
+    public List<Map<String, Object>> statisticsByUserId(@Param("userId") Long userId,
+                                                         @Param("tableSuffix") String tableSuffix);
+}

+ 115 - 0
fs-service/src/main/java/com/fs/live/service/ILiveUserBehaviorTrackService.java

@@ -0,0 +1,115 @@
+package com.fs.live.service;
+
+import com.fs.live.domain.LiveUserBehaviorTrack;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 直播用户行为轨迹Service接口
+ *
+ * @author xw
+ * @date 2026-01-05
+ */
+public interface ILiveUserBehaviorTrackService {
+
+    /**
+     * 查询直播用户行为轨迹
+     *
+     * @param id 主键
+     * @param userId 用户ID(用于计算表后缀)
+     * @return 直播用户行为轨迹
+     */
+    public LiveUserBehaviorTrack selectLiveUserBehaviorTrackById(Long id, Long userId);
+
+    /**
+     * 查询直播用户行为轨迹列表
+     *
+     * @param track 直播用户行为轨迹
+     * @return 直播用户行为轨迹集合
+     */
+    public List<LiveUserBehaviorTrack> selectLiveUserBehaviorTrackList(LiveUserBehaviorTrack track);
+
+    /**
+     * 新增直播用户行为轨迹(单条)
+     *
+     * @param track 直播用户行为轨迹
+     * @return 结果
+     */
+    public int insertLiveUserBehaviorTrack(LiveUserBehaviorTrack track);
+
+    /**
+     * 异步新增直播用户行为轨迹(推荐使用,不阻塞主线程)
+     *
+     * @param track 直播用户行为轨迹
+     */
+    public void asyncInsertLiveUserBehaviorTrack(LiveUserBehaviorTrack track);
+
+    /**
+     * 批量新增直播用户行为轨迹
+     *
+     * @param trackList 直播用户行为轨迹列表
+     * @return 结果
+     */
+    public int batchInsertLiveUserBehaviorTrack(List<LiveUserBehaviorTrack> trackList);
+
+    /**
+     * 修改直播用户行为轨迹
+     *
+     * @param track 直播用户行为轨迹
+     * @return 结果
+     */
+    public int updateLiveUserBehaviorTrack(LiveUserBehaviorTrack track);
+
+    /**
+     * 批量删除直播用户行为轨迹
+     *
+     * @param ids 需要删除的数据主键集合
+     * @param userId 用户ID(用于计算表后缀)
+     * @return 结果
+     */
+    public int deleteLiveUserBehaviorTrackByIds(Long[] ids, Long userId);
+
+    /**
+     * 删除直播用户行为轨迹信息
+     *
+     * @param id 主键
+     * @param userId 用户ID(用于计算表后缀)
+     * @return 结果
+     */
+    public int deleteLiveUserBehaviorTrackById(Long id, Long userId);
+
+    /**
+     * 根据用户ID和直播ID查询行为轨迹
+     *
+     * @param userId 用户ID
+     * @param liveId 直播ID
+     * @return 行为轨迹列表
+     */
+    public List<LiveUserBehaviorTrack> selectByUserIdAndLiveId(Long userId, Long liveId);
+
+    /**
+     * 根据直播ID统计各类行为数量(聚合所有分表数据)
+     *
+     * @param liveId 直播ID
+     * @return 统计结果
+     */
+    public Map<String, Long> statisticsByLiveId(Long liveId);
+
+    /**
+     * 根据用户ID统计行为数量
+     *
+     * @param userId 用户ID
+     * @return 统计结果
+     */
+    public Map<String, Long> statisticsByUserId(Long userId);
+
+    /**
+     * 根据直播ID统计各类行为数量(指定分表)
+     *
+     * @param liveId 直播ID
+     * @param tableSuffix 表后缀
+     * @return 统计结果
+     */
+    public List<Map<String, Object>> statisticsByLiveIdWithTable(Long liveId, String tableSuffix);
+}

+ 284 - 0
fs-service/src/main/java/com/fs/live/service/impl/LiveUserBehaviorTrackServiceImpl.java

@@ -0,0 +1,284 @@
+package com.fs.live.service.impl;
+
+import com.fs.common.utils.DateUtils;
+import com.fs.live.domain.LiveUserBehaviorTrack;
+import com.fs.live.mapper.LiveUserBehaviorTrackMapper;
+import com.fs.live.service.ILiveUserBehaviorTrackService;
+import com.fs.live.util.LiveBehaviorTrackUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 直播用户行为轨迹Service业务层处理
+ *
+ * @author xw
+ * @date 2026-01-05
+ */
+@Slf4j
+@Service
+public class LiveUserBehaviorTrackServiceImpl implements ILiveUserBehaviorTrackService {
+
+    @Autowired
+    private LiveUserBehaviorTrackMapper liveUserBehaviorTrackMapper;
+
+    /**
+     * 查询直播用户行为轨迹
+     *
+     * @param id 主键
+     * @param userId 用户ID(用于计算表后缀)
+     * @return 直播用户行为轨迹
+     */
+    @Override
+    public LiveUserBehaviorTrack selectLiveUserBehaviorTrackById(Long id, Long userId) {
+        String tableSuffix = LiveBehaviorTrackUtil.getTableSuffix(userId);
+        return liveUserBehaviorTrackMapper.selectLiveUserBehaviorTrackById(id, tableSuffix);
+    }
+
+    /**
+     * 查询直播用户行为轨迹列表
+     *
+     * @param track 直播用户行为轨迹
+     * @return 直播用户行为轨迹集合
+     */
+    @Override
+    public List<LiveUserBehaviorTrack> selectLiveUserBehaviorTrackList(LiveUserBehaviorTrack track) {
+        // 如果有userId,直接查询对应的分表
+        if (track.getUserId() != null) {
+            String tableSuffix = LiveBehaviorTrackUtil.getTableSuffix(track.getUserId());
+            track.setTableSuffix(tableSuffix);
+            return liveUserBehaviorTrackMapper.selectLiveUserBehaviorTrackList(track);
+        }
+
+        // 如果没有userId,需要查询所有分表并聚合结果
+        List<LiveUserBehaviorTrack> resultList = new ArrayList<>();
+        for (int i = 0; i < 10; i++) {
+            track.setTableSuffix(String.valueOf(i));
+            List<LiveUserBehaviorTrack> list = liveUserBehaviorTrackMapper.selectLiveUserBehaviorTrackList(track);
+            if (list != null && !list.isEmpty()) {
+                resultList.addAll(list);
+            }
+        }
+
+        // 按时间倒序排序
+        resultList.sort((a, b) -> {
+            if (a.getBehaviorTime() == null || b.getBehaviorTime() == null) {
+                return 0;
+            }
+            return b.getBehaviorTime().compareTo(a.getBehaviorTime());
+        });
+
+        return resultList;
+    }
+
+    /**
+     * 新增直播用户行为轨迹(同步)
+     *
+     * @param track 直播用户行为轨迹
+     * @return 结果
+     */
+    @Override
+    public int insertLiveUserBehaviorTrack(LiveUserBehaviorTrack track) {
+        // 自动设置分表后缀
+        if (track.getTableSuffix() == null) {
+            track.setTableSuffix(LiveBehaviorTrackUtil.getTableSuffix(track.getUserId()));
+        }
+        // 设置创建时间
+        if (track.getCreateTime() == null) {
+            track.setCreateTime(DateUtils.getNowDate());
+        }
+        // 设置行为时间戳
+        if (track.getBehaviorTime() == null) {
+            track.setBehaviorTime(System.currentTimeMillis());
+        }
+
+        return liveUserBehaviorTrackMapper.insertLiveUserBehaviorTrack(track);
+    }
+
+    /**
+     * 异步新增直播用户行为轨迹(推荐使用,不阻塞主线程)
+     *
+     * @param track 直播用户行为轨迹
+     */
+    @Async
+    @Override
+    public void asyncInsertLiveUserBehaviorTrack(LiveUserBehaviorTrack track) {
+        try {
+            insertLiveUserBehaviorTrack(track);
+        } catch (Exception e) {
+            log.error("异步插入用户行为轨迹失败: userId={}, liveId={}, behaviorType={}",
+                     track.getUserId(), track.getLiveId(), track.getBehaviorType(), e);
+        }
+    }
+
+    /**
+     * 批量新增直播用户行为轨迹
+     *
+     * @param trackList 直播用户行为轨迹列表
+     * @return 结果
+     */
+    @Override
+    public int batchInsertLiveUserBehaviorTrack(List<LiveUserBehaviorTrack> trackList) {
+        if (trackList == null || trackList.isEmpty()) {
+            return 0;
+        }
+
+        // 按分表后缀分组
+        Map<String, List<LiveUserBehaviorTrack>> groupedMap = trackList.stream()
+                .peek(track -> {
+                    // 自动设置分表后缀
+                    if (track.getTableSuffix() == null) {
+                        track.setTableSuffix(LiveBehaviorTrackUtil.getTableSuffix(track.getUserId()));
+                    }
+                    // 设置创建时间
+                    if (track.getCreateTime() == null) {
+                        track.setCreateTime(DateUtils.getNowDate());
+                    }
+                    // 设置行为时间戳
+                    if (track.getBehaviorTime() == null) {
+                        track.setBehaviorTime(System.currentTimeMillis());
+                    }
+                })
+                .collect(Collectors.groupingBy(LiveUserBehaviorTrack::getTableSuffix));
+
+        int totalCount = 0;
+        // 分表批量插入
+        for (Map.Entry<String, List<LiveUserBehaviorTrack>> entry : groupedMap.entrySet()) {
+            try {
+                int count = liveUserBehaviorTrackMapper.batchInsertLiveUserBehaviorTrack(entry.getValue());
+                totalCount += count;
+            } catch (Exception e) {
+                log.error("批量插入用户行为轨迹失败: tableSuffix={}", entry.getKey(), e);
+            }
+        }
+
+        return totalCount;
+    }
+
+    /**
+     * 修改直播用户行为轨迹
+     *
+     * @param track 直播用户行为轨迹
+     * @return 结果
+     */
+    @Override
+    public int updateLiveUserBehaviorTrack(LiveUserBehaviorTrack track) {
+        // 自动设置分表后缀
+        if (track.getTableSuffix() == null) {
+            track.setTableSuffix(LiveBehaviorTrackUtil.getTableSuffix(track.getUserId()));
+        }
+        return liveUserBehaviorTrackMapper.updateLiveUserBehaviorTrack(track);
+    }
+
+    /**
+     * 批量删除直播用户行为轨迹
+     *
+     * @param ids 需要删除的数据主键集合
+     * @param userId 用户ID(用于计算表后缀)
+     * @return 结果
+     */
+    @Override
+    public int deleteLiveUserBehaviorTrackByIds(Long[] ids, Long userId) {
+        String tableSuffix = LiveBehaviorTrackUtil.getTableSuffix(userId);
+        return liveUserBehaviorTrackMapper.deleteLiveUserBehaviorTrackByIds(ids, tableSuffix);
+    }
+
+    /**
+     * 删除直播用户行为轨迹信息
+     *
+     * @param id 主键
+     * @param userId 用户ID(用于计算表后缀)
+     * @return 结果
+     */
+    @Override
+    public int deleteLiveUserBehaviorTrackById(Long id, Long userId) {
+        String tableSuffix = LiveBehaviorTrackUtil.getTableSuffix(userId);
+        return liveUserBehaviorTrackMapper.deleteLiveUserBehaviorTrackById(id, tableSuffix);
+    }
+
+    /**
+     * 根据用户ID和直播ID查询行为轨迹
+     *
+     * @param userId 用户ID
+     * @param liveId 直播ID
+     * @return 行为轨迹列表
+     */
+    @Override
+    public List<LiveUserBehaviorTrack> selectByUserIdAndLiveId(Long userId, Long liveId) {
+        String tableSuffix = LiveBehaviorTrackUtil.getTableSuffix(userId);
+        return liveUserBehaviorTrackMapper.selectByUserIdAndLiveId(userId, liveId, tableSuffix);
+    }
+
+    /**
+     * 根据直播ID统计各类行为数量(聚合所有分表数据)
+     *
+     * @param liveId 直播ID
+     * @return 统计结果
+     */
+    @Override
+    public Map<String, Long> statisticsByLiveId(Long liveId) {
+        Map<String, Long> resultMap = new HashMap<>();
+
+        // 查询所有分表并聚合结果
+        for (int i = 0; i < 10; i++) {
+            String tableSuffix = String.valueOf(i);
+            try {
+                List<Map<String, Object>> list = liveUserBehaviorTrackMapper.statisticsByLiveId(liveId, tableSuffix);
+                if (list != null && !list.isEmpty()) {
+                    for (Map<String, Object> map : list) {
+                        String behaviorDesc = (String) map.get("behaviorDesc");
+                        Long count = ((Number) map.get("count")).longValue();
+                        resultMap.merge(behaviorDesc, count, Long::sum);
+                    }
+                }
+            } catch (Exception e) {
+                log.error("统计直播行为失败: liveId={}, tableSuffix={}", liveId, tableSuffix, e);
+            }
+        }
+
+        return resultMap;
+    }
+
+    /**
+     * 根据用户ID统计行为数量
+     *
+     * @param userId 用户ID
+     * @return 统计结果
+     */
+    @Override
+    public Map<String, Long> statisticsByUserId(Long userId) {
+        Map<String, Long> resultMap = new HashMap<>();
+        String tableSuffix = LiveBehaviorTrackUtil.getTableSuffix(userId);
+
+        try {
+            List<Map<String, Object>> list = liveUserBehaviorTrackMapper.statisticsByUserId(userId, tableSuffix);
+            if (list != null && !list.isEmpty()) {
+                for (Map<String, Object> map : list) {
+                    String behaviorDesc = (String) map.get("behaviorDesc");
+                    Long count = ((Number) map.get("count")).longValue();
+                    resultMap.put(behaviorDesc, count);
+                }
+            }
+        } catch (Exception e) {
+            log.error("统计用户行为失败: userId={}", userId, e);
+        }
+
+        return resultMap;
+    }
+
+    /**
+     * 根据直播ID统计各类行为数量(指定分表)
+     *
+     * @param liveId 直播ID
+     * @param tableSuffix 表后缀
+     * @return 统计结果
+     */
+    @Override
+    public List<Map<String, Object>> statisticsByLiveIdWithTable(Long liveId, String tableSuffix) {
+        return liveUserBehaviorTrackMapper.statisticsByLiveId(liveId, tableSuffix);
+    }
+}

+ 293 - 0
fs-service/src/main/java/com/fs/live/util/LiveBehaviorTrackUtil.java

@@ -0,0 +1,293 @@
+package com.fs.live.util;
+
+import com.alibaba.fastjson.JSON;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.common.utils.ip.IpUtils;
+import com.fs.live.domain.LiveUserBehaviorTrack;
+import com.fs.live.enums.LiveBehaviorTypeEnum;
+import com.fs.live.enums.LiveBehaviorResourceTypeEnum;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 直播用户行为轨迹埋点工具类
+ * 提供统一的行为记录方法,支持数据量大的情况下根据userId尾数进行分表
+ *
+ * @author xw
+ * @date 2026-01-05
+ */
+@Slf4j
+@Component
+public class LiveBehaviorTrackUtil {
+
+    private static final int TABLE_COUNT = 10;
+
+    public static String getTableSuffix(Long userId) {
+        if (userId == null) {
+            return "0";
+        }
+        int suffix = (int) (userId % TABLE_COUNT);
+        return String.valueOf(suffix);
+    }
+
+    /**
+     * 构建行为轨迹记录对象
+     *
+     * @param userId 用户ID
+     * @param liveId 直播间ID
+     * @param behaviorType 行为类型
+     * @return LiveUserBehaviorTrack
+     */
+    public static LiveUserBehaviorTrack buildBehaviorTrack(Long userId, Long liveId, LiveBehaviorTypeEnum behaviorType) {
+        return buildBehaviorTrack(userId, liveId, null, null, behaviorType, null, null, null);
+    }
+
+    /**
+     * 构建行为轨迹记录对象(带资源信息)
+     *
+     * @param userId 用户ID
+     * @param liveId 直播间ID
+     * @param behaviorType 行为类型
+     * @param resourceId 资源ID
+     * @param resourceType 资源类型
+     * @return LiveUserBehaviorTrack
+     */
+    public static LiveUserBehaviorTrack buildBehaviorTrack(Long userId, Long liveId,
+                                                           LiveBehaviorTypeEnum behaviorType,
+                                                           Long resourceId,
+                                                           LiveBehaviorResourceTypeEnum resourceType) {
+        return buildBehaviorTrack(userId, liveId, null, null, behaviorType, resourceId, resourceType, null);
+    }
+
+    /**
+     * 构建行为轨迹记录对象(完整版)
+     *
+     * @param userId 用户ID
+     * @param liveId 直播间ID
+     * @param companyId 企业ID
+     * @param companyUserId 企业用户ID(销售ID)
+     * @param behaviorType 行为类型
+     * @param resourceId 资源ID
+     * @param resourceType 资源类型
+     * @param extData 扩展数据
+     * @return LiveUserBehaviorTrack
+     */
+    public static LiveUserBehaviorTrack buildBehaviorTrack(Long userId, Long liveId,
+                                                           Long companyId, Long companyUserId,
+                                                           LiveBehaviorTypeEnum behaviorType,
+                                                           Long resourceId,
+                                                           LiveBehaviorResourceTypeEnum resourceType,
+                                                           Map<String, Object> extData) {
+        LiveUserBehaviorTrack track = new LiveUserBehaviorTrack();
+        track.setUserId(userId);
+        track.setLiveId(liveId);
+        track.setCompanyId(companyId);
+        track.setCompanyUserId(companyUserId);
+        track.setBehaviorType(behaviorType.getCode());
+        track.setBehaviorDesc(behaviorType.getDesc());
+        track.setResourceId(resourceId);
+        if (resourceType != null) {
+            track.setResourceType(resourceType.getCode());
+        }
+        if (extData != null && !extData.isEmpty()) {
+            track.setExtData(JSON.toJSONString(extData));
+        }
+        track.setBehaviorTime(System.currentTimeMillis());
+
+        // 设置分表后缀
+        track.setTableSuffix(getTableSuffix(userId));
+
+        // 自动获取请求信息
+        try {
+            HttpServletRequest request = ServletUtils.getRequest();
+            if (request != null) {
+                track.setIpAddress(IpUtils.getIpAddr(request));
+                track.setUserAgent(request.getHeader("User-Agent"));
+                // 可以从User-Agent解析设备信息
+                track.setDeviceInfo(parseDeviceInfo(request.getHeader("User-Agent")));
+            } else {
+                // WebSocket环境下无法获取HTTP请求上下文,这是正常现象
+                log.debug("当前非HTTP请求上下文(可能是WebSocket),无法获取请求信息");
+                track.setDeviceInfo("Unknown");
+            }
+        } catch (Exception e) {
+            log.debug("获取请求信息失败", e);
+            track.setDeviceInfo("Unknown");
+        }
+
+        return track;
+    }
+
+    /**
+     * 从User-Agent解析简单的设备信息
+     */
+    private static String parseDeviceInfo(String userAgent) {
+        if (StringUtils.isEmpty(userAgent)) {
+            return "Unknown";
+        }
+        if (userAgent.contains("MicroMessenger")) {
+            return "WeChat";
+        } else if (userAgent.contains("Android")) {
+            return "Android";
+        } else if (userAgent.contains("iPhone") || userAgent.contains("iPad")) {
+            return "iOS";
+        } else if (userAgent.contains("Windows")) {
+            return "Windows";
+        } else if (userAgent.contains("Mac")) {
+            return "Mac";
+        }
+        return "Other";
+    }
+
+    /**
+     * 快速记录行为 - 简单版(仅行为类型)
+     *
+     * @param userId 用户ID
+     * @param liveId 直播间ID
+     * @param behaviorType 行为类型
+     */
+    public static LiveUserBehaviorTrack quickTrack(Long userId, Long liveId, LiveBehaviorTypeEnum behaviorType) {
+        return buildBehaviorTrack(userId, liveId, behaviorType);
+    }
+
+    /**
+     * 快速记录行为 - 带资源信息
+     *
+     * @param userId 用户ID
+     * @param liveId 直播间ID
+     * @param behaviorType 行为类型
+     * @param resourceId 资源ID
+     * @param resourceType 资源类型
+     */
+    public static LiveUserBehaviorTrack quickTrack(Long userId, Long liveId,
+                                                   LiveBehaviorTypeEnum behaviorType,
+                                                   Long resourceId,
+                                                   LiveBehaviorResourceTypeEnum resourceType) {
+        return buildBehaviorTrack(userId, liveId, behaviorType, resourceId, resourceType);
+    }
+
+    /**
+     * 记录进入直播间
+     */
+    public static LiveUserBehaviorTrack trackEnterLive(Long userId, Long liveId) {
+        return quickTrack(userId, liveId, LiveBehaviorTypeEnum.ENTER_LIVE);
+    }
+
+    /**
+     * 记录退出直播间
+     */
+    public static LiveUserBehaviorTrack trackLeaveLive(Long userId, Long liveId) {
+        return quickTrack(userId, liveId, LiveBehaviorTypeEnum.LEAVE_LIVE);
+    }
+
+    /**
+     * 记录点赞
+     */
+    public static LiveUserBehaviorTrack trackLike(Long userId, Long liveId) {
+        return quickTrack(userId, liveId, LiveBehaviorTypeEnum.LIKE);
+    }
+
+    /**
+     * 记录发送消息
+     */
+    public static LiveUserBehaviorTrack trackSendMessage(Long userId, Long liveId, Long messageId) {
+        return quickTrack(userId, liveId, LiveBehaviorTypeEnum.SEND_MESSAGE,
+                         messageId, LiveBehaviorResourceTypeEnum.MESSAGE);
+    }
+
+    /**
+     * 记录点击商品
+     */
+    public static LiveUserBehaviorTrack trackClickGoods(Long userId, Long liveId, Long goodsId) {
+        return quickTrack(userId, liveId, LiveBehaviorTypeEnum.CLICK_GOODS,
+                         goodsId, LiveBehaviorResourceTypeEnum.GOODS);
+    }
+
+    /**
+     * 记录加入购物车
+     */
+    public static LiveUserBehaviorTrack trackAddCart(Long userId, Long liveId, Long goodsId) {
+        return quickTrack(userId, liveId, LiveBehaviorTypeEnum.ADD_CART,
+                         goodsId, LiveBehaviorResourceTypeEnum.GOODS);
+    }
+
+    /**
+     * 记录购买商品
+     */
+    public static LiveUserBehaviorTrack trackPurchase(Long userId, Long liveId, Long orderId, Map<String, Object> orderInfo) {
+        return buildBehaviorTrack(userId, liveId, null, null,
+                                 LiveBehaviorTypeEnum.PURCHASE,
+                                 orderId, LiveBehaviorResourceTypeEnum.ORDER, orderInfo);
+    }
+
+    /**
+     * 记录收藏商品
+     */
+    public static LiveUserBehaviorTrack trackCollectGoods(Long userId, Long liveId, Long goodsId) {
+        return quickTrack(userId, liveId, LiveBehaviorTypeEnum.COLLECT_GOODS,
+                         goodsId, LiveBehaviorResourceTypeEnum.GOODS);
+    }
+
+    /**
+     * 记录关注主播
+     */
+    public static LiveUserBehaviorTrack trackFollowAnchor(Long userId, Long liveId, Long anchorId) {
+        return quickTrack(userId, liveId, LiveBehaviorTypeEnum.FOLLOW_ANCHOR,
+                         anchorId, LiveBehaviorResourceTypeEnum.ANCHOR);
+    }
+
+    /**
+     * 记录领取优惠券
+     */
+    public static LiveUserBehaviorTrack trackReceiveCoupon(Long userId, Long liveId, Long couponId) {
+        return quickTrack(userId, liveId, LiveBehaviorTypeEnum.RECEIVE_COUPON,
+                         couponId, LiveBehaviorResourceTypeEnum.COUPON);
+    }
+
+    /**
+     * 记录参与抽奖
+     */
+    public static LiveUserBehaviorTrack trackJoinLottery(Long userId, Long liveId, Long lotteryId) {
+        return quickTrack(userId, liveId, LiveBehaviorTypeEnum.JOIN_LOTTERY,
+                         lotteryId, LiveBehaviorResourceTypeEnum.LOTTERY);
+    }
+
+    /**
+     * 记录领取红包
+     */
+    public static LiveUserBehaviorTrack trackReceiveRedPacket(Long userId, Long liveId, Long redPacketId) {
+        return quickTrack(userId, liveId, LiveBehaviorTypeEnum.RECEIVE_RED_PACKET,
+                         redPacketId, LiveBehaviorResourceTypeEnum.RED_PACKET);
+    }
+
+    /**
+     * 记录分享直播间
+     */
+    public static LiveUserBehaviorTrack trackShareLive(Long userId, Long liveId) {
+        return quickTrack(userId, liveId, LiveBehaviorTypeEnum.SHARE_LIVE);
+    }
+
+    /**
+     * 记录观看时长
+     */
+    public static LiveUserBehaviorTrack trackWatchDuration(Long userId, Long liveId, Long duration) {
+        Map<String, Object> extData = new HashMap<>();
+        extData.put("duration", duration);
+        return buildBehaviorTrack(userId, liveId, null, null,
+                                 LiveBehaviorTypeEnum.WATCH_DURATION,
+                                 null, null, extData);
+    }
+
+    /**
+     * 记录完课
+     */
+    public static LiveUserBehaviorTrack trackCompletion(Long userId, Long liveId) {
+        return quickTrack(userId, liveId, LiveBehaviorTypeEnum.COMPLETION);
+    }
+}

+ 166 - 0
fs-service/src/main/resources/mapper/live/LiveUserBehaviorTrackMapper.xml

@@ -0,0 +1,166 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.live.mapper.LiveUserBehaviorTrackMapper">
+
+    <resultMap type="com.fs.live.domain.LiveUserBehaviorTrack" id="LiveUserBehaviorTrackResult">
+        <result property="id"    column="id"    />
+        <result property="liveId"    column="live_id"    />
+        <result property="userId"    column="user_id"    />
+        <result property="companyId"    column="company_id"    />
+        <result property="companyUserId"    column="company_user_id"    />
+        <result property="behaviorType"    column="behavior_type"    />
+        <result property="behaviorDesc"    column="behavior_desc"    />
+        <result property="resourceId"    column="resource_id"    />
+        <result property="resourceType"    column="resource_type"    />
+        <result property="extData"    column="ext_data"    />
+        <result property="behaviorTime"    column="behavior_time"    />
+        <result property="ipAddress"    column="ip_address"    />
+        <result property="deviceInfo"    column="device_info"    />
+        <result property="userAgent"    column="user_agent"    />
+        <result property="createTime"    column="create_time"    />
+        <result property="tableSuffix"    column="table_suffix"    />
+    </resultMap>
+
+    <sql id="selectLiveUserBehaviorTrackVo">
+        select id, live_id, user_id, company_id, company_user_id, behavior_type, behavior_desc,
+               resource_id, resource_type, ext_data, behavior_time, ip_address, device_info,
+               user_agent, create_time
+        from live_user_behavior_track_${tableSuffix}
+    </sql>
+
+    <select id="selectLiveUserBehaviorTrackList" parameterType="com.fs.live.domain.LiveUserBehaviorTrack" resultMap="LiveUserBehaviorTrackResult">
+        <include refid="selectLiveUserBehaviorTrackVo"/>
+        <where>
+            <if test="liveId != null "> and live_id = #{liveId}</if>
+            <if test="userId != null "> and user_id = #{userId}</if>
+            <if test="companyId != null "> and company_id = #{companyId}</if>
+            <if test="companyUserId != null "> and company_user_id = #{companyUserId}</if>
+            <if test="behaviorType != null "> and behavior_type = #{behaviorType}</if>
+            <if test="resourceId != null "> and resource_id = #{resourceId}</if>
+            <if test="resourceType != null "> and resource_type = #{resourceType}</if>
+            <if test="params.beginTime != null and params.beginTime != ''">
+                and DATE_FORMAT(create_time,'%Y-%m-%d') &gt;= DATE_FORMAT(#{params.beginTime},'%Y-%m-%d')
+            </if>
+            <if test="params.endTime != null and params.endTime != ''">
+                and DATE_FORMAT(create_time,'%Y-%m-%d') &lt;= DATE_FORMAT(#{params.endTime},'%Y-%m-%d')
+            </if>
+        </where>
+        order by behavior_time desc, id desc
+    </select>
+
+    <select id="selectLiveUserBehaviorTrackById" parameterType="Long" resultMap="LiveUserBehaviorTrackResult">
+        <include refid="selectLiveUserBehaviorTrackVo"/>
+        where id = #{id}
+    </select>
+
+    <select id="selectByUserIdAndLiveId" resultMap="LiveUserBehaviorTrackResult">
+        <include refid="selectLiveUserBehaviorTrackVo"/>
+        where user_id = #{userId} and live_id = #{liveId}
+        order by behavior_time desc
+    </select>
+
+    <select id="statisticsByLiveId" resultType="java.util.Map">
+        select
+            behavior_type as behaviorType,
+            behavior_desc as behaviorDesc,
+            count(1) as count
+        from live_user_behavior_track_${tableSuffix}
+        where live_id = #{liveId}
+        group by behavior_type, behavior_desc
+        order by behavior_type
+    </select>
+
+    <select id="statisticsByUserId" resultType="java.util.Map">
+        select
+            behavior_type as behaviorType,
+            behavior_desc as behaviorDesc,
+            count(1) as count
+        from live_user_behavior_track_${tableSuffix}
+        where user_id = #{userId}
+        group by behavior_type, behavior_desc
+        order by behavior_type
+    </select>
+
+    <insert id="insertLiveUserBehaviorTrack" parameterType="com.fs.live.domain.LiveUserBehaviorTrack" useGeneratedKeys="true" keyProperty="id">
+        insert into live_user_behavior_track_${tableSuffix}
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="liveId != null">live_id,</if>
+            <if test="userId != null">user_id,</if>
+            <if test="companyId != null">company_id,</if>
+            <if test="companyUserId != null">company_user_id,</if>
+            <if test="behaviorType != null">behavior_type,</if>
+            <if test="behaviorDesc != null">behavior_desc,</if>
+            <if test="resourceId != null">resource_id,</if>
+            <if test="resourceType != null">resource_type,</if>
+            <if test="extData != null">ext_data,</if>
+            <if test="behaviorTime != null">behavior_time,</if>
+            <if test="ipAddress != null">ip_address,</if>
+            <if test="deviceInfo != null">device_info,</if>
+            <if test="userAgent != null">user_agent,</if>
+            <if test="createTime != null">create_time,</if>
+         </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="liveId != null">#{liveId},</if>
+            <if test="userId != null">#{userId},</if>
+            <if test="companyId != null">#{companyId},</if>
+            <if test="companyUserId != null">#{companyUserId},</if>
+            <if test="behaviorType != null">#{behaviorType},</if>
+            <if test="behaviorDesc != null">#{behaviorDesc},</if>
+            <if test="resourceId != null">#{resourceId},</if>
+            <if test="resourceType != null">#{resourceType},</if>
+            <if test="extData != null">#{extData},</if>
+            <if test="behaviorTime != null">#{behaviorTime},</if>
+            <if test="ipAddress != null">#{ipAddress},</if>
+            <if test="deviceInfo != null">#{deviceInfo},</if>
+            <if test="userAgent != null">#{userAgent},</if>
+            <if test="createTime != null">#{createTime},</if>
+         </trim>
+    </insert>
+
+    <insert id="batchInsertLiveUserBehaviorTrack" parameterType="java.util.List">
+        <foreach collection="list" item="item" separator=";">
+            insert into live_user_behavior_track_${item.tableSuffix}
+            (live_id, user_id, company_id, company_user_id, behavior_type, behavior_desc,
+             resource_id, resource_type, ext_data, behavior_time, ip_address, device_info,
+             user_agent, create_time)
+            values
+            (#{item.liveId}, #{item.userId}, #{item.companyId}, #{item.companyUserId},
+             #{item.behaviorType}, #{item.behaviorDesc}, #{item.resourceId}, #{item.resourceType},
+             #{item.extData}, #{item.behaviorTime}, #{item.ipAddress}, #{item.deviceInfo},
+             #{item.userAgent}, #{item.createTime})
+        </foreach>
+    </insert>
+
+    <update id="updateLiveUserBehaviorTrack" parameterType="com.fs.live.domain.LiveUserBehaviorTrack">
+        update live_user_behavior_track_${tableSuffix}
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="liveId != null">live_id = #{liveId},</if>
+            <if test="userId != null">user_id = #{userId},</if>
+            <if test="companyId != null">company_id = #{companyId},</if>
+            <if test="companyUserId != null">company_user_id = #{companyUserId},</if>
+            <if test="behaviorType != null">behavior_type = #{behaviorType},</if>
+            <if test="behaviorDesc != null">behavior_desc = #{behaviorDesc},</if>
+            <if test="resourceId != null">resource_id = #{resourceId},</if>
+            <if test="resourceType != null">resource_type = #{resourceType},</if>
+            <if test="extData != null">ext_data = #{extData},</if>
+            <if test="behaviorTime != null">behavior_time = #{behaviorTime},</if>
+            <if test="ipAddress != null">ip_address = #{ipAddress},</if>
+            <if test="deviceInfo != null">device_info = #{deviceInfo},</if>
+            <if test="userAgent != null">user_agent = #{userAgent},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteLiveUserBehaviorTrackById" parameterType="Long">
+        delete from live_user_behavior_track_${tableSuffix} where id = #{id}
+    </delete>
+
+    <delete id="deleteLiveUserBehaviorTrackByIds">
+        delete from live_user_behavior_track_${tableSuffix} where id in
+        <foreach item="id" collection="ids" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+</mapper>

+ 1 - 0
fs-user-app/src/main/java/com/fs/FsUserAppApplication.java

@@ -16,6 +16,7 @@ import org.springframework.transaction.annotation.EnableTransactionManagement;
 @EnableTransactionManagement
 @EnableSwaggerBootstrapUI
 @EnableScheduling
+@EnableAsync
 public class FsUserAppApplication
 {
     public static void main(String[] args)

+ 144 - 0
fs-user-app/src/main/java/com/fs/app/controller/live/LiveBehaviorTrackController.java

@@ -0,0 +1,144 @@
+package com.fs.app.controller.live;
+
+import com.fs.app.annotation.Login;
+import com.fs.app.controller.AppBaseController;
+import com.fs.common.core.domain.R;
+import com.fs.live.domain.LiveUserBehaviorTrack;
+import com.fs.live.enums.LiveBehaviorResourceTypeEnum;
+import com.fs.live.enums.LiveBehaviorTypeEnum;
+import com.fs.live.service.ILiveUserBehaviorTrackService;
+import com.fs.live.util.LiveBehaviorTrackUtil;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 直播用户行为轨迹Controller
+ * 提供行为数据查询和统计接口
+ *
+ * @author xw
+ * @date 2026-01-05
+ */
+@Api("直播用户行为轨迹接口")
+@Slf4j
+@RestController
+@RequestMapping("/app/live/behavior")
+public class LiveBehaviorTrackController extends AppBaseController {
+
+    @Autowired
+    private ILiveUserBehaviorTrackService behaviorTrackService;
+
+    /**
+     * 查询用户在某个直播间的行为轨迹
+     */
+    @Login
+    @ApiOperation("查询用户行为轨迹")
+    @GetMapping("/list")
+    public R list(@RequestParam Long liveId) {
+        Long userId = Long.parseLong(getUserId());
+        List<LiveUserBehaviorTrack> list = behaviorTrackService.selectByUserIdAndLiveId(userId, liveId);
+        return R.ok().put("data", list);
+    }
+
+    /**
+     * 统计用户的所有行为
+     */
+    @Login
+    @ApiOperation("统计用户行为")
+    @GetMapping("/statistics/user")
+    public R statisticsUser() {
+        Long userId = Long.parseLong(getUserId());
+        Map<String, Long> statistics = behaviorTrackService.statisticsByUserId(userId);
+        return R.ok().put("data", statistics);
+    }
+
+    /**
+     * 统计直播间的所有行为
+     */
+    @ApiOperation("统计直播间行为")
+    @GetMapping("/statistics/live/{liveId}")
+    public R statisticsLive(@PathVariable Long liveId) {
+        Map<String, Long> statistics = behaviorTrackService.statisticsByLiveId(liveId);
+        return R.ok().put("data", statistics);
+    }
+
+    /**
+     * 手动记录行为(通用接口,用于前端主动上报)
+     */
+    @Login
+    @ApiOperation("记录用户行为")
+    @PostMapping("/track")
+    public R track(@RequestBody Map<String, Object> params) {
+        Long userId = Long.parseLong(getUserId());
+        Long liveId = Long.parseLong(params.get("liveId").toString());
+        Integer behaviorType = Integer.parseInt(params.get("behaviorType").toString());
+
+        LiveBehaviorTypeEnum behaviorTypeEnum = LiveBehaviorTypeEnum.getByCode(behaviorType);
+        if (behaviorTypeEnum == null) {
+            return R.error("无效的行为类型");
+        }
+
+        // 构建行为轨迹
+        LiveUserBehaviorTrack track = LiveBehaviorTrackUtil.buildBehaviorTrack(
+            userId, liveId, behaviorTypeEnum
+        );
+
+        // 如果有资源ID和资源类型
+        if (params.containsKey("resourceId") && params.get("resourceId") != null) {
+            track.setResourceId(Long.parseLong(params.get("resourceId").toString()));
+        }
+        if (params.containsKey("resourceType") && params.get("resourceType") != null) {
+            track.setResourceType(Integer.parseInt(params.get("resourceType").toString()));
+        }
+
+        // 异步保存
+        behaviorTrackService.asyncInsertLiveUserBehaviorTrack(track);
+
+        return R.ok("行为记录成功");
+    }
+
+    /**
+     * 批量记录行为(用于前端批量上报)
+     */
+    @Login
+    @ApiOperation("批量记录用户行为")
+    @PostMapping("/track/batch")
+    public R trackBatch(@RequestBody List<Map<String, Object>> paramsList) {
+        Long userId = Long.parseLong(getUserId());
+
+        for (Map<String, Object> params : paramsList) {
+            try {
+                Long liveId = Long.parseLong(params.get("liveId").toString());
+                Integer behaviorType = Integer.parseInt(params.get("behaviorType").toString());
+
+                LiveBehaviorTypeEnum behaviorTypeEnum = LiveBehaviorTypeEnum.getByCode(behaviorType);
+                if (behaviorTypeEnum == null) {
+                    continue;
+                }
+
+                LiveUserBehaviorTrack track = LiveBehaviorTrackUtil.buildBehaviorTrack(
+                    userId, liveId, behaviorTypeEnum
+                );
+
+                if (params.containsKey("resourceId") && params.get("resourceId") != null) {
+                    track.setResourceId(Long.parseLong(params.get("resourceId").toString()));
+                }
+                if (params.containsKey("resourceType") && params.get("resourceType") != null) {
+                    track.setResourceType(Integer.parseInt(params.get("resourceType").toString()));
+                }
+
+                behaviorTrackService.asyncInsertLiveUserBehaviorTrack(track);
+            } catch (Exception e) {
+                log.error("批量记录行为失败", e);
+            }
+        }
+
+        return R.ok("批量记录成功");
+    }
+}

+ 23 - 2
fs-user-app/src/main/java/com/fs/app/controller/live/LiveDataController.java

@@ -3,14 +3,18 @@ package com.fs.app.controller.live;
 import com.fs.app.annotation.Login;
 import com.fs.app.controller.AppBaseController;
 import com.fs.common.core.domain.R;
+import com.fs.common.utils.StringUtils;
 import com.fs.his.domain.FsUser;
 import com.fs.his.service.IFsUserService;
 import com.fs.live.domain.LiveData;
+import com.fs.live.domain.LiveUserBehaviorTrack;
 import com.fs.live.service.ILiveDataService;
+import com.fs.live.service.ILiveUserBehaviorTrackService;
 import com.fs.live.service.ILiveWatchUserService;
+import com.fs.live.util.LiveBehaviorTrackUtil;
 import com.fs.live.vo.HisFsUserVO;
 import com.fs.live.vo.LiveWatchUserVO;
-import com.hc.openapi.tool.util.StringUtils;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 import springfox.documentation.annotations.ApiIgnore;
@@ -19,6 +23,7 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
+@Slf4j
 @RestController
 @RequestMapping("/app/live/liveData")
 public class LiveDataController extends AppBaseController {
@@ -31,6 +36,9 @@ public class LiveDataController extends AppBaseController {
 
     @Autowired
     IFsUserService fsUserService;
+
+    @Autowired
+    private ILiveUserBehaviorTrackService behaviorTrackService;
     /**
      * 查询直播数据列表
      * */
@@ -56,7 +64,20 @@ public class LiveDataController extends AppBaseController {
     @GetMapping("/like/{liveId}")
     @ApiIgnore("直播端点赞接口")
     public R like(@PathVariable("liveId") Long liveId) {
-        return liveDataService.updateLikeByLiveId(liveId,  Long.parseLong(getUserId()));
+        Long userId = Long.parseLong(getUserId());
+        
+        // 埋点:记录点赞行为
+        if (liveId != null && userId != null) {
+            try {
+                LiveUserBehaviorTrack track = LiveBehaviorTrackUtil.trackLike(userId, liveId);
+                behaviorTrackService.asyncInsertLiveUserBehaviorTrack(track);
+            } catch (Exception e) {
+                log.error("[埋点] 记录点赞行为失败, liveId={}, userId={}",
+                        liveId, userId, e);
+            }
+        }
+        
+        return liveDataService.updateLikeByLiveId(liveId, userId);
     }
 
     /**

+ 23 - 1
fs-user-app/src/main/java/com/fs/app/controller/live/LiveGoodsController.java

@@ -19,9 +19,14 @@ import com.fs.hisStore.service.IFsStoreProductAttrValueScrmService;
 import com.fs.hisStore.service.IFsStoreProductRelationScrmService;
 import com.fs.hisStore.service.IFsStoreProductScrmService;
 import com.fs.live.domain.LiveGoods;
+import com.fs.live.domain.LiveUserBehaviorTrack;
 import com.fs.live.service.ILiveGoodsService;
+import com.fs.live.service.ILiveUserBehaviorTrackService;
+import com.fs.live.util.LiveBehaviorTrackUtil;
+import com.fs.live.enums.LiveBehaviorTypeEnum;
 import com.github.pagehelper.PageHelper;
 import com.github.pagehelper.PageInfo;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 
@@ -37,6 +42,7 @@ import java.util.concurrent.TimeUnit;
  * @author fs
  * @date 2025-07-08
  */
+@Slf4j
 @RestController
 @RequestMapping("/app/live/liveGoods")
 public class LiveGoodsController extends AppBaseController
@@ -49,6 +55,9 @@ public class LiveGoodsController extends AppBaseController
 
     @Autowired
     private RedisCache redisCache;
+    
+    @Autowired
+    private ILiveUserBehaviorTrackService behaviorTrackService;
     @Autowired
     private IFsStoreProductAttrScrmService attrService;
     @Autowired
@@ -136,7 +145,7 @@ public class LiveGoodsController extends AppBaseController
      *商品详情
      */
     @GetMapping("/liveGoodsDetail/{productId}")
-    public R liveGoodsDetail(@PathVariable Long productId)
+    public R liveGoodsDetail(@PathVariable Long productId, @RequestParam(required = false) Long liveId)
     {
         // 先从缓存中获取商品详情
         String cacheKey = String.format(LiveKeysConstant.PRODUCT_DETAIL_CACHE, productId);
@@ -194,6 +203,19 @@ public class LiveGoodsController extends AppBaseController
                 productRelationService.insertFsStoreProductRelation(relation);
             }
         }
+        
+        // 埋点:记录点击商品行为
+        if (liveId != null && userId != null) {
+            try {
+                LiveUserBehaviorTrack track = LiveBehaviorTrackUtil.trackClickGoods(
+                    Long.parseLong(userId), liveId, productId);
+                behaviorTrackService.asyncInsertLiveUserBehaviorTrack(track);
+            } catch (Exception e) {
+                log.error("[埋点] 记录点击商品行为失败, liveId={}, userId={}, productId={}", 
+                    liveId, userId, productId, e);
+            }
+        }
+        
         return R.ok().put("product",product).put("productAttr",productAttr).put("productValues",productValues);
     }
 

+ 45 - 1
fs-user-app/src/main/java/com/fs/app/controller/live/LiveOrderController.java

@@ -45,6 +45,7 @@ import com.fs.huifuPay.sdk.opps.core.request.V2TradePaymentScanpayQueryRequest;
 import com.fs.huifuPay.service.HuiFuService;
 import com.fs.live.domain.LiveOrder;
 import com.fs.live.domain.LiveOrderPayment;
+import com.fs.live.domain.LiveUserBehaviorTrack;
 import com.fs.live.dto.LiveOrderComputeDTO;
 import com.fs.live.enums.LiveOrderCancleReason;
 import com.fs.live.mapper.LiveOrderPaymentMapper;
@@ -52,6 +53,8 @@ import com.fs.live.param.*;
 import com.fs.live.service.ILiveAfterSalesService;
 import com.fs.live.service.ILiveOrderItemService;
 import com.fs.live.service.ILiveOrderService;
+import com.fs.live.service.ILiveUserBehaviorTrackService;
+import com.fs.live.util.LiveBehaviorTrackUtil;
 import com.fs.live.vo.FsMyLiveOrderListQueryVO;
 import com.fs.live.vo.LiveOrderItemListUVO;
 import com.fs.live.vo.LiveOrderListVo;
@@ -131,6 +134,9 @@ public class LiveOrderController extends AppBaseController
     @Autowired
     private HuiFuService huiFuService;
 
+    @Autowired
+    private ILiveUserBehaviorTrackService behaviorTrackService;
+
 
 
 
@@ -320,7 +326,45 @@ public class LiveOrderController extends AppBaseController
         String userId= getUserId();
         log.info("开始创建订单,登录用户id:{}", userId);
         param.setUserId(userId);
-        return orderService.createLiveOrder(param);
+        R result = orderService.createLiveOrder(param);
+
+        // 埋点:记录购买商品行为
+        if (result.get("code").equals(200) && param.getLiveId() != null) {
+            try {
+                // 从返回结果中获取订单对象
+                Object orderObj = result.get("order");
+                Long orderId = null;
+                if (orderObj != null && orderObj instanceof LiveOrder) {
+                    LiveOrder order = (LiveOrder) orderObj;
+                    orderId = order.getOrderId();
+                }
+
+                // 构建订单扩展信息
+                Map<String, Object> orderInfo = new HashMap<>();
+                if (param.getProductId() != null) {
+                    orderInfo.put("productId", param.getProductId());
+                }
+                if (param.getProductNum() != null) {
+                    orderInfo.put("productNum", param.getProductNum());
+                }
+                if (param.getPayPrice() != null) {
+                    orderInfo.put("payPrice", param.getPayPrice());
+                }
+                // 添加订单详细信息
+                if (orderObj != null) {
+                    orderInfo.put("orderData", orderObj);
+                }
+
+                LiveUserBehaviorTrack track = LiveBehaviorTrackUtil.trackPurchase(
+                    Long.parseLong(userId), param.getLiveId(), orderId, orderInfo);
+                behaviorTrackService.asyncInsertLiveUserBehaviorTrack(track);
+            } catch (Exception e) {
+                log.error("[埋点] 记录购买商品行为失败, liveId={}, userId={}",
+                    param.getLiveId(), userId, e);
+            }
+        }
+
+        return result;
     }
 
     @Login

+ 2 - 0
fs-user-app/src/main/java/com/fs/framework/config/ThreadPoolConfig.java

@@ -4,6 +4,7 @@ import com.fs.common.utils.Threads;
 import org.apache.commons.lang3.concurrent.BasicThreadFactory;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.annotation.EnableAsync;
 import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
 
 import java.util.concurrent.ScheduledExecutorService;
@@ -16,6 +17,7 @@ import java.util.concurrent.ThreadPoolExecutor;
 
  **/
 @Configuration
+@EnableAsync
 public class ThreadPoolConfig
 {
     // 核心线程池大小