这是我参与「第五届青训营 」伴学笔记创作活动的第 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的技巧,更重要的是懂得了代码规范和沟通协作的重要性,协作是比较难的事情,希望自己在以后的项目开发中能做的更好。