极简版抖音项目 feed 的实现 | 青训营笔记

487 阅读2分钟

极简版抖音项目 feed 的实现

这是我参与「第三届青训营 -后端场」笔记创作活动的第5篇笔记。

01 简介

主要介绍一下 feed 视频流的实现。

02 逻辑推导

需求:不限制登录状态,返回按投稿时间倒序的视频列表,视频数由服务端控制,单次最多30个。

不限制登陆状态:表明无论是否登陆都需要返回相应的视频数据。只是如果是登录的话,就要将该用户和所刷到的视频是否点赞、和该视频的作者是否关注等信息一并返回。

倒序:

  • 假设现在是12点,在所有的视频当中最新发布的视频是11点发布的,然后根据 latest_time ,不填表示当前时间,即就是12点。然后跟查询小于或等于12点的30个视频并倒序排序。
  • 假设在返回的这30个视频当中,最早发布的是10点,然后在这一轮就将这个10点作为 NextTime 返回,作为下一次的 latest_time 。那么下一次查询的时候就只需要从小于或等于10点开始再次查询30个就好了。
  • 假设一共有31个视频。由于是倒序,那么第31个视频,如果一直查小于或等于,那么永远只返回第31个视频,就永远只能查到一个。所以,这时加个判断条件:只能查到一个的时候,就直接从头开始返还30个视频数据就好了。

03 实现

部分代码说明:

1、将string转换为int64:latestTime, err := strconv.ParseInt(latestTimeString, 10, 64)

2、获取当前时间:time.Now()

3、转换成时间戳:Unix()

4、int64 转换成时间戳并转换成 "2006-01-02 15:04:05" 格式:

 timeLayout := "2006-01-02 15:04:05"
 time.Unix(latestTime, 0).Format(timeLayout))

controller/feed.go

 package controller
 ​
 import (
     "douyin/dao"
     "douyin/model"
     "fmt"
     "github.com/gin-gonic/gin"
     "log"
     "net/http"
     "strconv"
     "time"
 )
 ​
 type FeedResponse struct {
     model.Response
     VideoList []model.Video `json:"video_list,omitempty"`
     NextTime  int64         `json:"next_time,omitempty"`
 }
 ​
 func Feed(c *gin.Context) {
     token := c.Query("token")
     user, exist := TokenIsValid(token)
 ​
     // 可选参数,限制返回视频的最新投稿时间戳,精确到秒,不填表示当前时间
     latestTimeString := c.Query("latest_time")
     latestTime, err := strconv.ParseInt(latestTimeString, 10, 64)
     if err != nil {
         log.Println(err)
     }
 ​
     videos, err := dao.Mgr.GetAllVideo(latestTime)
     if err != nil {
         log.Println("GetAllVideo:", err)
     }
 ​
     videoLen := len(videos)
     fmt.Println("--------Number of video records returned-------", len(videos))
     if videoLen == 0 {
         c.JSON(http.StatusOK, FeedResponse{
             Response:  model.Response{StatusCode: 0},
             VideoList: DemoVideos,
             NextTime:  time.Now().Unix(),
         })
         return
     }
 ​
     // 如果已登录
     if exist {
         for i := 0; i < videoLen; i++ {
             b, _ := dao.Mgr.IsFavorite(user.Id, videos[i].Id)
             if b {
                 videos[i].IsFavorite = true
             } else {
                 videos[i].IsFavorite = false
             }
 ​
             b, _ = dao.Mgr.IsFollow(user.Id, videos[i].Author.Id)
             if b {
                 videos[i].Author.IsFollow = true
             } else {
                 videos[i].Author.IsFollow = false
             }
         }
     }
 ​
     // 本次结构体数组最后一个的下标,
     earliestTime := videoLen - 1
     // NextTime 本次返回的视频中,发布最早的时间,作为下次请求时的latest_time
     c.JSON(http.StatusOK, FeedResponse{
         Response:  model.Response{StatusCode: 0},
         VideoList: videos,
         NextTime:  videos[earliestTime].Model.CreatedAt.Unix(),
     })
 ​
 }
 ​

dao/feed.go

 package dao
 ​
 import (
     "douyin/model"
     "douyin/pkg/constrant"
     "fmt"
     "time"
 )
 ​
 func (mgr manager) GetAllVideo(latestTime int64) ([]model.Video, error) {
     var videos []model.Video
     timeLayout := "2006-01-02 15:04:05"
     var count int64
     mgr.db.Model(&model.Video{}).Count(&count)
     fmt.Println("---Count---", count)
     // "created_at <= ?" : 按道理是 <= ,下面会判断当视频刷完时,只能刷到最开始那个,因为是 <=
     result := mgr.db.Model(&model.Video{}).Where("created_at <= ?", time.Unix(latestTime, 0).Format(timeLayout)).
         Order("created_at DESC").Preload("Author").Limit(30).Count(&count).Find(&videos)
 ​
     videoLen := len(videos)
     for i := 0; i < videoLen; i++ {
         videos[i].PlayUrl = StrBuilder(constrant.Root, videos[i].PlayUrl)
         videos[i].CoverUrl = StrBuilder(constrant.Root, videos[i].CoverUrl)
     }
 ​
     if count == 1 {
         //  是小于等于最早时间只能查到1个视频记录, 证明已经刷完了
         result := mgr.db.Model(&model.Video{}).Order("created_at DESC").
             Preload("Author").Limit(30).Count(&count).Find(&videos)
         videoLen = len(videos)
         for i := 0; i < videoLen; i++ {
             videos[i].PlayUrl = StrBuilder(constrant.Root, videos[i].PlayUrl)
             videos[i].CoverUrl = StrBuilder(constrant.Root, videos[i].CoverUrl)
         }
         return videos, result.Error
     }
     return videos, result.Error
 }
 ​
 // IsFavorite 一个人访问另一个人的视频,查询是否点赞
 func (mgr manager) IsFavorite(userID int64, videoId int64) (bool, error) {
     var favorite model.Favorite
     var count int64
 ​
     if err := mgr.db.Where("user_id = ? AND video_id = ?", userID, videoId).
         Find(&favorite).Count(&count).Error; err != nil {
         return false, err
     }
 ​
     if count > 0 {
         return true, nil
     }
 ​
     return false, nil
 }
 ​
 func (mgr manager) IsFollow(userId, toUserId int64) (bool, error) {
     var count int64
     var follow model.Follow
     if err := mgr.db.Where("user_id = ? AND follow_id = ?", userId, toUserId).
         Find(&follow).Count(&count).Error; err != nil {
         log.Println(err)
         return false, err
     }
 ​
     if count > 0 {
         return true, nil
     }
 ​
     return false, nil
 }
 ​
 func StrBuilder(first string, finalName string) string {
     var builder strings.Builder
 ​
     builder.WriteString(first)
     builder.WriteString(finalName)
 ​
     return builder.String()
 }

04 总结

在本次feed接口的实现当中,任然存在一些不足,比如说使用for循环拼接地址、if-else频繁使用等。

还有许多改进空间,继续加油!

我们组的地址:douyin