zxl
2025-06-26 b305b6ce82fbb975acf42af3ad450aa4c7699d5e
客户分析,和浏览视频记录
8个文件已修改
3个文件已添加
345 ■■■■■ 已修改文件
framework/src/main/java/cn/lili/modules/lmk/domain/query/FootPrintQuery.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
framework/src/main/java/cn/lili/modules/lmk/domain/vo/VideoFootInfoVo.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
framework/src/main/java/cn/lili/modules/lmk/domain/vo/VideoFootVO.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
framework/src/main/java/cn/lili/modules/lmk/domain/vo/VideoVO.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
framework/src/main/java/cn/lili/modules/lmk/mapper/VideoMapper.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
framework/src/main/java/cn/lili/modules/lmk/service/impl/CustomerServiceImpl.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
framework/src/main/java/cn/lili/modules/member/service/FootprintService.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
framework/src/main/java/cn/lili/modules/member/serviceimpl/FootprintServiceImpl.java 83 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
framework/src/main/resources/mapper/lmk/CustomerMapper.xml 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
framework/src/main/resources/mapper/lmk/VideoMapper.xml 106 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
manager-api/src/main/java/cn/lili/controller/lmk/CustomerController.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
framework/src/main/java/cn/lili/modules/lmk/domain/query/FootPrintQuery.java
New file
@@ -0,0 +1,22 @@
package cn.lili.modules.lmk.domain.query;
import cn.lili.base.AbsQuery;
import io.swagger.annotations.ApiModel;
import lombok.Data;
/**
 * lmk-shop-java
 * 视频浏览足迹查询对象
 *
 * @author : zxl
 * @date : 2025-06-25 14:15
 **/
@Data
@ApiModel(value = "视频浏览足迹查询对象", description = "视频浏览足迹查询对象")
public class FootPrintQuery extends AbsQuery {
    /**
     * 会员id
     */
    private String memberId;
}
framework/src/main/java/cn/lili/modules/lmk/domain/vo/VideoFootInfoVo.java
New file
@@ -0,0 +1,34 @@
package cn.lili.modules.lmk.domain.vo;
import io.swagger.annotations.ApiModel;
import lombok.Data;
/**
 * lmk-shop-java
 *
 * @author : zxl
 * @date : 2025-06-25 15:56
 **/
@Data
@ApiModel(value = "会员浏览视频汇总信息", description = "会员浏览视频汇总信息")
public class VideoFootInfoVo {
    /**
     * 总播放视频时间(毫秒)
     */
    private Double totalDuration;
    /**
     * 总浏览视频数
     */
    private Long VideoCount;
    /**
     * 平均完播率
     */
    private Double avgCompletionRate;
}
framework/src/main/java/cn/lili/modules/lmk/domain/vo/VideoFootVO.java
New file
@@ -0,0 +1,31 @@
package cn.lili.modules.lmk.domain.vo;
import io.swagger.annotations.ApiModel;
import lombok.Data;
/**
 * lmk-shop-java
 * 会员浏览视频足迹
 *
 * @author : zxl
 * @date : 2025-06-25 14:00
 **/
@Data
@ApiModel(value = "会员浏览视频足迹", description = "会员浏览视频足迹")
public class VideoFootVO extends VideoVO{
    /**
     * 观看时长(毫秒)
     */
    private Long viewDuration;
    /**
     * 视频播放至(秒)
     */
    private String playAt;
    private Double playProgress;
    private String coverCOSUrl;
}
framework/src/main/java/cn/lili/modules/lmk/domain/vo/VideoVO.java
@@ -97,6 +97,8 @@
    @ApiModelProperty("审核通过时间")
    private Date auditPassTime;
    public static VideoVO getVoByEntity(@NonNull Video entity, VideoVO vo) {
        if(vo == null) {
            vo = new VideoVO();
framework/src/main/java/cn/lili/modules/lmk/mapper/VideoMapper.java
@@ -130,4 +130,21 @@
     * @return
     */
    List<VideoGoodsDetailVO> getVideoGoods(@Param("id") String videoId);
    /**
     * 查询用户视频浏览足迹
     * @param page
     * @param query
     * @return
     */
    IPage videoFootPage(IPage page, FootPrintQuery query);
    /**
     * 获得用户浏览视频足迹汇总数据
     * @param id
     * @return
     */
    VideoFootInfoVo getVideoFootInfo(String id);
    List<VideoFootVO> videoFoot(String id);
}
framework/src/main/java/cn/lili/modules/lmk/service/impl/CustomerServiceImpl.java
@@ -47,15 +47,12 @@
            return Result.error("该账号没有注册店铺");
        }
        return getMemberPage(customerQuery);
    }
    @Override
    public Result getMember(String id) {
        MemberVO memberVO = memberService.getMember(id);
        //查询用户标签
//        memberVO.setCustomerTagList();
        return Result.ok().data(memberVO);
    }
framework/src/main/java/cn/lili/modules/member/service/FootprintService.java
@@ -1,6 +1,10 @@
package cn.lili.modules.member.service;
import cn.lili.base.Result;
import cn.lili.common.vo.PageVO;
import cn.lili.modules.lmk.domain.query.FootPrintQuery;
import cn.lili.modules.lmk.domain.vo.VideoFootVO;
import cn.lili.modules.lmk.domain.vo.VideoVO;
import cn.lili.modules.member.entity.dos.FootPrint;
import cn.lili.modules.member.entity.dto.FootPrintQueryParams;
import cn.lili.modules.search.entity.dos.EsGoodsIndex;
@@ -49,6 +53,21 @@
    IPage<EsGoodsIndex> footPrintPage(FootPrintQueryParams params);
    /**
     * 获取会员视频浏览历史分页
     *
     * @param params 分页
     * @return 会员浏览历史列表
     */
    Result videoFootPrintPage(FootPrintQuery params);
    /**
     * 获取会员行为分析
     * @param id 会员i
     * @return
     */
    Result memberActionAnalyse(String id);
    /**
     * 获取当前会员的浏览记录数量
     *
     * @return 当前会员的浏览记录数量
framework/src/main/java/cn/lili/modules/member/serviceimpl/FootprintServiceImpl.java
@@ -1,26 +1,34 @@
package cn.lili.modules.member.serviceimpl;
import cn.lili.base.Result;
import cn.lili.common.security.context.UserContext;
import cn.lili.modules.goods.entity.dos.GoodsSku;
import cn.lili.modules.goods.service.GoodsSkuService;
import cn.lili.modules.lmk.domain.query.FootPrintQuery;
import cn.lili.modules.lmk.domain.vo.SimpleVideoTagVO;
import cn.lili.modules.lmk.domain.vo.VideoFootInfoVo;
import cn.lili.modules.lmk.domain.vo.VideoFootVO;
import cn.lili.modules.lmk.domain.vo.VideoVO;
import cn.lili.modules.lmk.mapper.VideoMapper;
import cn.lili.modules.member.entity.dos.FootPrint;
import cn.lili.modules.member.entity.dto.FootPrintQueryParams;
import cn.lili.modules.member.mapper.FootprintMapper;
import cn.lili.modules.member.service.FootprintService;
import cn.lili.modules.search.entity.dos.EsGoodsIndex;
import cn.lili.mybatis.util.PageUtil;
import cn.lili.utils.COSUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
@@ -36,6 +44,12 @@
    @Autowired
    private GoodsSkuService goodsSkuService;
    @Autowired
    private VideoMapper videoMapper;
    @Autowired
    private COSUtil cOSUtil;
    @Override
    public FootPrint saveFootprint(FootPrint footPrint) {
@@ -64,7 +78,6 @@
        Page<FootPrint> footPrintPages = this.page(PageUtil.initPage(params), params.queryWrapper());
        //定义结果
        Page<EsGoodsIndex> esGoodsIndexIPage = new Page<>();
        if (footPrintPages.getRecords() != null && !footPrintPages.getRecords().isEmpty()) {
            List<String> skuIds = footPrintPages.getRecords().stream().map(FootPrint::getSkuId).collect(Collectors.toList());
            List<GoodsSku> goodsSkuByIdFromCache = goodsSkuService.getGoodsSkuByIdFromCache(skuIds);
@@ -92,10 +105,70 @@
    }
    @Override
    public Result videoFootPrintPage(FootPrintQuery query) {
        IPage<VideoFootVO> page = cn.lili.utils.PageUtil.getPage(query,VideoFootVO.class);
        videoMapper.videoFootPage(page,query);
        VideoFootInfoVo videoFootInfoVo = videoMapper.getVideoFootInfo(query.getMemberId());
        for (VideoFootVO videoFootVO : page.getRecords()) {
            videoFootVO.setCoverCOSUrl(cOSUtil.getPreviewUrl(videoFootVO.getCoverUrl()));
            //计算播放进度
            if(videoFootVO.getPlayAt() == null || videoFootVO.getPlayAt().isEmpty()){
                videoFootVO.setPlayProgress(0.0);
                continue;
            }
            BigDecimal value = BigDecimal.valueOf(Double.parseDouble(videoFootVO.getPlayAt()) / videoFootVO.getVideoDuration())
                    .setScale(2, RoundingMode.HALF_UP);
            if (value.compareTo(BigDecimal.ONE) > 0) {
                // 这里可以添加处理逻辑,比如:自动修正为1.0
                value = BigDecimal.ONE;
            }
            videoFootVO.setPlayProgress(value.doubleValue());
        }
        HashMap<String,Object> map = new HashMap<>();
        map.put("data", page.getRecords());
        map.put("total", page.getTotal());
        if (videoFootInfoVo == null) {
            map.put("avgProgress",0);
            map.put("totalDuration",0);
        }else {
            map.put("avgProgress",videoFootInfoVo.getAvgCompletionRate());
            map.put("totalDuration",videoFootInfoVo.getTotalDuration());
        }
        return Result.ok().data(map);
    }
    @Override
    public Result memberActionAnalyse(String id){
        //视频分类
        List<VideoFootVO> list = videoMapper.videoFoot(id);
        Map<String, Long> tagCountMap = list.stream()
                .flatMap(video -> video.getTagList().stream()) // 展开所有 tag
                .filter(Objects::nonNull)                      // 过滤 null
                .collect(Collectors.groupingBy(
                        SimpleVideoTagVO::getTagName,          // 按 tagName 分组
                        Collectors.counting()                  // 计算每个 tagName 出现的次数
                ));
        return Result.ok().data(tagCountMap);
    }
    @Override
    public long getFootprintNum() {
        LambdaQueryWrapper<FootPrint> lambdaQueryWrapper = Wrappers.lambdaQuery();
        lambdaQueryWrapper.eq(FootPrint::getMemberId, Objects.requireNonNull(UserContext.getCurrentUser()).getId());
        lambdaQueryWrapper.eq(FootPrint::getDeleteFlag, false);
        return this.count(lambdaQueryWrapper);
    }
}
framework/src/main/resources/mapper/lmk/CustomerMapper.xml
@@ -27,10 +27,10 @@
        <result property="experience" column="experience"/>
        <result property="createTime" column="create_time"/>
        <result property="blackId" column="blackId"/>
        <collection property="customerTagList"  ofType="cn.lili.modules.lmk.domain.vo.CustomerTagVO"
                    select="selectTagByMemberId"
                    column="id"
        />
<!--        <collection property="customerTagList"  ofType="cn.lili.modules.lmk.domain.vo.CustomerTagVO"-->
<!--                    select="selectTagByMemberId"-->
<!--                    column="id"-->
<!--        />-->
    </resultMap>
    <select id="getPage" resultMap="BaseResultMap">
@@ -38,7 +38,7 @@
            LM.*
        FROM
            li_member LM
        LEFT JOIN lmk_customer_black LMK ON LM.id = LMK.user_id and LMK.delete_flag = 0
        <where>
        <!-- 用户名模糊查询 -->
        <if test="query.username != null and query.username != ''">
framework/src/main/resources/mapper/lmk/VideoMapper.xml
@@ -26,6 +26,7 @@
        <collection property="goodsList" column="id" select="getVideoGoods" ofType="cn.lili.modules.lmk.domain.vo.VideoGoodsDetailVO"/>
    </resultMap>
    <resultMap id="VideoGoodsMap" type="cn.lili.modules.lmk.domain.vo.VideoGoodsDetailVO">
        <result column="goods_id" property="goodsId"/>
        <result column="goods_sku_id" property="id"/>
@@ -545,4 +546,109 @@
        <if test="query.authorId != null and query.authorId != ''">AND LV.author_id = #{query.authorId}</if>
        <if test="query.status != null and query.status != ''">AND LV.status = #{query.status}</if>
    </select>
    <resultMap id="videoFootMap" type="cn.lili.modules.lmk.domain.vo.VideoFootVO">
        <id column="id" property="id"/>
        <result column="author_id" property="authorId" />
        <result column="authorName" property="authorName" />
        <result column="cover_url" property="coverUrl" />
        <result column="video_file_key" property="videoFileKey" />
        <result column="video_fit" property="videoFit" />
        <result column="title" property="title" />
        <result column="video_duration" property="videoDuration" />
        <result column="recommend" property="recommend" />
        <result column="status" property="status" />
        <result column="play_num" property="playNum" />
        <result column="collect_num" property="collectNum" />
        <result column="comment_num" property="commentNum" />
        <result column="weight" property="weight" />
        <result column="audit_pass_time" property="auditPassTime" />
        <result column="update_time" property="updateTime" />
        <result column="video_content_type" property="videoContentType" />
        <result column="video_type" property="videoType" />
        <result column="video_imgs" property="videoImgs" />
        <result column="view_duration" property="viewDuration"/>
        <result column="play_at" property="playAt"/>
        <collection property="goodsList" column="id" select="getVideoGoods" ofType="cn.lili.modules.lmk.domain.vo.VideoGoodsDetailVO"/>
        <collection property="tagList" column="id" select="getVideoTags" ofType="cn.lili.modules.lmk.domain.vo.SimpleVideoTagVO"/>
    </resultMap>
    <select id="videoFootPage" resultMap="videoFootMap">
        SELECT
            LFP.view_duration,
            LFP.play_at,
            LV.*
        from
            li_foot_print LFP
                INNER JOIN lmk_video LV
                          ON LFP.ref_id = lV.id AND LV.delete_flag = 0
        where LFP.member_id = #{query.memberId}
          AND LFP.ref_id IS NOT NULL
          AND LFP.delete_flag = 0
          AND LFP.view_type = 'video'
    </select>
    <select id="videoFoot" resultMap="videoFootMap">
        SELECT
            LFP.view_duration,
            LFP.play_at,
            LV.*
        from
            li_foot_print LFP
                INNER JOIN lmk_video LV
                           ON LFP.ref_id = lV.id AND LV.delete_flag = 0
        where LFP.member_id = #{query.memberId}
          AND LFP.ref_id IS NOT NULL
          AND LFP.delete_flag = 0
          AND LFP.view_type = 'video'
    </select>
    <resultMap id="videoFootInfo" type="cn.lili.modules.lmk.domain.vo.VideoFootInfoVo">
        <result column="total_duration" property="totalDuration"/>
        <result column="video_count" property="videoCount"/>
        <result column="avg_completion_rate" property="avgCompletionRate"/>
    </resultMap>
    <resultMap id="videoTagMap" type="cn.lili.modules.lmk.domain.vo.SimpleVideoTagVO">
        <id property="id" column="id"/>
        <result property="tagName" column="tag_name"/>
    </resultMap>
    <select id="getVideoTags" parameterType="String" resultMap="videoTagMap">
        SELECT LVT.id,
               LVT.tag_name
        from lmk_video_tag_ref LVTR
                 LEFT JOIN lmk_video_tag LVT ON LVT.id = LVTR.video_tag_id  and LVT.delete_flag = 0
        WHERE LVTR.video_id = #{id}
    </select>
    <select id="getVideoFootInfo" resultMap="videoFootInfo">
        SELECT
            SUM(LFP.view_duration) AS total_duration,
            ROUND(AVG(CASE
                          WHEN LV.video_duration > 0 THEN LFP.play_at / LV.video_duration
                          ELSE 0
                END),2) AS avg_completion_rate,
            COUNT(*) AS video_count
        from
            li_foot_print LFP
                INNER JOIN lmk_video LV
                          ON LFP.ref_id = lV.id AND LV.delete_flag = 0
        where LFP.member_id = #{id}
          AND LFP.ref_id IS NOT NULL
          AND LFP.delete_flag = 0
          AND LFP.view_type = 'video'
          AND LV.id IS NOT NULL
        GROUP BY
            LFP.member_id
    </select>
</mapper>
manager-api/src/main/java/cn/lili/controller/lmk/CustomerController.java
@@ -10,11 +10,16 @@
import cn.lili.modules.lmk.domain.form.CustomerTagRefForm;
import cn.lili.modules.lmk.domain.query.CustomerQuery;
import cn.lili.modules.lmk.domain.query.CustomerTagQuery;
import cn.lili.modules.lmk.domain.query.FootPrintQuery;
import cn.lili.modules.lmk.domain.vo.VideoFootVO;
import cn.lili.modules.lmk.service.CustomerService;
import cn.lili.modules.lmk.service.CustomerTagRefService;
import cn.lili.modules.lmk.service.CustomerTagService;
import cn.lili.modules.member.entity.dto.FootPrintQueryParams;
import cn.lili.modules.member.entity.vo.MemberSearchVO;
import cn.lili.modules.member.entity.vo.MemberVO;
import cn.lili.modules.member.service.FootprintService;
import cn.lili.modules.member.service.MemberService;
import com.baomidou.mybatisplus.core.metadata.IPage;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
@@ -31,6 +36,7 @@
    private final CustomerService customerService;
    private final CustomerTagService customerTagService;
    private final CustomerTagRefService customerTagRefService;
    private final FootprintService footprintService;
    @ApiOperation(value = "商铺下拉列表")
    @GetMapping("/store/selectOption")
@@ -105,4 +111,16 @@
        return customerTagRefService.removeById(id);
    }
    @GetMapping("/videoFootPage")
    @ApiOperation(value = "视频浏览历史分页", notes = "视频浏览历史分页")
    public Result videoFootPage(FootPrintQuery query){
        return footprintService.videoFootPrintPage(query);
    }
    @GetMapping("/memberActionAnalyse/{id}")
    @ApiOperation(value = "会员行为分析", notes = "会员行为分析")
    public Result memberActionAnalyse(@PathVariable("id") String id){
        return footprintService.memberActionAnalyse(id);
    }
}