极简版抖音项目 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