这是我参与「第五届青训营 」伴学笔记创作活动的第 16 天
抖音大项目概要 - feed流
一、路由分发
前端获取操作的路由,然后分发给FeedVideoListHandler函数,但是此业务是对任何用户都有的,因此是不用jwt鉴权的
r := gin.Default()
r.Static("static", "./static")
baseGroup := r.Group("/douyin")
baseGroup.GET("/feed/", video.FeedVideoListHandler)
二、有关video的结构体
service包
type FeedVideoList struct {
Videos []*models.Video `json:"video_list,omitempty"`
NextTime int64 `json:"next_time,omitempty"`
}
- video的信息
- video的上传时间
type QueryFeedVideoListFlow struct {
userId int64
latestTime time.Time
videos []*models.Video
nextTime int64
feedVideo *FeedVideoList
}
query获取的视频信息的结构体
handlers包
type FeedResponse struct {
models.CommonResponse
*video.FeedVideoList
}
- 返回信息
- video的信息列表
type ListResponse struct {
models.CommonResponse
*video.List
}
- 返回信息
- 视频拉取列表
models包
type Video struct {
Id int64 `json:"id,omitempty"` // 视频id
UserInfoId int64 `json:"-"`// 用户id
Author UserInfo `json:"author,omitempty" gorm:"-"` // 视频的作者(多对一)
PlayUrl string `json:"play_url,omitempty"` // 视频播放url
CoverUrl string `json:"cover_url,omitempty"`// 图片展示的url
FavoriteCount int64 `json:"favorite_count,omitempty"`// 喜爱的数量
CommentCount int64 `json:"comment_count,omitempty"`// 评论数量
IsFavorite bool `json:"is_favorite,omitempty"`// 是否喜爱
Title string `json:"title,omitempty"` // 视频标题
Users []*UserInfo `json:"-" gorm:"many2many:user_favor_videos;"` // 用户信息
Comments []*Comment `json:"-"` // 评论信息
CreatedAt time.Time `json:"-"` // 创建时间
UpdatedAt time.Time `json:"-"` // 更新时间
}
视频需要有评论、点赞、播放、封面等操作,上面结构体就包括了这一些
三、返回信息设置
返回一个结构体,第一个元素是http协议的状态码,第二个元素是返回的信息
type CommonResponse struct {
StatusCode int32 `json:"status_code"`
StatusMsg string `json:"status_msg,omitempty"`
}
针对feed的返回信息
type FeedResponse struct {
models.CommonResponse
*video.FeedVideoList
}
四、handlers层
- 解析得到参数
- 调用下层逻辑
设置视频feed的最大数量
const (
MaxVideoNum = 30
)
外部函数
strconv .ParseInt
func ParseInt(s string, base int, bitSize int) (i int64, err error)
- 参数1 数字的字符串形式
- 参数2 数字字符串的进制
- 参数3 返回结果的bit大小
intTime, err := strconv.ParseInt(rawTimestamp, 10, 64)
将前端返回的时间的二进制数据解析成十进制数据
time.Unix
func Unix(sec int64, nsec int64) Time
- 秒
- 纳秒
获取时间戳
latestTime = time.Unix(0, intTime*1e6) //注意:前端传来的时间戳是以ms为单位的
解析信息
未登录
没有登录,即没有颁发token
func (p *ProxyFeedVideoList) DoNoToken() error {
rawTimestamp := p.Query("latest_time")
var latestTime time.Time
intTime, err := strconv.ParseInt(rawTimestamp, 10, 64)
if err == nil {
latestTime = time.Unix(0, intTime*1e6) //注意:前端传来的时间戳是以ms为单位的
}
videoList, err := video.QueryFeedVideoList(0, latestTime)
if err != nil {
return err
}
p.FeedVideoListOk(videoList)
return nil
}
已登录
func (p *ProxyFeedVideoList) DoHasToken(token string) error {
//解析成功
if claim, ok := middleware.ParseToken(token); ok {
//token超时
if time.Now().Unix() > claim.ExpiresAt {
return errors.New("token超时")
}
rawTimestamp := p.Query("latest_time")
var latestTime time.Time
intTime, err := strconv.ParseInt(rawTimestamp, 10, 64)
if err != nil {
latestTime = time.Unix(0, intTime*1e6) //注意:前端传来的时间戳是以ms为单位的
}
//调用service层接口
videoList, err := video.QueryFeedVideoList(claim.UserId, latestTime)
if err != nil {
return err
}
p.FeedVideoListOk(videoList)
return nil
}
//解析失败
return errors.New("token不正确")
}
信息解析完后就调用service层
until包,填充用户是否喜欢该视频的信息
func FillVideoListFields(userId int64, videos *[]*models.Video) (*time.Time, error) {
size := len(*videos)
if videos == nil || size == 0 {
return nil, errors.New("util.FillVideoListFields videos为空")
}
dao := models.NewUserInfoDAO()
p := cache.NewProxyIndexMap()
latestTime := (*videos)[size-1].CreatedAt //获取最近的投稿时间
//添加作者信息,以及is_follow状态
for i := 0; i < size; i++ {
var userInfo models.UserInfo
err := dao.QueryUserInfoById((*videos)[i].UserInfoId, &userInfo)
if err != nil {
continue
}
userInfo.IsFollow = p.GetUserRelation(userId, userInfo.Id) //根据cache更新是否被点赞
(*videos)[i].Author = userInfo
//填充有登录信息的点赞状态
if userId > 0 {
(*videos)[i].IsFavorite = p.GetVideoFavorState(userId, (*videos)[i].Id)
}
}
return &latestTime, nil
}
成功返回
func (p *ProxyFeedVideoList) FeedVideoListOk(videoList *video.FeedVideoList) {
p.JSON(http.StatusOK, FeedResponse{
CommonResponse: models.CommonResponse{
StatusCode: 0,
},
FeedVideoList: videoList,
},
)
}
//拉取成功
func (p *ProxyQueryVideoList) QueryVideoListOk(videoList *video.List) {
p.c.JSON(http.StatusOK, ListResponse{
CommonResponse: models.CommonResponse{
StatusCode: 0,
},
List: videoList,
})
}
错误返回
func (p *ProxyFeedVideoList) FeedVideoListError(msg string) {
p.JSON(http.StatusOK, FeedResponse{CommonResponse: models.CommonResponse{
StatusCode: 1,
StatusMsg: msg,
}})
}
//拉取失败
func (p *ProxyQueryVideoList) QueryVideoListError(msg string) {
p.c.JSON(http.StatusOK, ListResponse{CommonResponse: models.CommonResponse{
StatusCode: 1,
StatusMsg: msg,
}})
}
拉取视频列表
func QueryVideoListHandler(c *gin.Context) {
p := NewProxyQueryVideoList(c)
rawId, _ := c.Get("user_id")
err := p.DoQueryVideoListByUserId(rawId)
if err != nil {
p.QueryVideoListError(err.Error())
}
}
五、service层
- 检查参数
- 准备数据
- 打包数据
检查userid是否有效
func (q *QueryFeedVideoListFlow) checkNum() {
//上层通过把userId置零,表示userId不存在或不需要
if q.userId > 0 {
//这里说明userId是有效的,可以定制性的做一些登录用户的专属视频推荐
}
if q.latestTime.IsZero() {
q.latestTime = time.Now()
}
}
准备数据、打包数据
func (q *QueryFeedVideoListFlow) prepareData() error {
err := models.NewVideoDAO().QueryVideoListByLimitAndTime(MaxVideoNum, q.latestTime, &q.videos)
if err != nil {
return err
}
//如果用户为登录状态,则更新该视频是否被该用户点赞的状态
latestTime, _ := util.FillVideoListFields(q.userId, &q.videos) //不是致命错误,不返回
//准备好时间戳
if latestTime != nil {
q.nextTime = (*latestTime).UnixNano() / 1e6
return nil
}
q.nextTime = time.Now().Unix() / 1e6
return nil
}
func (q *QueryFeedVideoListFlow) packData() error {
q.feedVideo = &FeedVideoList{
Videos: q.videos,
NextTime: q.nextTime,
}
return nil
}
service层真正做的
func (q *QueryFeedVideoListFlow) Do() (*FeedVideoList, error) {
//所有传入的参数不填也应该给他正常处理
q.checkNum()
if err := q.prepareData(); err != nil {
return nil, err
}
if err := q.packData(); err != nil {
return nil, err
}
return q.feedVideo, nil
}
返回给handlers层的信息
func QueryFeedVideoList(userId int64, latestTime time.Time) (*FeedVideoList, error) {
return NewQueryFeedVideoListFlow(userId, latestTime).Do()
}
func NewQueryFeedVideoListFlow(userId int64, latestTime time.Time) *QueryFeedVideoListFlow {
return &QueryFeedVideoListFlow{userId: userId, latestTime: latestTime}
}
六、models
对数据库的增删改查
func (v *VideoDAO) QueryVideoByVideoId(videoId int64, video *Video) error {
if video == nil {
return errors.New("QueryVideoByVideoId 空指针")
}
return DB.Where("id=?", videoId).
Select([]string{"id", "user_info_id", "play_url", "cover_url", "favorite_count", "comment_count", "is_favorite", "title"}).
First(video).Error
}
func (v *VideoDAO) QueryVideoCountByUserId(userId int64, count *int64) error {
if count == nil {
return errors.New("QueryVideoCountByUserId count 空指针")
}
return DB.Model(&Video{}).Where("user_info_id=?", userId).Count(count).Error
}
func (v *VideoDAO) QueryVideoListByUserId(userId int64, videoList *[]*Video) error {
if videoList == nil {
return errors.New("QueryVideoListByUserId videoList 空指针")
}
return DB.Where("user_info_id=?", userId).
Select([]string{"id", "user_info_id", "play_url", "cover_url", "favorite_count", "comment_count", "is_favorite", "title"}).
Find(videoList).Error
}
// QueryVideoListByLimitAndTime 返回按投稿时间倒序的视频列表,并限制为最多limit个
func (v *VideoDAO) QueryVideoListByLimitAndTime(limit int, latestTime time.Time, videoList *[]*Video) error {
if videoList == nil {
return errors.New("QueryVideoListByLimit videoList 空指针")
}
return DB.Model(&Video{}).Where("created_at<?", latestTime).
Order("created_at ASC").Limit(limit).
Select([]string{"id", "user_info_id", "play_url", "cover_url", "favorite_count", "comment_count", "is_favorite", "title", "created_at", "updated_at"}).
Find(videoList).Error
}