极简版抖音项目的实现 | 青训营笔记

2023-03-06 18:48:21 浏览数 (2)

极简版抖音项目的实现 | 青训营笔记

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

代码语言:javascript复制
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 生成规范的模板代码:

代码语言:javascript复制
#!/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

代码语言:javascript复制
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 调用实现如下:

代码语言:javascript复制
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 服务中注册路由实现:

代码语言:javascript复制
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,
    )
}

引用

该文章部分内容来自于以下网页:

  • Toktik-Team/toktik at feat-feed-service (github.com)

0 人点赞