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); } }