青训营大项目总结(3) | 青训营笔记

73 阅读4分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 15 天
本文将介绍我参与编写的另外两个部分:评论接口和关系接口,以及搭建静态文件服务的方法和踩到的坑。

评论接口

/douyin/comment/action/ - 评论操作

接口描述:登录用户对视频进行评论。
前端发送的数据为:

message douyin_comment_action_request {
required string token = 1; // 用户鉴权token
required int64 video_id = 2; // 视频id
required int32 action_type = 3; // 1-发布评论,2-删除评论
optional string comment_text = 4; // 用户填写的评论内容,在action_type=1的时候使用
optional int64 comment_id = 5; // 要删除的评论id,在action_type=2的时候使用
}

token中包含了负载和用户签名,其中用户签名将在jwt中间件中完成校验,token负载里包含了当前登陆用户的信息,比如用户id,用户昵称,关注数等,这里主要用到的是用户id。
接口将根据action_type分别执行不同的操作,action_type为1时发布评论,使用Gorm在comments表里新增一条记录,记录评论用户的id,视频id和评论内容,同时数据库返回该记录的创建时间和自增的主键id,分别对应响应体中的create_time和comment_id。需要注意的是create_time的格式不能为时间戳也不能包含时刻信息,必须要格式化为“mm-dd”的形式。
action_type为2时删除评论,这时要根据请求体中的commnet_id去数据库内删除指定id的数据,这部分比较简单就不多赘述了。

/douyin/comment/list/ - 视频评论列表

接口描述:查看视频的所有评论,按发布时间倒序。

message douyin_comment_list_request {
required string token = 1; // 用户鉴权token
required int64 video_id = 2; // 视频id
}
message douyin_comment_list_response {
required int32 status_code = 1; // 状态码,0-成功,其他值-失败
optional string status_msg = 2; // 返回状态描述
repeated Comment comment_list = 3; // 评论列表
}

这个的接口的要求是根据video_id按照时间倒序返回该视频下所有的评论列表。接口比较简单,编写相应的数据库查询语句就可以了。将返回的结果存入commentsDal。

var commentsDal []Comment
err := tx.Model(&Comment{}).Order("create_time desc").Where("video_id=?", VideoId).Find(&commentsDal).Error

关系接口

/douyin/relatioin/follow/list/ - 用户关注列表

接口描述:登录用户关注的所有用户列表。

message douyin_relation_follow_list_request {
required int64 user_id = 1; // 用户id
required string token = 2; // 用户鉴权token
}

message douyin_relation_follow_list_response {
required int32 status_code = 1; // 状态码,0-成功,其他值-失败
optional string status_msg = 2; // 返回状态描述
repeated User user_list = 3; // 用户信息列表
}

用户关系表采用了多对多的方式,使用两个字段:from_id和to_id分别记录了关系者和被关注者的用户id。用户关注列表接口提供的是from_id,我们需要找到该from_id对应的所有to_id以及对应的用户信息,因此数据库的操作还会涉及到关系表和用户表的连接。以下是用到的代码:

var users []model.User
err := tx.Table("follow_relations").
   Select("users.id,users.name").
   Where("follow_relations.from_id = ?", fromId).
   Joins("left join users on to_id = users.id").Find(&users).Error

users数组中保存了所有from_id对应的to_id。这里有一个显示BUG,APP的用户关注列表中不仅会展示所有被关注者的名称,在名称右边还有一个“已关注”/“未关注”按钮,按钮显示的文字由user中的is_follow字段决定。用户关注列表中所有的用户必定是“已关注”状态的,然而关系表和用户表中是没有is_follow字段的,因此仅仅返回数据库数据无法正确将is_follow设置为true。这里,我遍历了一次users列表,手动将所有user的is_follow设置为了true。

for i := 0; i < len(users); i++ {
   users[i].IsFollow = true
}

/douyin/relation/follower/list/ - 用户粉丝列表

接口描述:所有关注登录用户的粉丝列表。

message douyin_relation_follower_list_request {
required int64 user_id = 1; // 用户id
required string token = 2; // 用户鉴权token
}

message douyin_relation_follower_list_response {
required int32 status_code = 1; // 状态码,0-成功,其他值-失败
optional string status_msg = 2; // 返回状态描述
repeated User user_list = 3; // 用户列表
}

用户粉丝接口的实现和关注接口很相似,连接关系表和用户表然后查询所有to_id对应的from_id。

var users []model.User
err := tx.Table("follow_relations").
   Select("users.id,users.name).
   Where("follow_relations.to_id = ?", toId).
   Joins("left join users on from_id = users.id").Find(&users).Error

此外,还需要遍历一次users列表,判断用户自己有没有关注粉丝。

for i := 0; i < len(users); i++ {
   res, _ := isFollow(DB, toId, users[i].Id)
   if res == true {
      users[i].IsFollow = true
   }
}

这里调用了isFollow函数,isFollow查询关系表中是否存在(to_id,from_id)记录。如果存在就说明用户也关注了该粉丝。

搭建静态文件服务

用户发布视频以后,视频会被保存在根目录下的public/video文件夹下,视频封面保存在public/photo下,具体的视频路径url则保存在video_meta下。feed函数从数据库中读取视频url,从public/video下加载视频。这里会遇到一个问题,视频的url是文件的路径,APP无法通过文件路径直接从项目目录下读取文件。
为了解决这个问题需要搭建静态文件服务,我们通过配置Hertz实现。

var VideoStoreRoot = "public"
var VideoRelateFsDir = VideoStoreRoot + "/video/"
var VideoCoverRelateFsDir = VideoStoreRoot + "/photo/"
var VideoReachRoot = "public"

func (*video) InitVideoFs(hertz *server.Hertz) {
   fs := &app.FS{Root: "./" + VideoStoreRoot, PathRewrite: getPathRewriter("/" + VideoReachRoot)}
   hertz.StaticFS("/"+VideoReachRoot, fs)
}

func getPathRewriter(prefix string) app.PathRewriteFunc {
   // Cannot have an empty prefix
   if prefix == "" {
      prefix = "/"
   }
   // Prefix always start with a '/' or '*'
   if prefix[0] != '/' {
      prefix = "/" + prefix
   }

   // Is prefix a direct wildcard?
   isStar := prefix == "/*"
   // Is prefix a partial wildcard?
   if strings.Contains(prefix, "*") {
      isStar = true
      prefix = strings.Split(prefix, "*")[0]
      // Fix this later
   }
   prefixLen := len(prefix)
   if prefixLen > 1 && prefix[prefixLen-1:] == "/" {
      // /john/ -> /john
      prefixLen--
      prefix = prefix[:prefixLen]
   }
   return func(ctx *app.RequestContext) []byte {
      path := ctx.Path()
      if len(path) >= prefixLen {
         if isStar && string(path[0:prefixLen]) == prefix {
            path = append(path[0:0], '/')
         } else {
            path = path[prefixLen:]
            if len(path) == 0 || path[len(path)-1] != '/' {
               path = append(path, '/')
            }
         }
      }
      if len(path) > 0 && path[0] != '/' {
         path = append([]byte("/"), path...)
      }
      return path
   }
}

参考代码:github.com/cloudwego/h…
这里我直接复制了开源代码,具体的实现原理不是很清楚,因此这里就不多做介绍了。

感谢

最后,我想对我的队友们(钟弋辰,黄廷禾,王子琦)表达感谢,simple_tiktok项目的完成是大家共同努力的结果。这是我第一次通过git和飞书云文档进行项目协作,simple_tiktok项目让我管中窥豹看到了企业项目开发的雏形,在这次的项目开发和课程学习中我学到了很多新知识以及处理bug的技巧,更重要的是懂得了代码规范和沟通协作的重要性,协作是比较难的事情,希望自己在以后的项目开发中能做的更好。