Feed流开发

356 阅读4分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 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层

  1. 解析得到参数
  2. 调用下层逻辑

设置视频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层

  1. 检查参数
  2. 准备数据
  3. 打包数据

检查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
}