TH8-小视频方案

news2024/12/24 20:25:33

TH8-小视频方案

    • 说明
    • 1、我的访客
      • 1.1、dubbo服务
        • 1.1.1、实体对象
        • 1.1.2、定义接口
        • 1.1.3、编写实现
      • 1.2、记录访客数据
      • 1.3、首页谁看过我
        • 1.3.1、VO对象
        • 1.3.2、MovementController
        • 1.3.3、MovementService
    • 2、小视频功能说明
    • 3、FastDFS
      • 2.1、FastDFS是什么?
      • 2.2、工作原理
        • 2.1.3、文件的上传
        • 2.1.4、文件的下载
      • 2.3、FastDFS环境搭建
        • 2.3.1、搭建服务
        • 2.3.2、java client
          • application.yml
          • 测试
    • 4、发布小视频
      • 4.0、分析过程
        • 表结构
        • 操作步骤
      • 4.1、搭建环境
        • 4.1.1 实体类
        • 4.1.2 API接口与实现
          • 定义接口
          • 接口实现类
        • 4.1.3、controller控制器
        • 4.1.4、service业务层
      • 4.2、发布视频
        • 4.2.1、接口文档
        • 4.2.2、SmallVideosController
        • 4.2.3、SmallVideosService
        • 4.2.4、API接口
        • 4.2.5、测试问题
        • 4.2.6、配置文件解析
    • 5、小视频列表
      • 5.0、分析
      • 5.1、Controller
      • 5.2、Service
      • 5.3、API接口和实现
      • 5.4、定义Vo对象
      • 5.5、测试
        • 测试问题
        • 解决方法
    • 6、关注用户
      • 6.1、实体类对象
      • 6.2、Controller
      • 6.3、Service
      • 6.4、API和实现类
      • 6.5、修改查询视频列表
    • 7、通用缓存SpringCache
      • 7.1、重要概念
      • 7.2、入门案例
        • 7.2.1 导入依赖
        • 7.2.2 开启缓存支持
        • 7.2.3 编写UserInfoService
        • 7.2.3 缓存@Cacheable
        • 7.2.4 清除@CacheEvict
      • 7.3、视频列表缓存处理
      • 7.4、发布视频清空缓存

1. 我的动态(查询思路)
2. 圈子的互动(点赞,喜欢,评论)
	a. 数据表:quanzi_comment(commentType记录操作类型)
	b. 点赞,喜欢,评论(保存记录)
	c. 取消点赞,取消喜欢(删除记录)
	d. 为了保证效率(结合redis缓存,互动数量记录到动态表)

说明

  • 小视频功能说明
  • FastDFS环境搭建
  • 小视频的功能实现

1、我的访客

查询别人来访了我的主页的信息,其他用户在浏览我的主页时,需要记录访客数据。访客在一天内每个用户只记录一次。

查询数据时,如果用户查询过列表,就需要记录这次查询数据的时间,下次查询时查询大于等于该时间的数据。

如果,用户没有记录查询时间,就查询最近的5个来访用户。

1.1、dubbo服务

1.1.1、实体对象

@Data
@NoArgsConstructor
@AllArgsConstructor
@Document(collection = "visitors")
public class Visitors implements java.io.Serializable{

    private static final long serialVersionUID = 2811682148052386573L;

    private ObjectId id;
    private Long userId; //我的id
    private Long visitorUserId; //来访用户id
    private String from; //来源,如首页、圈子等
    private Long date; //来访时间
    private String visitDate;//来访日期
    private Double score; //得分

}

1.1.2、定义接口

package com.tanhua.dubbo.server.api;

import com.tanhua.dubbo.server.pojo.Visitors;

import java.util.List;

public interface VisitorsApi {

    /**
     * 保存访客数据
     *
     * @param userId 我的id
     * @param visitorUserId 访客id
     * @param from 来源
     * @return
     */
    String save(Long userId, Long visitorUserId, String from);

    /**
     * 查询我的访客数据,存在2种情况:
     * 1. 我没有看过我的访客数据,返回前5个访客信息
     * 2. 之前看过我的访客,从上一次查看的时间点往后查询5个访客数据
     */
    List<Visitors> queryMyVisitor(Long userId);

}

1.1.3、编写实现

package com.tanhua.dubbo.server.api;

import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.dubbo.config.annotation.Service;
import com.tanhua.dubbo.server.pojo.RecommendUser;
import com.tanhua.dubbo.server.pojo.Visitors;
import org.bson.types.ObjectId;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.redis.core.RedisTemplate;

import java.util.List;

@Service(version = "1.0.0")
public class VisitorsApiImpl implements VisitorsApi {

    @Autowired
    private MongoTemplate mongoTemplate;

    private static final String VISITOR_REDIS_KEY = "VISITOR_USER";

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @Override
    public String saveVisitor(Long userId, Long visitorUserId, String from) {
        //校验
        if (!ObjectUtil.isAllNotEmpty(userId, visitorUserId, from)) {
            return null;
        }

        //查询访客用户在今天是否已经记录过,如果已经记录过,不再记录
        String today = DateUtil.today();
        Long minDate = DateUtil.parseDateTime(today + " 00:00:00").getTime();
        Long maxDate = DateUtil.parseDateTime(today + " 23:59:59").getTime();

        Query query = Query.query(Criteria.where("userId").is(userId)
                .and("visitorUserId").is(visitorUserId)
                .andOperator(Criteria.where("date").gte(minDate),
                        Criteria.where("date").lte(maxDate)
                )
        );
        long count = this.mongoTemplate.count(query, Visitors.class);
        if (count > 0) {
            //今天已经记录过的
            return null;
        }

        Visitors visitors = new Visitors();
        visitors.setFrom(from);
        visitors.setVisitorUserId(visitorUserId);
        visitors.setUserId(userId);
        visitors.setDate(System.currentTimeMillis());
        visitors.setId(ObjectId.get());

        //存储数据
        this.mongoTemplate.save(visitors);

        return visitors.getId().toHexString();
    }


    @Override
    public List<Visitors> queryMyVisitor(Long userId) {
        // 查询前5个访客数据,按照访问时间倒序排序
        // 如果用户已经查询过列表,记录查询时间,后续查询需要按照这个时间往后查询
        // 上一次查询列表的时间
        Long date = Convert.toLong(this.redisTemplate.opsForHash().get(VISITOR_REDIS_KEY, String.valueOf(userId)));

        PageRequest pageRequest = PageRequest.of(0, 5, Sort.by(Sort.Order.desc("date")));
        Query query = Query.query(Criteria.where("userId").is(userId))
                .with(pageRequest);
        if (ObjectUtil.isNotEmpty(date)) {
            query.addCriteria(Criteria.where("date").gte(date));
        }

        List<Visitors> visitorsList = this.mongoTemplate.find(query, Visitors.class);
        //查询每个来访用户的得分
        for (Visitors visitors : visitorsList) {

            Query queryScore = Query.query(Criteria.where("toUserId")
                    .is(userId).and("userId").is(visitors.getVisitorUserId())
            );
            RecommendUser recommendUser = this.mongoTemplate.findOne(queryScore, RecommendUser.class);
            if(ObjectUtil.isNotEmpty(recommendUser)){
                visitors.setScore(recommendUser.getScore());
            }else {
                //默认得分
                visitors.setScore(90d);
            }
        }

        return visitorsList;
    }
}

1.2、记录访客数据

//com.tanhua.server.service.TanHuaService

    public TodayBest queryUserInfo(Long userId) {

        UserInfo userInfo = this.userInfoService.queryUserInfoByUserId(userId);
        if(ObjectUtil.isEmpty(userInfo)){
            return null;
        }

        TodayBest todayBest = new TodayBest();
        todayBest.setId(userId);
        todayBest.setAge(userInfo.getAge());
        todayBest.setGender(userInfo.getSex().name().toLowerCase());
        todayBest.setNickname(userInfo.getNickName());
        todayBest.setTags(Convert.toStrArray(StrUtil.split(userInfo.getTags(),',')));
        todayBest.setAvatar(userInfo.getLogo());

        //缘分值
        User user = UserThreadLocal.get();
        todayBest.setFateValue(this.recommendUserService.queryScore(userId, user.getId()).longValue());

        //记录来访用户
        this.visitorsApi.saveVisitor(userId, user.getId(), "个人主页");

        return todayBest;
    }

1.3、首页谁看过我

1.3.1、VO对象

@Data
@NoArgsConstructor
@AllArgsConstructor
public class VisitorsVo {

    private Long id; //用户id
    private String avatar;
    private String nickname;
    private String gender; //性别 man woman
    private Integer age;
    private String[] tags;
    private Long fateValue; //缘分值

    /**
     * 在vo对象中,补充一个工具方法,封装转化过程
     */
    public static VisitorsVo init(UserInfo userInfo, Visitors visitors) {
        VisitorsVo vo = new VisitorsVo();
        BeanUtils.copyProperties(userInfo,vo);
        if(userInfo.getTags() != null) {
            vo.setTags(userInfo.getTags().split(","));
        }
        vo.setFateValue(visitors.getScore().longValue());
        return vo;
    }
}

1.3.2、MovementController

    /**
     * 谁看过我
     */
    @GetMapping("visitors")
    public ResponseEntity queryVisitorsList(){
        List<VisitorsVo> list = movementService.queryVisitorsList();
        return ResponseEntity.ok(list);
    }

1.3.3、MovementService


    public List<VisitorsVo> queryVisitorsList() {
        User user = UserThreadLocal.get();
        List<Visitors> visitorsList = this.visitorsApi.queryMyVisitor(user.getId());

        if (CollUtil.isEmpty(visitorsList)) {
            return Collections.emptyList();
        }

        List<Object> userIds = CollUtil.getFieldValues(visitorsList, "visitorUserId");
        List<UserInfo> userInfoList = this.userinfoService.queryByUserIdList(userIds);

        List<VisitorsVo> visitorsVoList = new ArrayList<>();

        for (Visitors visitor : visitorsList) {
            for (UserInfo userInfo : userInfoList) {
                if (ObjectUtil.equals(visitor.getVisitorUserId(), userInfo.getUserId())) {
                    VisitorsVo visitorsVo = BeanUtil.toBeanIgnoreError(userInfo, VisitorsVo.class);
                    visitorsVo.setGender(userInfo.getSex().name().toLowerCase());
                    visitorsVo.setFateValue(visitor.getScore().intValue());
                    visitorsVoList.add(visitorsVo);
                    break;
                }
            }
        }

        return visitorsVoList;
    }

2、小视频功能说明

小视频功能类似于抖音、快手小视频的应用,用户可以上传小视频进行分享,也可以浏览查看别人分享的视频,并且可以对视频评论和点赞操作。

1、视频发布(视频:容量大,视频存储到什么位置?)

2、查询视频列表(问题:数据库表)

3、关注视频作者

4、视频播放(客户端获取视频的URL地址,自动的播放)

效果:

在这里插入图片描述

查看详情:

在这里插入图片描述

评论:

在这里插入图片描述

点赞:

在这里插入图片描述

3、FastDFS

视频存储

  • 阿里云OSS(视频简单,贵!!!)
  • 自建存储系统

对于小视频的功能的开发,核心点就是:存储 + 推荐 + 加载速度 。

  • 对于存储而言,小视频的存储量以及容量都是非常巨大的
    • 所以我们选择自己搭建分布式存储系统 FastDFS进行存储
  • 对于推荐算法,我们将采用多种权重的计算方式进行计算
  • 对于加载速度,除了提升服务器带宽外可以通过CDN的方式进行加速,当然了这需要额外购买CDN服务

2.1、FastDFS是什么?

FastDFS是分布式文件系统。使用 FastDFS很容易搭建一套高性能的文件服务器集群提供文件上传、下载等服务。

2.2、工作原理

FastDFS 架构包括 Tracker server 和 Storage server。客户端请求 Tracker server 进行文件上传、下载,通过 Tracker server 调度最终由 Storage server 完成文件上传和下载。

Tracker server 作用是负载均衡和调度,通过 Tracker server 在文件上传时可以根据一些策略找到 Storage server 提供文件上传服务。可以将 tracker 称为追踪服务器或调度服务器。

Storage server 作用是文件存储,客户端上传的文件最终存储在 Storage 服务器上,Storage server 没有实现自己的文件系统而是利用操作系统的文件系统来管理文件。可以将storage称为存储服务器。

在这里插入图片描述

每个 tracker 节点地位平等。收集 Storage 集群的状态。

Storage 分为多个组,每个组之间保存的文件是不同的。每个组内部可以有多个成员,组成员内部保存的内容是一样的,组成员的地位是一致的,没有主从的概念。

2.1.3、文件的上传

在这里插入图片描述

客户端上传文件后存储服务器将文件 ID 返回给客户端,此文件 ID 用于以后访问该文件的索引信息。文件索引信息包括:组名,虚拟磁盘路径,数据两级目录,文件名。

2.1.4、文件的下载

在这里插入图片描述

客户端下载请求到Tracker服务,Tracker返回给客户端storage的信息,客户端根据这些信息进行请求storage获取到文件。

2.3、FastDFS环境搭建

​ 企业中搭建FastDFS是一个比较繁琐和复杂的过程(多个服务器之间的配合和配置等,专业的人员搭建),但是在学习阶段。由于所有的组件全部配置到linux虚拟机,已docker运行。所以linux的内存有要求(运行的过程中,可能会出现fastdfs的容器,启动之后自动关闭,表示虚拟机内存不足,适当的扩大内存),学习环境中使用一台调度服务器,一台存储服务器

2.3.1、搭建服务

我们使用docker进行搭建。目前所有的组件全部以docker的形式配置

#进入目录
cd /root/docker-file/fastdfs/
#启动
docker-compose up -d
#查看容器
docker ps -a

在这里插入图片描述

FastDFS调度服务器地址:192.168.136.160:22122
FastDFS存储服务器地址:http://192.168.136.160:8888/

2.3.2、java client

  • 导入依赖(已经存在,被注释)
  • 在application.yml中配置Fastdfs
  • 注入FastFileStorageClient对象,完成文件上传

导入依赖:

找到tanhua-server的pom文件,打开fastdfs的依赖如下

<dependency>
    <groupId>com.github.tobato</groupId>
    <artifactId>fastdfs-client</artifactId>
    <version>1.26.7</version>
    <exclusions>
        <exclusion>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
        </exclusion>
    </exclusions>
</dependency>
application.yml
# ===================================================================
# 分布式文件系统FDFS配置
# ===================================================================
fdfs:
  so-timeout: 1500
  connect-timeout: 600
  #缩略图生成参数
  thumb-image:
    width: 150
    height: 150
  #TrackerList参数,支持多个
  tracker-list: 192.168.136.160:22122
  web-server-url: http://192.168.136.160:8888/
测试
package com.tanhua.server.test;

import com.github.tobato.fastdfs.domain.conn.FdfsWebServer;
import com.github.tobato.fastdfs.domain.fdfs.StorePath;
import com.github.tobato.fastdfs.service.FastFileStorageClient;
import com.tanhua.server.TanhuaServerApplication;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;

@RunWith(SpringRunner.class)
@SpringBootTest(classes = TanhuaServerApplication.class)
public class TestFastDFS {

    //测试将文件上传到FastDFS文件系统中

    //从调度服务器获取,一个目标存储服务器,上传
    @Autowired
    private FastFileStorageClient client;

    @Autowired
    private FdfsWebServer webServer;// 获取存储服务器的请求URL

    @Test
    public void testFileUpdate() throws FileNotFoundException {
 		//1、指定文件
        File file = new File("D:\\1.jpg");
		//2、文件上传
        StorePath path = client.uploadFile(new FileInputStream(file),
                file.length(), "jpg", null);
		//3、拼接访问路径
        String url = webServer.getWebServerUrl() + path.getFullPath();
    }
}

存储服务器:

  • 在线的存储服务器:阿里云OSS
  • 自己搭建分布式的存储服务器:fastdfs

4、发布小视频

4.0、分析过程

表结构

{
    "_id" : ObjectId("5fa60707ed0ad13fa89925cc"),
    "vid" : NumberLong(1),
    "userId" : NumberLong(1),
    "text" : "我就是我不一样的烟火~",
    "picUrl" : "https://tanhua-dev.oss-cn-zhangjiakou.aliyuncs.com/images/video/video_1.png",
    "videoUrl" : "https://tanhua-dev.oss-cn-zhangjiakou.aliyuncs.com/images/video/1576134125940400.mp4",
    "created" : NumberLong(1604716296066),
    "likeCount" : 0,
    "commentCount" : 0,
    "loveCount" : 0,
    "_class" : "com.tanhua.domain.mongo.Video"
}

在这里插入图片描述

操作步骤

在这里插入图片描述

4.1、搭建环境

4.1.1 实体类

tanhua-domain中配置实体类Video

package com.tanhua.domain.mongo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.bson.types.ObjectId;
import org.springframework.data.mongodb.core.mapping.Document;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Document(collection = "video")
public class Video implements java.io.Serializable {

    private static final long serialVersionUID = -3136732836884933873L;

    private ObjectId id; //主键id
    private Long vid; //自动增长
    private Long created; //创建时间


    private Long userId;
    private String text; //文字
    private String picUrl; //视频封面文件,URL
    private String videoUrl; //视频文件,URL


    private Integer likeCount=0; //点赞数
    private Integer commentCount=0; //评论数
    private Integer loveCount=0; //喜欢数
}

4.1.2 API接口与实现

定义接口

tanhua-dubbo-interface工程定义API接口VideoApi

package com.tanhua.dubbo.api.mongo;

import com.tanhua.domain.mongo.FollowUser;
import com.tanhua.domain.mongo.Video;
import com.tanhua.domain.vo.PageResult;

public interface VideoApi {

}

接口实现类

tanhua-dubbo-service工程定义API接口实现类VideoApiImpl

package com.tanhua.dubbo.api.mongo;

import com.tanhua.domain.mongo.FollowUser;
import com.tanhua.domain.mongo.Video;
import com.tanhua.domain.vo.PageResult;
import com.tanhua.dubbo.utils.IdService;
import org.apache.dubbo.config.annotation.Service;
import org.bson.types.ObjectId;
import org.checkerframework.checker.units.qual.C;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;

import java.util.List;

@Service
public class VideoApiImpl implements VideoApi {
    
}

4.1.3、controller控制器

tanhua-server定义SmallVideoController

package com.tanhua.server.controller;

import com.tanhua.server.service.VideoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;

@RestController
@RequestMapping("/smallVideos")
public class SmallVideoController {

    @Autowired
    private SmallVideosService videosService;
    
}

4.1.4、service业务层

tanhua-server定义SmallVideosService

package com.tanhua.server.service;

import com.github.tobato.fastdfs.domain.conn.FdfsWebServer;
import com.github.tobato.fastdfs.domain.fdfs.StorePath;
import com.github.tobato.fastdfs.service.FastFileStorageClient;
import com.tanhua.autoconfig.templates.OssTemplate;
import com.tanhua.domain.db.UserInfo;
import com.tanhua.domain.mongo.FollowUser;
import com.tanhua.domain.mongo.Video;
import com.tanhua.domain.vo.PageResult;
import com.tanhua.domain.vo.VideoVo;
import com.tanhua.dubbo.api.UserInfoApi;
import com.tanhua.dubbo.api.mongo.VideoApi;
import com.tanhua.server.interceptor.UserHolder;
import org.apache.dubbo.config.annotation.Reference;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

@Service
public class SmallVideosService {

    @Autowired
    private OssTemplate ossTemplate;

    @Autowired
    private FastFileStorageClient client;

    @Autowired
    private FdfsWebServer webServer;

    @Reference
    private VideoApi videoApi;

    @Reference
    private UserInfoApi userInfoApi;

    @Autowired
    private RedisTemplate<String,String> redisTemplate;
}

4.2、发布视频

4.2.1、接口文档

http://192.168.136.160:3000/project/19/interface/api/214
在这里插入图片描述

4.2.2、SmallVideosController

@RestController
@RequestMapping("/smallVideos")
public class SmallVideosController {

    @Autowired
    private SmallVideosService videosService;

    /**
     * 发布视频
     *  接口路径:POST
     *  请求参数:
     *      videoThumbnail:封面图
     *      videoFile:视频文件
     */
    @PostMapping
    public ResponseEntity saveVideos(MultipartFile videoThumbnail,MultipartFile videoFile) throws IOException {
        videosService.saveVideos(videoThumbnail,videoFile);
        return ResponseEntity.ok(null);
    }
}

4.2.3、SmallVideosService

@Service
public class SmallVideosService {

    @Reference
    private VideoApi videoApi;

    @Reference
    private UserInfoApi userInfoApi;

    @Autowired
    private OssTemplate ossTemplate;

    @Autowired
    private FastFileStorageClient storageClient;

    @Autowired
    private FdfsWebServer webServer;

    @Autowired
    private RedisTemplate<String,String> redisTemplate;

    /**
     * 发布视频
     */
    //@CacheEvict(value = "videoList",allEntries = true)
    public ResponseEntity saveVideos(MultipartFile videoThumbnail, MultipartFile videoFile) throws IOException {
        //1、封面图上传到阿里云OSS,获取地址
        String picUrl = ossTemplate.upload(videoThumbnail.getOriginalFilename(), videoThumbnail.getInputStream());
        //2、视频上传到fdfs上,获取请求地址
        //获取文件的后缀名
        String filename = videoFile.getOriginalFilename();  //ssss.avi
        String sufix = filename.substring(filename.lastIndexOf(".")+1);
        StorePath storePath = storageClient.uploadFile(videoFile.getInputStream(),
                                    videoFile.getSize(), sufix, null);//文件输入流,文件长度,文件后缀,元数据
        String videoUrl = webServer.getWebServerUrl() + storePath.getFullPath();
        //3、创建Video对象,并设置属性
        Video video = new Video();
        video.setUserId(UserHolder.getUserId());
        video.setPicUrl(picUrl);
        video.setVideoUrl(videoUrl);
        video.setText("我就是我,不一样的烟火");
        //4、调用API完成保存
        videoApi.save(video);
        //5、构造返回值
        return ResponseEntity.ok(null);
    }
}

4.2.4、API接口

VideoApiVideoApiImpl中编写保存video的方法

//视频服务实现类
@Service
public class VideoApiImpl implements VideoApi {

    @Autowired
    private MongoTemplate mongoTemplate;

    @Autowired
    private IdService idService;

    //保存
    public void save(Video video) {
        video.setId(ObjectId.get());
        video.setCreated(System.currentTimeMillis());
        video.setVid(idService.getNextId("video"));
        mongoTemplate.save(video);
    }
}

4.2.5、测试问题

对于SpringBoot工程进行文件上传,默认支持最大的文件是1M。为了解决这个问题,需要在application.yml中配置文件限制

如果上传视频,会导致异常,是因为请求太大的缘故:

在这里插入图片描述

4.2.6、配置文件解析

tanhua-server工程的application.yml中添加解析器,配置请求文件和请求体

Spring:
  servlet:
    multipart:
      max-file-size: 30MB
      max-request-size: 30MB

5、小视频列表

5.0、分析

数据库表:video

  • 创建VO对象

  • 创建controller对象,并配置分页查询接口方法

  • 创建service对象,其中调用API,构造vo对象返回值

    • 调用API:PageResult
    • 将Video转化成VO对象
  • 在API服务层,创建方法,分页查询小视频列表,返回PageResult<Video>

5.1、Controller

    /**
     * 视频列表
     */
    @GetMapping
    public ResponseEntity queryVideoList(@RequestParam(defaultValue = "1")  Integer page,
                                         @RequestParam(defaultValue = "10") Integer pagesize) {
        PageResult result = videosService.queryVideoList(page, pagesize);
        return ResponseEntity.ok(result);
    }

5.2、Service

    public PageResult queryVideoList(Integer page, Integer pagesize) {
        //1、调用API查询分页数据 PageResult<Video>
        PageResult result = videoApi.findAll(page,pagesize);
        //2、获取分页对象中list集合  List<Video>
        List<Video> items = (List<Video>)result.getItems();
        //3、一个Video转化成一个VideoVo对象
        List<VideoVo> list = new ArrayList<>();
        for (Video item : items) {
            UserInfo info = userInfoApi.findById(item.getUserId());
            VideoVo vo = VideoVo.init(info, item);
            //加入了作者关注功能,从redis判断是否存在关注的key,如果存在设置hasFocus=1
            if(redisTemplate.hasKey("followUser_"+UserHolder.getUserId()+"_"+item.getUserId())) {
                vo.setHasFocus(1);
            }
            list.add(vo);
        }
        //4、替换PageResult中的list列表
        result.setItems(list);
        //5、构造返回值
       return result;
    }

5.3、API接口和实现

VideoApiVideoApiImpl中编写分页查询方法

    //分页查询视频列表
    public PageResult findAll(Integer page, Integer pagesize) {
        //1、查询总数
        long count = mongoTemplate.count(new Query(), Video.class);
        //2、分页查询数据列表
        Query query = new Query().limit(pagesize).skip((page-1) * pagesize)
                        .with(Sort.by(Sort.Order.desc("created")));
        List<Video> list = mongoTemplate.find(query, Video.class);
        //3、构建返回
        return new PageResult(page,pagesize,(int) count,list);
    }

5.4、定义Vo对象

tanhua-domain定义返回VO对象VideoVo

package com.tanhua.domain.vo;

import com.tanhua.domain.db.UserInfo;
import com.tanhua.domain.mongo.Video;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.beans.BeanUtils;

import java.io.Serializable;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class VideoVo implements Serializable {


    private Long userId;
    private String avatar; //头像
    private String nickname; //昵称

    private String id;
    private String cover; //封面
    private String videoUrl; //视频URL
    private String signature; //发布视频时,传入的文字内容


    private Integer likeCount; //点赞数量
    private Integer hasLiked; //是否已赞(1是,0否)
    private Integer hasFocus; //是否关注 (1是,0否)
    private Integer commentCount; //评论数量

    public static VideoVo init(UserInfo userInfo, Video item) {
        VideoVo vo = new VideoVo();
        //copy用户属性
        BeanUtils.copyProperties(userInfo,vo);  //source,target
        //copy视频属性
        BeanUtils.copyProperties(item,vo);
        vo.setCover(item.getPicUrl());
        vo.setId(item.getId().toHexString());
        vo.setSignature(item.getText());
        vo.setHasFocus(0);
        vo.setHasLiked(0);
        return vo;
    }
}

5.5、测试

测试问题

在运行测试时,及其容易出现空指针等异常。

在这里插入图片描述

解决方法

之所以出现这类问题“”或者空指针异常,是由于MongoDB中非关系数据库,不能自动约束检测表关系。我们检查Video数据库表得知。其中有几条数据的发布人是虚拟构造,在用户表中并不存在

在这里插入图片描述

解决思路很简单,删除错误数据即可

6、关注用户

关注用户是关注小视频发布的作者,这样我们后面计算推荐时,关注的用户将权重更重一些。

关注用户

  • controller:接受参数
  • service:调用API,操作redis
  • api接口(VideoAPI):关注的保存和删除
  • 修改之前的查询service,从redis获取数据(如果存在返回1:,不存在返回0)

6.1、实体类对象

package com.tanhua.domain.mongo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.bson.types.ObjectId;
import org.springframework.data.mongodb.core.mapping.Document;

//用户关注表(关注小视频的发布作者)
@Data
@NoArgsConstructor
@AllArgsConstructor
@Document(collection = "focus_user")
public class FocusUser implements java.io.Serializable{

    private static final long serialVersionUID = 3148619072405056052L;

    private ObjectId id; //主键id
    private Long userId; //用户id    106
    private Long followUserId; //关注的用户id   1
    private Long created; //关注时间
}

6.2、Controller

/**
 * 关注视频作者
 */
@PostMapping("/{id}/userFocus")
public ResponseEntity userFocus(@PathVariable("id") Long followUserId) {
    videosService.userFocus(followUserId);
	return ResponseEntity.ok(null);
}

/**
 * 取消关注视频作者
 */
@PostMapping("/{id}/userUnFocus")
public ResponseEntity userUnFocus(@PathVariable("id") Long followUserId) {
    videosService.userUnFocus(followUserId);
	return ResponseEntity.ok(null);    
}

6.3、Service

    //关注视频作者
    public void userFocus(Long followUserId) {
        //1、创建FollowUser对象,并设置属性
        FollowUser followUser = new FollowUser();
        followUser.setUserId(UserHolder.getUserId());
        followUser.setFollowUserId(followUserId);
        //2、调用API保存
        videoApi.saveFollowUser(followUser);
        //3、将关注记录存入redis中
        String key = Constants.FOCUS_USER_KEY + UserHolder.getUserId();
        String hashKey = String.valueOf(followUserId);
        redisTemplate.opsForHash().put(key,hashKey,"1");
    }

    //取消关注视频作者
    public void userUnFocus(Long followUserId) {
        //1、调用API删除关注数据
        videoApi.deleteFollowUser(UserHolder.getUserId(),followUserId);
        //2、删除redis中关注记录
        String key = Constants.FOCUS_USER_KEY + UserHolder.getUserId();
        String hashKey = String.valueOf(followUserId);
        redisTemplate.opsForHash().delete(key,hashKey);
    }

6.4、API和实现类

VideoApiVideoApiImpl中编写关注方法

在这里插入图片描述

解决重复关注的问题:

在保存关注数据时,可以根据userId和followUserId查询数据库,如果存在则不再保存数据

6.5、修改查询视频列表

查询视频列表是,从redis中获取关注数据

在这里插入图片描述

7、通用缓存SpringCache

实现缓存逻辑有2种方式:

  1. 每个接口单独控制缓存逻辑
  2. 统一控制缓存逻辑

Spring从3.1开始定义了org.springframework.cache.Cache和org.springframework.cache.CacheManager接口来统一不同的缓存技术;并支持使用JCache(JSR-107)注解简化我们开发;

  • Cache接口为缓存的组件规范定义,包含缓存的各种操作集合;

  • Cache接口下Spring提供了各种xxxCache的实现;如RedisCache,EhCacheCache ,ConcurrentMapCache等;

  • 每次调用需要缓存功能的方法时,Spring会检查检查指定参数的指定的目标方法是否已经被调用过;如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓存结果后返回给用户。下次调用直接从缓存中获取。

  • 使用Spring缓存抽象时我们需要关注以下两点;

    1、确定方法需要被缓存以及他们的缓存策略

    2、从缓存中读取之前缓存存储的数据

内部使用AOP的形式,对redis操作进行简化

7.1、重要概念

名称解释
@Cacheable主要针对方法配置,能够根据方法的请求参数对其进行缓存
@CacheEvict清空缓存

7.2、入门案例

7.2.1 导入依赖

导入SpringDataRedis的依赖,并在application.yml中配置 (略)

7.2.2 开启缓存支持

然后在启动类注解@EnableCaching开启缓存

@SpringBootApplication
@EnableCaching  //开启缓存
public class DemoApplication{
 
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

7.2.3 编写UserInfoService

package com.tanhua.server.test;

import com.tanhua.domain.db.UserInfo;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
public class UserInfoService {

    //根据id查询
    public UserInfo queryById(Long userId) {
        //从数据库查询
        UserInfo user = new UserInfo();
        user.setId(userId);
        user.setNickname("ceshi");
        return user;
    }

    //根据id修改
    public void update(Long userId) {
        UserInfo user = new UserInfo();
        user.setId(userId);
        user.setNickname("itcast");
    }
}

7.2.3 缓存@Cacheable

@Cacheable注解会先查询是否已经有缓存,有会使用缓存,没有则会执行方法并缓存。

@Cacheable(value = "user",key = "#userId")
public UserInfo queryById(Long userId) {
    //从数据库查询
    UserInfo user = new UserInfo();
    user.setId(userId);
    user.setNickname("ceshi");
    return user;
}

此处的value是必需的,它指定了你的缓存存放在哪块命名空间。

此处的key是使用的spEL表达式,参考上章。这里有一个小坑,如果你把methodName换成method运行会报错,观察它们的返回类型,原因在于methodNameStringmethohMethod

此处的User实体类一定要实现序列化public class User implements Serializable,否则会报java.io.NotSerializableException异常。

到这里,你已经可以运行程序检验缓存功能是否实现。

7.2.4 清除@CacheEvict

@CachEvict 的作用 主要针对方法配置,能够根据一定的条件对缓存进行清空 。

//根据id修改
@CacheEvict(value = "user",key = "#userId")
public void update(Long userId) {
    //修改用户
    UserInfo user = new UserInfo();
    user.setId(userId);
    user.setNickname("itcast");
}

7.3、视频列表缓存处理

修改VideoService,分页列表存入缓存,发布视频删除缓存

由于使用Reids缓存处理数据时,不能缓存ResponseEntity对象,所以需要修改方法返回值为PageResult

@Cacheable(value="videoList",key="#page + '_' +  #pagesize")
public PageResult queryVideoList(Integer page, Integer pagesize) {
    //1、调用API查询 : PageReulst<Video>
    PageResult result = videoApi.findAll(page,pagesize);
    //2、获取分页中的list集合  List<Video>
    List<Video> items = (List<Video>)result.getItems();
    //3、循环视频列表,一个Video构造一个Vo
    List<VideoVo> list = new ArrayList<>();
    for (Video item : items) {
        UserInfo userInfo = userInfoApi.findById(item.getUserId());
        VideoVo vo = VideoVo.init(userInfo, item);
        //从redis中获取,当前用户是否已经关注了视频发布作者
        String key = "followUser_"+UserHolder.getUserId()+"_"+item.getUserId();
        if (redisTemplate.hasKey(key)) {
            vo.setHasFocus(1);
        }
        list.add(vo);
    }
    //4、替换result中的item数据
    result.setItems(list);
    //5、构造返回值
    result;
}

7.4、发布视频清空缓存

//发布视频
@CacheEvict(value="videoList",allEntries = true)
public ResponseEntity saveVideo(MultipartFile videoThumbnail, MultipartFile videoFile) throws IOException {
    //1、图片上传到阿里云oss,获取请求地址
    String picUrl = ossTemplate.upload(videoThumbnail.getOriginalFilename(), videoThumbnail.getInputStream());
    //2、视频上传到fdfs上,获取请求地址
    String filename = videoFile.getOriginalFilename(); //xxxx.avi
    //获取文件后缀
    String sufix = filename.substring(filename.lastIndexOf(".")+1);

    StorePath storePath = client.uploadFile(videoFile.getInputStream(),
                                            videoFile.getSize(), sufix, null); //文件输入流,文件长度(大小),文件的后缀名,元数据(null)
    String videoUrl = webServer.getWebServerUrl() + storePath.getFullPath();
    //3、构建Video对象,并设置属性
    Video video = new Video();
    video.setPicUrl(picUrl);
    video.setVideoUrl(videoUrl);
    video.setText("传智播客是一个负责任的教育机构~"); //客户端未传递,手动模拟
    video.setUserId(UserHolder.getUserId());
    //4、调用api保存
    videoApi.save(video);
    //5、构建返回值
    return ResponseEntity.ok(null);
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/73944.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

会员消费占比高达96%,孩子王究竟是怎么做到的?

&#x1f446;点击关注公众号&#x1f446;1.孩子王&#xff1a;依靠会员“稳江山”2021年上半年&#xff0c;增长黑盒独家发布了一篇关于孩子王的研究文章《万字拆解孩子王&#xff1a;充满矛盾的母婴零售之王》&#xff0c;彼时&#xff0c;孩子王尚在二度上市的前夕等待敲钟…

JAVA SCRIPT设计模式--行为型--设计模式之Template Method模板方法(22)

JAVA SCRIPT设计模式是本人根据GOF的设计模式写的博客记录。使用JAVA SCRIPT语言来实现主体功能&#xff0c;所以不可能像C&#xff0c;JAVA等面向对象语言一样严谨&#xff0c;大部分程序都附上了JAVA SCRIPT代码&#xff0c;代码只是实现了设计模式的主体功能&#xff0c;不代…

2023最新SSM计算机毕业设计选题大全(附源码+LW)之java课程教学过程f6oz5

对于即将毕业或者即将做课设的同学而言&#xff0c;由于经验的欠缺&#xff0c;面临的第一个难题就是选题&#xff0c;确定好题目之后便是开题报告&#xff0c;如果选题首先看自己学习那些技术&#xff0c;不同技术适合做不同的产品&#xff0c;比如自己会些简单的Java语言&…

BSV 上的 Graftroot

我们已经演示了如何使用无合约的合约在 BSV 上实现 Taproot。我们将展示了其后续提案 Graftroot 可以以类似的方式实施。 BTC 中的 Grabroot 与 Taproot 类似&#xff0c;有两种方式可以使用锁定在由多方创建的聚合公钥 P 中的资金&#xff1a; 合作案例&#xff1a;又名默认…

kubernetes 安装 Harbor 仓库

文章目录kubernetes 安装 Harbor 仓库1. 下载 Harbor2. 安装 docker3. 优化 docker 配置4. 下载 docker-compose5. 安装 Harbor:one: 上传 harbor 文件包:two: 解压:three: 修改配置文件:four: 执行安装脚本安装:five: 配置开机自启6. 登陆测试:one: 浏览器登陆:two: 命令行登陆…

为什么需要对相机标定?

以下内容来自系统教程如何搞定单目/鱼眼/双目/阵列 相机标定&#xff1f; 点击领取相机标定资料和代码 为什么需要对相机标定&#xff1f; 我们所处的世界是三维的&#xff0c;而相机拍摄的照片却是二维的&#xff0c;丢失了其中距离/深度的信息。从数学上可以简单理解为&…

Peppol网络对接流程

Peppol 代表泛欧在线公共采购&#xff0c;现在连接到 Peppol 的组织可以通过高度安全的国际网络交换商业文件。知行软件通过了 PEPPOL 的 AS2 及 AS4 测试&#xff0c;被 OpenPEPPOL AISBL 正式认证为 PEPPOL 接入点供应商。可以在Peppol查询到相关接入点信息&#xff0c;如下&…

TH9-搭建后台系统

TH9-搭建后台系统1、项目架构1.1 概述1.2 API网关1.2.1 搭建网关依赖引导类跨域问题配置类配置文件1.2.2 配置鉴权管理器1.3 Nacos配置中心1.3.1 添加依赖1.3.2 添加bootstrap.yml配置1.3.3 nacos添加配置2、后台系统2.1 概述2.2 环境前端搭建2.2.1 导入数据库2.2.2 导入静态页…

MYSQL-INNODB索引构成详解

作者&#xff1a;郑啟龙 摘要&#xff1a; 对于MYSQL的INNODB存储引擎的索引&#xff0c;大家是不陌生的&#xff0c;都能想到是 B树结构&#xff0c;可以加速SQL查询。但对于B树索引&#xff0c;它到底“长”得什么样子&#xff0c;它具体如何由一个个字节构成的&#xff0c…

插入排序

目录 插入排序 思路: 原理视频: 代码: 时间复杂度: 总结: 题目链接: 插入排序 题目描述&#xff1a; 插入排序基本思想是每一步将一个待排序的记录&#xff0c;插入到前面已经排好序的有序序列中去&#xff0c;直到插完所有元素为止。 输入N个整数&#xff0c;将它们从…

【设计模式】代理模式(Proxy Pattern)

代理模式属于结构型模式&#xff0c;当一个对象不处于相同内存空间时、创建开销大时、需要进行安全控制时或需要代理处理一些其他事物时可以使用代理模式。代理模式通过为另一个类提供一个替身类来控制对这个类的对象的访问。 文章目录代理模式的介绍代理的分类&#xff1a;优点…

了解CloudCompare软件

CloudCompare是一款基于GPL开源协议的3D点云处理软件&#xff0c; 打开一个点云&#xff1b; 按住鼠标拖动旋转会出现坐标轴&#xff1b; 如果选中 TLS/GBL&#xff0c; 会出来右边这个线框&#xff0c;可以一起旋转&#xff1b;查了一下&#xff0c;TLS/GBL似乎是地面激光雷达…

2022年国际工程行业研究报告

第一章 行业概况 国际工程是指一个工程项目从咨询、投资、招投标、承包(包括分包)、设备采购、培训到监理各个阶段的参与者来自不止一个国家&#xff0c;并且按照国际工程项目管理模式进行管理的工程。国际工程是一种综合性的国际经济合作方式&#xff0c;是国际技术贸易的一种…

了解Linux内核内存映射

【推荐阅读】 深入linux内核架构--进程&线程 路由选择协议——RIP协议 轻松学会linux下查看内存频率,内核函数,cpu频率 纯干货&#xff0c;linux内存管理——内存管理架构&#xff08;建议收藏&#xff09; 概述Linux内核驱动之GPIO子系统API接口 一. 内存映射原理 由于所…

shadow阴影属性

shadow阴影属性 盒子阴影CSS中新增了盒子阴影&#xff0c;我们可以使用box-shadow属性为盒子添加阴影 源代码 box-shadow: h-shadow v-shadow blur spread color inset; h-shadow 必需&#xff0c;水平阴影的位置&#xff0c;允许负值 v-shadow…

AI 揭晓答案,2022世界杯冠军已出炉

卡塔尔世界杯&#xff0c;究竟谁能捧起大力神杯&#xff0c;就让我们用机器学习预测一下吧&#xff01; 文章目录数据源技术提升数据集构建功能开发数据分析模型世界杯模拟结论数据源 为了构建机器学习模型&#xff0c;我们需要来自团队的数据。首先需要一些能够说明球队表现的…

Java学习之多态参数

目录 多态参数 父类-Employee类 子类-Worker类 子类-Manager类 Test类-要求1 main类-PolyParameter 在main类中调用Test类的showEmpAnnual(Employee e) 方法 运行结果 Test类-要求2 代码 main方法内调用 分析 运行结果 多态参数 方法定义的形参类型是父类&#xff0…

青竹画材创业板IPO被终止:年营收4.15亿 文投基金是股东

雷递网 雷建平 12月8日河北青竹画材科技股份有限公司&#xff08;简称&#xff1a;“青竹画材”&#xff09;日前IPO被终止。青竹画材曾计划募资4.1亿元&#xff0c;其中&#xff0c;3.08亿元用于美术画材产能扩建项目&#xff0c;2317.35万元用于研发中心项目&#xff0c;7966…

Selenium+python常见问题,闪退,找不到元素

1、闪退问题&#xff1a; 由于缺少浏览器对应驱动。谷歌&#xff1a;chromedriver驱动&#xff1b;火狐&#xff1a;geckodriver驱动 查看Chrome版本 查找Chrome和ChromeDriver的对应关系 打开Chrome&#xff0c;在设置 – 关于Chrome下载对应ChromeDriver ChromeDriver下载…

本地存储:localStorage,sessionStorage,和cookie。区别

localStorage&#xff1a; 特点&#xff1a; 永久存储支持跨页面通讯&#xff0c;也就是在其他页面同样可以获取到你存好的数据。只能存储字符串类型的数据&#xff0c;不能存储复杂数据类型 sessionStorage&#xff1a; 特点&#xff1a; 临时存储&#xff0c;会话级别&am…