SpringBoot2.x仿B站第四章打造高性能的视频与弹幕系统笔记及源码

文章目录

    • 1.搭建FastDFS文件服务器
      • 1.1FastDfs的架构图
      • 1.2Nginx结合FastDFS实现文件资源HTTP访问
      • 内网穿透
    • 2.SpringBoot整合FastDFS
    • 3.视频功能开发
      • 3.1断点续传
      • 3.2 秒传(小难点)
      • 3.3 视频投稿
      • 3.4瀑布流视频列表(视频分页查询)
      • 3.5视频在线播放(添加视频,删除视频,添加视频标签....)
        • 删除对应视屏标签
      • 3.6 视频点赞,收藏,投币,评论
        • 视频点赞
        • 取消收藏
        • 查看点赞数
        • 添加收藏
        • 查看收藏
        • 添加视频投币
        • 评论分页请求
    • 4.弹幕系统
      • 4.1查看弹幕
        • 1.创建websocketService

这一章属于难点章节,包含视频的上传于弹幕系统的开发

SpringBoot2.x仿B站第四章打造高性能的视频与弹幕系统笔记及源码_第1张图片

1.搭建FastDFS文件服务器

  • 什么是FastDFS?
    开源的轻量级分布式文件系统,用于解决大数据量存储和负载均衡等问题。
  • 优点:
    支持HTTP协议传输文件(结合NGINX);对文件内容做Hash处理,节约磁盘空间;支持负载均衡,整体性能较佳。适用中小型系统

1.1FastDfs的架构图

SpringBoot2.x仿B站第四章打造高性能的视频与弹幕系统笔记及源码_第2张图片

1.2Nginx结合FastDFS实现文件资源HTTP访问

SpringBoot2.x仿B站第四章打造高性能的视频与弹幕系统笔记及源码_第3张图片

内网穿透

  • 这个项目采用的ngrok2.x;但是最新的已经是3了,因此大家自行选择,我是3不行
  • 我换的coplar这个,具体配置参上https://blog.csdn.net/weixin_59823583/article/details/126823857?spm=1001.2014.3001.5501

2.SpringBoot整合FastDFS

  • yml配置
fdfs:tracker-list:192.168.117.130:22122

3.视频功能开发

3.1断点续传

  • 为什么采用断点续传,解决大文件上传的痛点比如带宽资源,中断的地方继续上传而不用在完整的上传
  • 写一个工具类,FastdfsUtil基于FastFileStorageClient写的
    SpringBoot2.x仿B站第四章打造高性能的视频与弹幕系统笔记及源码_第4张图片
    -文件上传功能
    功能都是自己开发,在调用fastClientDfs中的方法进行实现
    SpringBoot2.x仿B站第四章打造高性能的视频与弹幕系统笔记及源码_第5张图片
  • 文件的参数传入都是Multipart
    SpringBoot2.x仿B站第四章打造高性能的视频与弹幕系统笔记及源码_第6张图片
   public String getFileType(MultipartFile file){
        if(file==null){
            throw new ConditionalException("非法文件!");
        }
        //根据文件获取文件类型,读取file的最后一个参数
        String fileName = file.getOriginalFilename();
        int index = fileName.lastIndexOf(".");
        return fileName.substring(index+1);
    }
    //上传
    public String uploadCommonFile(MultipartFile file) throws IOException {
        Set<MetaData>metaData = new HashSet<>();
        String fileType = this.getFileType(file);
        StorePath storePath = fastFileStorageClient.uploadFile(file.getInputStream(), file.getSize(), fileType, metaData);
        return storePath.getPath();
    }
  • 断点续传功能的开发(返回一个相对的传输路径)
    @Autowired
    private AppendFileStorageClient appendFileStorageClient;
    //断点续传的功能
    public String uploadAppendFile(MultipartFile file) throws IOException {
        String fileName = file.getOriginalFilename();
        String fileType = this.getFileType(file);
        StorePath storePath = appendFileStorageClient.uploadAppenderFile(DEFAULT_GROUP, file.getInputStream(), file.getSize(), fileType);
        return storePath.getPath();
    }
        //不会重复已经上传的
    public void modifyAppendFile(MultipartFile file,String filePath,long offset) throws IOException {
        appendFileStorageClient.modifyFile(DEFAULT_GROUP,filePath,file.getInputStream(),file.getSize(),offset);
    }

3.2 秒传(小难点)

SpringBoot2.x仿B站第四章打造高性能的视频与弹幕系统笔记及源码_第7张图片

  • 妙传也就是我们之前上传的已经存到redis里面的,如果进行上传的时候我们进行判断,发现已经有后直接从redis取到相应的数据即可
  • 这里引入切片的功能实现
 //分片上传,及那个path,uploadNo,uploadSize存到我们的redis
    public String uploadFileBySlices(MultipartFile file,String fileMD5,Integer slice,Integer totalSlice) throws IOException {
        if(file==null || slice==null || totalSlice == null){
            throw new ConditionalException("参数异常");
        }
        String pathKey = PATH_KEY + fileMD5;
        String uploadNoKey = UPLOAD_NO_KEY + fileMD5;
        String uploadSizeKey = UPLOAD_SIZE_KEY + fileMD5;
        //获取上传大小
        String uploadSizeStr = redisTemplate.opsForValue().get(uploadSizeKey);
        Long uploadedSize = 0L;
        if(!StringUtils.isNullOrEmpty(uploadSizeStr)){
            uploadedSize = Long.valueOf(uploadSizeStr);
        }
        String fileType = this.getFileType(file);
        //判断上传的是第一个分片还是其他分片
        if(slice==1){
            String path = this.uploadAppendFile(file);
            if(StringUtils.isNullOrEmpty(path)){
                throw new ConditionalException("上传失败! ");
            }
            redisTemplate.opsForValue().set(pathKey,path);
            redisTemplate.opsForValue().set(uploadNoKey,"1");
        }else{
            String filePath = redisTemplate.opsForValue().get(pathKey);
            if (StringUtils.isNullOrEmpty(filePath)){
                throw new ConnectException("上传失败!");
            }
            this.modifyAppendFile(file,filePath,uploadedSize);
            //更新分片
            redisTemplate.opsForValue().increment(uploadNoKey);
        }
        //更新上传大小
        uploadedSize +=file.getSize();
        redisTemplate.opsForValue().set(uploadSizeKey,String.valueOf(uploadedSize));
        //判断是否已经上传完,上传完后清除redis里面的key值
        String uploadedNoStr = redisTemplate.opsForValue().get(uploadNoKey);
        Integer uploadedNo = Integer.valueOf(uploadedNoStr);
        String resultPath = "";
        if(uploadedNo.equals(totalSlice)){
            resultPath = redisTemplate.opsForValue().get(pathKey);
            List<String> list = Arrays.asList(uploadNoKey, pathKey, uploadSizeKey);
            redisTemplate.delete(list);
        }
        return resultPath;
    }
  • 如何确定分片
//将multipart进行转换成io流中的file
    public File multipartFileToFile(MultipartFile multipartFile) throws IOException {
        String originalFilename = multipartFile.getOriginalFilename();
        String[]fileName = originalFilename.split("\\.");
        File file = File.createTempFile(fileName[0],"."+fileName[1]);
        multipartFile.transferTo(file);
        return file;
    }
  /**
     * 指定文件切成多少片,那么需要用到IO流的关系,我们先读取文件的大小
     * 确定好每次读取多少,比如SLICE_SIZE=1024*2,在指定我们写入的路径path
     * 
     */
    public void convertFileToSlice(MultipartFile multipartFile) throws IOException {
        String fileType = this.getFileType(multipartFile);
        File file =this.multipartFileToFile(multipartFile);
        long fileLength = file.length();
        int count = 1;
        for(int i = 0;i<fileLength;i+=SLICE_SIZE){
            RandomAccessFile randomAccessFile = new RandomAccessFile(file,"r");
            randomAccessFile.seek(i);
            byte[]bytes = new byte[SLICE_SIZE];
            int len = randomAccessFile.read();
            String path = "E:\\WorkSpace\\tmp\\tmpfile"+count+"."+fileType;
            File slice = new File(path);
            FileOutputStream fos = new FileOutputStream(slice);
            fos.write(bytes,0,len);
            fos.close();
            randomAccessFile.close();
            count++;
        }
        file.delete();//删除临时文件
    }
  • 视频删除
    //删除
    public void deleteFile(String filePath){
        fastFileStorageClient.deleteFile(filePath);
    }

3.3 视频投稿

  • 新建一个t_file表

SpringBoot2.x仿B站第四章打造高性能的视频与弹幕系统笔记及源码_第8张图片


DROP TABLE IF EXISTS `t_file`;
CREATE TABLE `t_file`  (
  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键id',
  `url` VARCHAR(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '文件存储路径',
  `type` VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '文件类型',
  `md5` VARCHAR(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '文件md5唯一标识串',
  `createTime` DATETIME NULL DEFAULT NULL COMMENT '创建时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = INNODB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '文件表' ROW_FORMAT = DYNAMIC;
  • Controller层,需要传文件,filemD5,分片的开始,总分片数量
   @PutMapping("/file-slices")
    public JsonResponse<String>uploadFileByServices(MultipartFile slice,
                                                    String fileMd5,
                                                    Integer sliceNo,
                                                    Integer totalSlice) throws Exception {
        String filePath = fileService.uploadFileBySlices(slice,fileMd5,sliceNo,totalSlice);
        return new JsonResponse<>(filePath);
    }
  • service层
   public String uploadFileBySlices(MultipartFile slice,
                                     String fileMD5,
                                     Integer sliceNo,
                                     Integer totalSliceNo) throws Exception {
        File dbFileMD5 = fileDao.getFileByMD5(fileMD5);
        if(dbFileMD5 != null){
            return dbFileMD5.getUrl();
        }
        String url = fastDFSUtil.uploadFileBySlices(slice, fileMD5, sliceNo, totalSliceNo);
        if(!StringUtil.isNullOrEmpty(url)){
            dbFileMD5 = new File();
            dbFileMD5.setCreateTime(new Date());
            dbFileMD5.setMd5(fileMD5);
            dbFileMD5.setUrl(url);
            dbFileMD5.setType(fastDFSUtil.getFileType(slice));
            fileDao.addFile(dbFileMD5);
        }
        return url;
    }

    public String getFileMD5(MultipartFile file) throws Exception {
        return MD5Util.getFileMD5(file);
    }
  • dao层

@Mapper
public interface FileDao {
    File getFileByMD5(String md5);

    Integer addFile(File file);
}
  • file.xml

DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.imooc.bilibili.dao.FileDao">
    <insert id="addFile" parameterType="com.imooc.bilibili.domain.File">
        insert into
            t_file(
                   url,
                   `type`,
                   md5,
                   createTime
        )values (
                 #{url},
                 #{type},
                 #{md5},
                 #{createTime}
                        )
    insert>


    <select id="getFileByMD5" resultType="com.imooc.bilibili.domain.File" parameterType="java.lang.String">
            select
                *
            from
                t_file
            where
                md5 = #{md5}
    select>
mapper>
  • 测试
    SpringBoot2.x仿B站第四章打造高性能的视频与弹幕系统笔记及源码_第9张图片

SpringBoot2.x仿B站第四章打造高性能的视频与弹幕系统笔记及源码_第10张图片

3.4瀑布流视频列表(视频分页查询)

  • controller层

    @GetMapping("/videos")
    public JsonResponse<PageResult<Video>>pageListVideos(Integer size,Integer no,String area){
    PageResult<Video>result = videoService.pageListVideo(size,no,area);
    return new JsonResponse<>(result);
    }
  • service层
   /**
     * 视频列表分页
     * @param size
     * @param no
     * @param area
     * @return
     */
    public PageResult<Video>pageListVideo(Integer size,Integer no,String area){
        if(size ==null || no == null){
            throw new ConditionalException("异常参数");
        }
        Map<String,Object>params = new HashMap<>();
        params.put("start",(no-1)*size);
        params.put("limit",size);
        params.put("area",area);
        List<Video>list = new ArrayList<>();//查出的信息放在列表
        Integer total = videoDao.pageCountVideos(params);
        if(total>0){
            list = videoDao.pageListVideos(params);
        }
        return new PageResult<>(total,list);
    }
  • Dao层
    Integer pageCountVideos(Map<String, Object> params);

    List<Video> pageListVideos(Map<String, Object> params);
  • xml文件
   
    <select id="pageCountVideos" resultType="java.lang.Integer" parameterType="java.util.Map">
        select
            count(1)
        from
            t_video
        where
            1=1
            <if test="area != null and area != '' ">
                and area = #{area}
            if>
    select>


    
    <select id="pageListVideos" resultType="com.imooc.bilibili.domain.Video" parameterType="java.util.Map">
        select
            *
        from
            t_video
        where
            1=1
            <if test="area !=null and area !='' ">
                and area = #{area}
            if>
        order by id desc
        limit #{start},#{limit}
    select>

3.5视频在线播放(添加视频,删除视频,添加视频标签…)

  • 1.添加视频(包含添加视频标签)
    @PostMapping("/videos")
    public JsonResponse<String>addVideos(@RequestBody Video video){
        Long userId = userSupport.getCurrentUserId();
        video.setUserId(userId);
        videoService.addVideos(video);
        return JsonResponse.success();
    }
  • service
    @Transactional
    public void addVideos(Video video) {
        Date now = new Date();
        video.setCreateTime(new Date());
        videoDao.addVideos(video);
        Long videoId = video.getId();
        List<VideoTag> tagList = video.getVideoTagList();
        tagList.forEach(item->{
            item.setCreateTime(new Date());
            item.setVideoId(videoId);
        });
        videoDao.batchAddVideoTags(tagList);
    }
  • dao
   <insert id="addVideos" parameterType="com.imooc.bilibili.domain.Video" keyProperty="id" useGeneratedKeys="true">
        insert into
            t_video(
            id,
            userId,
            thumbnail,
            title,
            `type`,
            duration,
            area,
            description,
            createTime
        )values(
            #{id},
            #{userId},
            #{thumbnail},
            #{title},
            #{type},
            #{duration},
            #{area},
            #{description},
            #{createTime}
                       )
    </insert>
    <insert id="batchAddVideoTags" parameterType="java.util.List">
        insert into
            t_video_tag(
              videoId,
              tagId,
              createTime
        )values(
                <foreach collection="tagList" item="videoTag" separator=",">
                    #{videoTag.videoId},
                    #{videoTag.tagId},
                    #{videoTag.createTime},
                </foreach>
                       )
    </insert>
  • 由于分页我们的视频包含了对应的标签,因此Video也要进行变化
@Data
public class Video {
    private Long id;

    private Long userId;//用户Id

    private String url;//视频链接

    private String thumbnail;//封面

    private String title;//标题

    private String type;//类型0 自制 1 转载

    private String duration;//时长

    private String area;//分区

    private List<VideoTag>videoTagList;//标签列表

    private String description;//简介

    private Date createTime;
}
  • 查询视频标签
  • controller
    /**
     * 查询对应的标签
     * @param videoId
     * @return
     */
    @GetMapping("/video-tags")
    public JsonResponse<List<Tag>>getVideoTags(@RequestParam Long videoId){
        List<Tag>list =  videoService.getVideoTags(videoId);
        return new JsonResponse<>(list);
    }
  • servcie层
  • 根据传的videoId查询对应所有关于videoTag的信息,进而收集他们的tagId,根据tagList集合查询对应的标签信息
    public List<Tag> getVideoTags(Long videoId){
        //或者该视频的所有tagId
        List<VideoTag>videoTagList = videoDao.getVideoTagList(videoId);
        Set<Long> tagList = videoTagList.stream().map(VideoTag::getTagId).collect(Collectors.toSet());
        List<Tag>tagInfoList = new ArrayList<>();
        if(tagList.size() > 0){
            tagInfoList = videoDao.getTagInfoList(tagList);
        }
        return tagInfoList;
    }
  • Dao层

    List<VideoTag> getVideoTagList(Long videoId);

    List<Tag> getTagInfoList(Set<Long> tagList);
  • XML层
    <select id="getVideoTagList" resultType="com.imooc.bilibili.domain.VideoTag" parameterType="java.lang.Long">
        select
            *
        from
            t_video_tag
        where
            videoId = #{videoId};
    select>
    <select id="getTagInfoList" resultType="com.imooc.bilibili.domain.Tag">
        select
            *
        from
            t_tag
        where
        1=1
        <if test="tagList !=null and tagList !=''">
            and tagId in
            <foreach collection="tagList" item="tagId" index="index" open="(" close=")" separator=",">
                #{tagId}
            foreach>
        if>
    select>

删除对应视屏标签

  • 前端传来一个params,里面包含了对应的videoId和标签的id
    /**
     * 删除视频标签
     * @param params
     * @return
     */
    public JsonResponse<String>deleteVideoTags(@RequestBody JSONObject params){
        String tagList = params.getString("tagIdList");
        Long videoId =  params.getLong("videoId");
        videoService.deleteVideoTags(JSONArray.parseArray(tagList).toJavaList(Long.class),videoId);
        return JsonResponse.success();
    }
  • service
    public void deleteVideoTags(List<Long> tagList, Long videoId) {
        videoDao.deleteVideoTags(tagList,videoId);
    }
  • DAO层

    Integer deleteVideoTags(@Param("tagList") List<Long> tagList
            , @Param("videoId") Long videoId);

-XML层

  
    <delete id="deleteVideoTags">
        delete from
            t_video_tag
        where
        videoId = #{videoId}
        and
        tagId in
        <foreach collection="tagList" item="tagId" open="(" close=")" separator=",">
            #{tagId}
        foreach>
    delete>

3.6 视频点赞,收藏,投币,评论

视频点赞

    /**
     * 视频点赞
     * @param videoId
     * @return
     */
    @PostMapping("/video-like")
    public JsonResponse<String>addVideoLike(@RequestParam Long videoId){
        Long userId = userSupport.getCurrentUserId();
        videoService.addVideoLike(videoId,userId);
        return JsonResponse.success();
    }
  • service
   //点赞
    public void addVideoLike(Long videoId, Long userId) {
        Video video =  videoDao.getVideoById(videoId);
        if(video==null){
            throw new ConditionalException("非法视频");
        }
        VideoLike videoLike =  videoDao.getVideoLikeByVideoIdAndUserId(videoId,userId);
        if(videoLike!=null){
            throw new ConditionalException("视频已点赞");
        }
        videoLike = new VideoLike();
        videoLike.setVideoId(videoId);
        videoLike.setUserId(userId);
        videoLike.setCreateTime(new Date());
        videoDao.addVideoLike(videoLike);
    }
  • Dao-xml
    
    <insert id="addVideoLike"  parameterType="com.imooc.bilibili.domain.VideoLike" >
            insert into
                t_video_like(
            videoId,
            userId,
            createTime
            )values(
            #{videoId},
            #{userId},
            #{createTime}
          )
    insert>

取消收藏

  • controller
   /**
     * 取消点赞
     * @param videoId
     * @return
     */
    @DeleteMapping("/video-likes")
    public JsonResponse<String>deleteVideoLike(@RequestParam Long videoId){
        Long userId = userSupport.getCurrentUserId();
        videoService.deleteVideoLike(videoId,userId);
        return JsonResponse.success();
    }
  • service层
    //取消点赞
    public void deleteVideoLike(Long videoId, Long userId) {
        videoDao.deleteVideoLike(videoId,userId);
    }
  • dao-xml
   Integer deleteVideoLike(@Param("videoId") Long videoId
                            ,@Param("userId") Long userId);
    
    <delete id="deleteVideoLike">
        delete from
            t_video_like
        where
            videoId = #{videoId} and userId = #{userId};
    delete>

查看点赞数

  /**
     * 查询点赞数
     * @param videoId
     * @return
     */
    @GetMapping("/video-likes")
    public JsonResponse<Map<String,Object>>getVideoLike(@RequestParam Long videoId){
        //需要进行游客和登陆用户的判断
        Long userId = null;
        try{
        userId = userSupport.getCurrentUserId();
        }catch (Exception ignore){}
        Map<String,Object>map =  videoService.getVideoLike(videoId,userId);
        return new JsonResponse<>(map);
    }
  • service
  • 点赞数包含自己点赞否,所以需要用到map记录自己点赞或者没有点赞
    //查看点赞数
    public Map<String, Object> getVideoLike(Long videoId, Long userId) {
        //Map里面装的是like count
        Long count = videoDao.getVideoLikes(videoId);
        VideoLike videoLike = videoDao.getVideoLikeByVideoIdAndUserId(videoId, userId);
        boolean like = videoLike!=null;
        Map<String,Object>result = new HashMap<>();
        result.put("count",count);
        result.put("like",like);
        return result;
    }
  • Dao
    Long getVideoLikes(Long videoId);
    VideoLike getVideoLikeByVideoIdAndUserId(@Param("videoId")Long videoId, @Param("userId") Long userId);
  • xml
   <!--查询用户是否点赞-->
    <select id="getVideoLikeByVideoIdAndUserId" resultType="com.imooc.bilibili.domain.VideoLike">
        select
            *
        from
            t_video_like
        where
            videoId = #{videoId} and userId = #{userId};
    </select>

    <!--查询用户点赞数的多少-->
    <select id="getVideoLikes" resultType="java.lang.Long" parameterType="java.lang.Long">
        select
            count(1)
        from
            t_video_like
        where
            videoId = #{videoId};
    </select>

添加收藏

SpringBoot2.x仿B站第四章打造高性能的视频与弹幕系统笔记及源码_第11张图片


    /**
     * 添加收藏
     * @param videoCollection
     * @return
     */
    @PostMapping("/video-collections")
    public JsonResponse<String>addVideoCollection(@RequestBody VideoCollection videoCollection){
        Long userId = userSupport.getCurrentUserId();
        videoCollection.setId(userId);
        videoService.addVideoCollection(videoCollection,userId);
        return JsonResponse.success();
    }
  • service
  • 添加收藏可以像之前一样,进行先删除在添加
    @Transactional
    public void addVideoCollection(VideoCollection videoCollection, Long userId) {
        Long videoId = videoCollection.getVideoId();
        Long groupId = videoCollection.getGroupId();
        if(videoId==null || groupId==null){
            throw  new ConditionalException("异常参数");
        }
        //查询下video是否在
        Video video = videoDao.getVideoById(videoId);
        if(video==null){
            throw new ConditionalException("非法视频");
        }
        //删除原有视频收藏
        videoDao.deleteVideoCollection(videoId, userId);
        //添加新的视频收藏
        videoCollection.setUserId(userId);
        videoCollection.setCreateTime(new Date());
        videoDao.addVideoCollection(videoCollection);
    }

  • dao
    Integer deleteVideoCollection(@Param("videoId") Long videoId,@Param("userId") Long userId);

    Integer addVideoCollection(VideoCollection videoCollection);

-xml



    <insert id="addVideoCollection" parameterType="com.imooc.bilibili.domain.VideoCollection">
        insert into
            t_video_collection(
                id,
                videoId,
                userId,
                groupId,
                createTime
        )values(
                #{id},
                #{videoId},
                #{userId},
                #{groupId},
                #{createTime}
                       )

    insert>
        
    <delete id="deleteVideoCollection">
        delete
        from
            t_video_collection
        where
              videoId = #{videoId}
          and userId = #{userId};
    delete>

查看收藏

  • Service层
  • 类似于我们的查看点赞
    //查询收藏数量
    public Map getVideoCollections(Long videoId, Long userId) {
        Long count = videoDao.getVideoCollections(videoId);
        //查询是否收藏
        VideoCollection videoCollection = videoDao.getVideoCollectionByVideoIdAndUserId(videoId, userId);
        boolean like = videoCollection != null;
        Map result = new HashMap<>();
        result.put("count",count);
        result.put("like",like);
        return result;
    }
  • dao
   Long getVideoCollections(Long videoId);

    VideoCollection getVideoCollectionByVideoIdAndUserId(Long videoId, Long userId);
  • xml
    <select id="getVideoCollections" resultType="java.lang.Long" parameterType="java.lang.Long">
        select
            count(1)
        from
            t_video_collection
        where
            videoId = #{videoId};
    select>

    
    <select id="getVideoCollectionByVideoIdAndUserId" resultType="com.imooc.bilibili.domain.VideoCollection">
        select
            *
        from
            t_video_collection
        where
            videoId = #{videoId} and userId = #{userId};
    select>

添加视频投币

  • 添加都是传userId和对应的参数
    /**
     * 添加视频投币
     * @param videoCoin
     * @return
     */
     @PostMapping("/video-coins")
     public JsonResponse<String>addVideoCoins(@RequestBody VideoCoin videoCoin){
        Long userId = userSupport.getCurrentUserId();
        videoService.addVideoCoins(videoCoin,userId);
        return JsonResponse.success();
     }
  • service

思路:对于视频的投币,根据视频参数的验证后,我们需要对其用户硬币数量进行校验,是否足够投,如果足够,那么视频是第一次还是第二次投币的问题

    @Transactional
    public void addVideoCoins(VideoCoin videoCoin, Long userId) {
        Integer amount = videoCoin.getAmount();//要投币的数量
        Long videoId = videoCoin.getVideoId();
        if(userId==null || videoId==null){
            throw new ConditionalException("非法参数");
        }
        Video video = videoDao.getVideoById(videoId);
        if(video==null){
            throw new ConditionalException("视频失效");
        }
        //查询当前用户硬币数量
        Integer userCoinAmount =  userCoinService.getUserCoinAmount(userId);
        userCoinAmount = userCoinAmount==null? 0:userCoinAmount;
        if(amount > userCoinAmount){
            throw new ConditionalException("账户硬币余额不足");
        }
        //开始进行投币,已经排除游客投币,余额不足
        //查询该用户对该视频投过多少币
        VideoCoin dbVideoCoin = videoDao.getVideoCoinByVideoIdAndUserId(videoId, userId);
        //新增投币数量
        if(dbVideoCoin==null){
            videoCoin.setUserId(userId);
            videoCoin.setCreateTime(new Date());
            videoDao.addVideoCoin(videoCoin);
        }else{
            Integer dbAmount = dbVideoCoin.getAmount();//视频硬币书
            dbAmount+=amount;
            videoCoin.setUserId(userId);
            videoCoin.setAmount(dbAmount);
            videoCoin.setUpdateTime(new Date());
            videoDao.updateVideoCoin(videoCoin);
        }
        userCoinService.updateUserCoinsAmount(userId,(userCoinAmount-amount));
    }
  • userCoinService

@Service
public class UserCoinService {
    @Autowired
    private UserCoinDao userCoinDao;

    public Integer getUserCoinAmount(Long userId) {
        return userCoinDao.getUserCoinAmount(userId);
    }


    public void updateUserCoinsAmount(Long userId, Integer amount) {
        Date updateTime = new Date();
        userCoinDao.updateUserCoinAmount(userId,amount,updateTime);
    }
}
- userCoinDao
```java
@Mapper
public interface UserCoinDao {

    Integer getUserCoinAmount(Long userId);

    Integer updateUserCoinAmount(@Param("userId") Long userId
                                ,@Param("amount") Integer amount
                                ,@Param("updateTime") Date updateTime);
}
  • xml
<mapper namespace="com.imooc.bilibili.dao.UserCoinDao">
    
    <update id="updateUserCoinAmount">
        update
            t_user_coin
        set
            amount = #{amount},
            updateTime = #{updateTime}
        where
            userId = #{userId}
    update>


    <select id="getUserCoinAmount" resultType="java.lang.Integer" parameterType="java.lang.Long">
        select
            amount
        from
            t_user_coin
        where
            userId = #{userId}
    select>
mapper>
-----------------------
   
    <select id="getVideoCoinByVideoIdAndUserId" resultType="com.imooc.bilibili.domain.VideoCoin">
        select
            *
        from
            t_video_coin
        where videoId = #{videoId} and userId = #{userId};
    select>
    
    <select id="getVideoCoinsAmount" resultType="java.lang.Long" parameterType="java.lang.Long">
        select
            sum(amount)
        from
            t_video_coin
        where
            videoId = #{videoId};
    select>
        
    <update id="updateVideoCoin" parameterType="com.imooc.bilibili.domain.VideoCoin">
        update
            t_video_coin
        set
            amount = #{amount},
            updateTime = #{updateTime}
        where videoId = #{videoId}
                and userId = #{userId};
    update>
  • 查询投币的数量
    //查询硬币数量
    public Map<String, Object> getVideoCoins(Long videoId, Long userId) {
        Long count = videoDao.getVideoCoinsAmount(videoId);
        VideoCoin videoCollection = videoDao.getVideoCoinByVideoIdAndUserId(videoId, userId);
        boolean like = videoCollection != null;
        Map<String, Object> result = new HashMap<>();
        result.put("count", count);
        result.put("like", like);
        return result;
    }

评论分页请求

  • Domain
@Data
public class VideoComment {
    private Long id;
    private Long videoId;
    private Long userId;
    private String comment;
    private Long replyUserId;
    private Long rootId;
    private Date createTime;
    private Date updateTime;
    private List<VideoComment>childList;
    private UserInfo userInfo;
    private UserInfo replyUserInfo;
}
  • controller从前端拿到对应的size,no,videoId
     @GetMapping("/video-comments")
     public JsonResponse<PageResult<VideoComment>>pageListVideoComments(@RequestParam Integer size,
                                                                        @RequestParam Integer no,
                                                                        @RequestParam Long videoId){
         PageResult<VideoComment>result = videoService.pageListVideoComments(size,no,videoId);
         return new JsonResponse<>(result);

  • service层(difficult)

B站用户的评论,分为1级和二级评论,每个评论下有多个二级评论,我们的需求是查出评论下的全部信息,这是一个列表

    /**
     * 评论分页
     * @param size
     * @param no
     * @param videoId
     * @return
     */
    public PageResult<VideoComment> pageListVideoComments(Integer size, Integer no, Long videoId) {
        Video video = videoDao.getVideoById(videoId);
        if(video==null){
            throw new ConditionalException("非法视频!");
        }
        Map<String,Object>params = new HashMap<>();
        params.put("start",(no-1)*size);
        params.put("limit",size);
        params.put("videoId",videoId);
        //计算评论总数
        Integer total =  videoDao.pageCountVideoComments(params);
        List<VideoComment>list = new ArrayList<>();
        if(total > 0){
            //1.获得评论分页数据
            list = videoDao.pageListVideoComments(params);
            //2.批量查询二级评论
            List<Long>parentIdList = list.stream().map(VideoComment::getId).collect(Collectors.toList());
            List<VideoComment>childCommentList = videoDao.batchGetVideoCommentsByRootIds(parentIdList);
            //批量查询用户信息
            Set<Long> userIdList = list.stream().map(VideoComment::getUserId).collect(Collectors.toSet());
//            Set replyUserId = childCommentList.stream().map(VideoComment::getUserId).collect(Collectors.toSet());
            Set<Long> replyUserIdList = childCommentList.stream().map(VideoComment::getReplyUserId).collect(Collectors.toSet());
            userIdList.addAll(replyUserIdList);
//            userIdList.addAll(childUserIdList);
            //根据评论下的各个userId查到对应的详细信息
            List<UserInfo>userInfoList = userService.batchGetUserInfoByUserIds(userIdList);
            Map<Long, UserInfo> userInfoMap = userInfoList.stream().collect(Collectors.toMap(UserInfo::getUserId, userInfo -> userInfo));
            //将一级评论下的二级评论也查出来
            list.forEach(comment->{
                Long id = comment.getId();
                List<VideoComment>childList = new ArrayList<>();
                childCommentList.forEach(child->{
                    if(id.equals(child.getRootId())){
                        child.setUserInfo(userInfoMap.get(child.getUserId()));
                        child.setReplyUserInfo(userInfoMap.get(child.getReplyUserId()));
                        childList.add(child);
                    }
                });
                comment.setChildList(childList);
                comment.setUserInfo(userInfoMap.get(comment.getUserId()));
            });
        }
            return new PageResult<>(total,list);
    }
  • dao
  Integer pageCountVideoComments(Map<String, Object> params);

    List<VideoComment> pageListVideoComments(Map<String, Object> params);

    List<VideoComment> batchGetVideoCommentsByRootIds(@Param("rootIdList") List<Long> rootIdList);

    Video getVideoDetails(Long id);
  • xml
   
    <select id="pageCountVideoComments" resultType="java.lang.Integer" parameterType="java.util.Map">
        select
            count(1)
        from
            t_video_comment
        where
            videoId = #{videoId}
            and rootId is null;
    select>

    
    <select id="pageListVideoComments" resultType="com.imooc.bilibili.domain.VideoComment" parameterType="java.util.Map">
        select
            *
        from
            t_video_comment
        where
            videoId = #{videoId}
            and rootId is null
            order by id desc
            limit #{start},#{limit}
    select>

    
    <select id="batchGetVideoCommentsByRootIds" resultType="com.imooc.bilibili.domain.VideoComment" parameterType="java.util.List">
        select
            *
        from
            t_video_comments
        where rootId in
        <foreach collection="rootIdList" item="rootId" open="(" close=")" separator=",">
            #{rootId}
        foreach>
        order by id;
    select>
       
    <select id="batchGetUserInfoByUserIds" resultType="com.imooc.bilibili.domain.UserInfo" >
        select
            *
        from
            t_user_info
        where
            userId in
        <foreach collection="userIdList" item="userId" open="(" close=")" separator=",">
            #{userId}
        foreach>
    select>

4.弹幕系统

SpringBoot2.x仿B站第四章打造高性能的视频与弹幕系统笔记及源码_第12张图片
SpringBoot2.x仿B站第四章打造高性能的视频与弹幕系统笔记及源码_第13张图片
SpringBoot2.x仿B站第四章打造高性能的视频与弹幕系统笔记及源码_第14张图片
SpringBoot2.x仿B站第四章打造高性能的视频与弹幕系统笔记及源码_第15张图片

4.1查看弹幕

1.创建websocketService

  • websocket是多例的,而spring是单例模式
@Component
@ServerEndpoint("/imserver/{token}")
public class WebSocketService {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    private static final AtomicInteger ONLINE_COUNT = new AtomicInteger(0);//安全性高

    public static final ConcurrentHashMap<String,WebSocketService>WEBSOCKET_MAP = new ConcurrentHashMap<>();//保证线程安全

    private Long userId;

    private Session session;

    private String sessionId;

    //构建多实例的socket bean
    private static ApplicationContext APPLICATION_CONTEXT;

    public static void setApplicationContext(ApplicationContext applicationContext){
        WebSocketService.APPLICATION_CONTEXT = applicationContext;
    }
    @OnOpen
    public void openConnection(Session session, @PathParam("token") String token){
        try {
            userId = TokenUtils.verifyToken(token);
        }catch (Exception ignored){}
        RedisTemplate<String,String>redisTemplate = (RedisTemplate)WebSocketService.APPLICATION_CONTEXT.getBean("redisTemplate");
        redisTemplate.opsForValue().get("sakura");
        this.sessionId = session.getId();
        this.session = session;
        if(WEBSOCKET_MAP.containsKey(sessionId)){
            WEBSOCKET_MAP.remove(sessionId);
            WEBSOCKET_MAP.put(sessionId,this);
        }else{
            WEBSOCKET_MAP.put(sessionId,this);
            ONLINE_COUNT.getAndIncrement();
        }
        logger.info("用户连接成功:"+sessionId + ",当前在线人数为:" + ONLINE_COUNT.get());
        try{
            this.sendMessage("0");
        }catch(Exception e){
            logger.error("连接异常");
        }
    }

    @OnClose
    public void closeConnection(){
        if(WEBSOCKET_MAP.containsKey(sessionId)){
            WEBSOCKET_MAP.remove(sessionId);
            ONLINE_COUNT.getAndDecrement();
        }
        logger.info("用户退出"+sessionId+"当前在线人数为:"+ONLINE_COUNT.get());
    }

    @OnMessage
    public void onMessage(String message){
        logger.info("用户信息:" + sessionId + ",报文:" + message);
        if(!StringUtils.isNullOrEmpty(message)){
            try{
                //群发消息
                for(Map.Entry<String, WebSocketService> entry : WEBSOCKET_MAP.entrySet()){
                    WebSocketService webSocketService = entry.getValue();
                    //获得生产者
                    DefaultMQProducer danmusProducer = (DefaultMQProducer) APPLICATION_CONTEXT.getBean("danmusProducer");
                    JSONObject jsonObject = new JSONObject();
                    jsonObject.put("message",message);
                    jsonObject.put("sessionId",webSocketService.getSessionId());
                    Message msg = new Message(UserMomentsConstant.TOPIC_DANMUS, jsonObject.toJSONString().getBytes(StandardCharsets.UTF_8));
                    RocketMQUtil.asyncSengMsg(danmusProducer,msg);
                }
                if(this.userId != null){
                    //保存弹幕到数据库
                    Danmu danmu = JSONObject.parseObject(message, Danmu.class);
                    danmu.setUserId(userId);
                    danmu.setCreateTime(new Date());
                    DanmuService danmuService = (DanmuService)APPLICATION_CONTEXT.getBean("danmuService");
                    danmuService.asyncAddDanmu(danmu);
                    //保存弹幕到redis
                    danmuService.addDanmusToRedis(danmu);
                }
            }catch (Exception e){
                logger.error("弹幕接收出现问题");
                e.printStackTrace();
            }
        }

    }

    @Scheduled(fixedRate = 5000)
    public void noticeOnlineCount0() throws IOException {
        for(Map.Entry<String,WebSocketService>entry:WEBSOCKET_MAP.entrySet()){
            WebSocketService webSocketService = entry.getValue();
            if(webSocketService.getSession().isOpen()){
                JSONObject jsonObject =new JSONObject();
                jsonObject.put("onlineCount",ONLINE_COUNT.get());
                jsonObject.put("msg","当前在线人数"+ONLINE_COUNT.get());
                webSocketService.sendMessage(jsonObject.toString());
            }
        }
    }

    @OnError
    public void onError(Throwable error){

    }

    public void sendMessage(String message) throws IOException {
        this.session.getBasicRemote().sendText(message);
    }

    public Session getSession() {
        return session;
    }

    public String getSessionId() {
        return sessionId;
    }
}


  • danmu表
    SpringBoot2.x仿B站第四章打造高性能的视频与弹幕系统笔记及源码_第16张图片

  • 业务层开发

@RestController
public class DanmuApi {
    @Autowired
    private UserSupport userSupport;

    @Autowired
    private DanmuService danmuService;
    @GetMapping("/danmus")
    public JsonResponse<List<Danmu>>getDanmus(@RequestParam Long videoId,
                                               String startTime,
                                                String endTime) throws Exception {
        List<Danmu>list;
        try{
            userSupport.getCurrentUserId();
            list = danmuService.getDanmus(videoId,startTime,endTime);
        }catch(Exception ignored){
            list = danmuService.getDanmus(videoId,null,null);
        }
        return new JsonResponse<>(list);
    }
}
  • service
@Service
public class DanmuService {

    @Autowired
    private DanmuDao danmuDao;

    @Autowired
    private RedisTemplate<String,String>redisTemplate;

    public static final String DANMU_KEY = "danmu-video-";

    public void addDanmu(Danmu danmu){
        danmuDao.addDanmu(danmu);
    }

    @Async
    public void asyncAddDanmu(Danmu danmu){
        danmuDao.addDanmu(danmu);
    }

    /**
     * 查询策略,应优先查询redis的弹幕数据
     * 如果reids没有在查询数据库
     * @param videoId
     * @param startTime
     * @param endTime
     * @return
     */
    public List<Danmu>getDanmus(Long videoId,
                                String startTime,
                                String endTime) throws Exception {
        String key = DANMU_KEY + videoId;
        String value = redisTemplate.opsForValue().get(key);
        List<Danmu>list;
        if(!StringUtils.isNullOrEmpty(value)){
            //通过redis获取到对应的list
            list = JSONArray.parseArray(value,Danmu.class);
            if(!StringUtils.isNullOrEmpty(startTime) && !StringUtils.isNullOrEmpty(endTime)){
                SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                Date startDate = sdf.parse(startTime);
                Date endDate = sdf.parse(endTime);
                List<Danmu>childList = new ArrayList<>();
                for(Danmu danmu:list){
                    Date createTime = danmu.getCreateTime();
                    if(createTime.after(startDate) && createTime.before(endDate)){
                        childList.add(danmu);
                    }
                }
                list = childList;
            }
        }else{
            Map<String,Object>params = new HashMap<>();
            params.put("videoId",videoId);
            params.put("startTime",startTime);
            params.put("endTime",endTime);
            list = danmuDao.getDanmus(params);
            redisTemplate.opsForValue().set(key,JSONObject.toJSONString(list));
        }
        return list;
    }

    public void addDanmusToRedis(Danmu danmu) {
        String key = DANMU_KEY+danmu.getVideoId();
        String value = redisTemplate.opsForValue().get(key);
        List<Danmu>list = new ArrayList<>();
        if(!StringUtils.isNullOrEmpty(value)){
            list = JSONArray.parseArray(value,Danmu.class);
        }
        list.add(danmu);
        redisTemplate.opsForValue().set(key, JSONObject.toJSONString(danmu));
    }
}
  • dao层
@Mapper
public interface DanmuDao {


     Integer addDanmu(Danmu danmu);

     List<Danmu> getDanmus(Map<String, Object> params);
}

  • xml
<mapper>
    <insert id="addDanmu" parameterType="com.imooc.bilibili.domain.Danmu">
        insert into
            t_danmu(
                userId,
                videoId,
                content,
                danmuTime,
                createTime
        )values(
                #{userId},
                #{videoId},
                #{content},
                #{danmuTime},
                #{createTime}
                       )
    insert>
    <select id="getDanmus" resultType="com.imooc.bilibili.domain.Danmu" parameterType="java.util.Map">
        select
        *
        from
        t_danmu
        where
        videoId = #{videoId}
        <if test="startTime != null and startTime != '' ">
            and createTime =]]> #{startTime}
        if>
        <if test="endTime != null and endTime != '' ">
            and createTime  #{endTime}
        if>
    select>
mapper>

你可能感兴趣的:(SpringBoot项目,服务器,java,springboot)