极简版抖音项目的实现 | 青训营笔记
这是我参与「第五届青训营」伴学笔记创作活动的第 11 天
前言
本文大致介绍了本人及本人所在小组为第五届字节跳动青训营后端专场大项目需求 —— 「实现一个极简版抖音」的部分实现细节。
需求
本届后端青训营大项目要求实现一个极简版抖音的后端服务,该后端服务通过 HTTP 协议向已被设计好的前端 App 传递数据,并通过 URL Query 获得请求参数。
该服务大致有如下类别的接口:
- 用户鉴权
- 用户基本信息
- 用户社交
- 视频投稿
- 视频流
- 视频互动
项目梗概
TokTik 项目基于 Go 开发,采用微服务架构,由网关(Gateway)服务接受 HTTP 请求,将其转换为 RPC 调用后传入路由对应的其他服务。服务内部统一使用 RPC 调用进行数据交换。
TokTik 使用 protobuf 作为 IDL 语言,使用 gorm 作为 ORM 框架,使用 Kitex 作为 RPC 框架,使用 Hertz 作为 HTTP 框架,使用 Consul 进行服务注册与发现,使用 PostgreSQL 作为数据库,使用 Amazon S3 作为对象存储服务,使用 monkey 作为单测 mock 框架。
分工及版本控制
Toktik 项目使用 Git 作为版本控制工具,并通过 GitHub 托管代码。目前,本人在项目中负责“视频流”接口的实现。
实现
视频流接口的接口基本信息和定义如下:
不限制登录状态,返回按投稿时间倒序的视频列表,视频数由服务端控制,单次最多30个
Route: /douyin/feed/
Parameter:
- latest_time
- 位置:query
- 类型:string
- 必填:否
- 说明:可选参数,限制返回视频的最新投稿时间戳,精确到秒,不填表示当前时间
- token
- 位置:query
- 类型:string
- 必填:否
- 说明:可选参数,用户登录状态下设置
Response Model(JSON):
type ApifoxModel struct {
NextTime *int64 `json:"next_time"` // 本次返回的视频中,发布最早的时间,作为下次请求时的latest_time
StatusCode int64 `json:"status_code"`// 状态码,0-成功,其他值-失败
StatusMsg *string `json:"status_msg"` // 返回状态描述
VideoList []Video `json:"video_list"` // 视频列表
}
// Video
type Video struct {
Author User `json:"author"` // 视频作者信息
CommentCount int64 `json:"comment_count"` // 视频的评论总数
CoverURL string `json:"cover_url"` // 视频封面地址
FavoriteCount int64 `json:"favorite_count"`// 视频的点赞总数
ID int64 `json:"id"` // 视频唯一标识
IsFavorite bool `json:"is_favorite"` // true-已点赞,false-未点赞
PlayURL string `json:"play_url"` // 视频播放地址
Title string `json:"title"` // 视频标题
}
// 视频作者信息
//
// User
type User struct {
FollowCount int64 `json:"follow_count"` // 关注总数
FollowerCount int64 `json:"follower_count"`// 粉丝总数
ID int64 `json:"id"` // 用户id
IsFollow bool `json:"is_follow"` // true-已关注,false-未关注
Name string `json:"name"` // 用户名称
}
Toktik 使用一个简单的 shell 脚本来通过 kitex
生成规范的模板代码:
#!/usr/bin/bash
mkdir -p kitex_gen
kitex -module "toktik" -I idl/ idl/"$1".proto
mkdir -p service/"$1"
cd service/"$1" && kitex -module "toktik" -service "$1" -use toktik/kitex_gen/ -I ../../idl/ ../../idl/"$1".proto
go mod tidy
在 idl
目录创建 feed.proto
:
syntax = "proto3";
package douyin.feed;
option go_package = "douyin/feed";
import "user.proto";
message Video {
uint32 id = 1;
user.User author = 2;
string play_url = 3;
string cover_url = 4;
uint32 favorite_count = 5;
uint32 comment_count = 6;
bool is_favorite = 7;
string title = 8;
}
message ListFeedRequest {
optional string latest_time = 1;
optional string token = 2;
}
message ListFeedResponse {
uint32 status_code = 1;
optional string status_msg = 2;
optional int64 next_time = 3;
repeated Video videos = 4;
}
service FeedService {
rpc ListVideos(ListFeedRequest) returns (ListFeedResponse);
}
运行 sh ./add-kitex-service.sh feed
,得到生成的模板代码。
ListVideos
调用实现如下:
func (s *FeedServiceImpl) ListVideos(ctx context.Context, req *feed.ListFeedRequest) (resp *feed.ListFeedResponse, err error) {
publish := gen.Q.Video
latestTime, err := strconv.ParseInt(*req.LatestTime, 10, 64)
if err != nil {
latestTime = time.Now().UnixMilli()
}
find, err := publish.WithContext(ctx).Where(publish.CreatedAt.Lte(time.UnixMilli(latestTime))).Order(publish.CreatedAt.Desc()).Limit(30).Offset(0).Find()
if err != nil {
return nil, err
}
nextTime := find[len(find)].CreatedAt.UnixMilli()
var videos []*feed.Video
for _, m := range find {
u := &user.User{
Id: m.UserId,
// TODO: fill other fields
}
playUrl, err := storage.GetLink(m.FileName)
if err != nil {
_ = fmt.Errorf("failed to fetch play url: %w", err)
continue
}
coverUrl, err := storage.GetLink(m.CoverName)
if err != nil {
_ = fmt.Errorf("failed to fetch cover url: %w", err)
continue
}
videos = append(videos, &feed.Video{
Id: m.ID,
Author: u,
PlayUrl: playUrl,
CoverUrl: coverUrl,
// TODO: finish this
FavoriteCount: 0,
// TODO: finish this
CommentCount: 0,
// TODO: finish this
IsFavorite: false,
Title: m.Title,
})
}
return &feed.ListFeedResponse{
StatusCode: OkStatusCode,
StatusMsg: &OkStatusMsg,
NextTime: &nextTime,
Videos: videos,
}, nil
}
使用 gorm gen 查询 publish
接口定义的数据表字段并进行排序和裁剪,通过 s3
API 获取存储桶中存储的视频和封面地址。最后,将得到的结果进行 map,返回请求。
接下来,在 web
服务中注册路由实现:
func FeedAction(ctx context.Context, c *app.RequestContext) {
latestTime := c.Query("latest_time")
token := c.Query("token")
response, err := feedClient.ListVideos(ctx, &feed.ListFeedRequest{
LatestTime: &latestTime,
Token: &token,
})
if err != nil {
c.JSON(
consts.StatusOK,
struct {
StatusCode int `json:"status_code"`
StatusMessage string `json:"status_message"`
}{1, err.Error()},
)
return
}
c.JSON(
consts.StatusOK,
response,
)
}
引用
该文章部分内容来自于以下网页:
分发
This work is licensed under CC BY-SA 4.0