├── mw ├── init.go ├── redis │ ├── monthly_active.go │ ├── token_maneger.go │ ├── video.go │ ├── init.go │ ├── comment.go │ ├── user_basic_info.go │ ├── rate_limiter.go │ └── count_limiter.go ├── localcache │ └── init.go ├── rocketMQ │ ├── favorite.go │ ├── init.go │ └── comment.go └── kafka │ ├── init.go │ ├── user_follower.go │ ├── favorite.go │ ├── comment.go │ ├── video.go │ └── message.go ├── dal ├── mysql │ ├── message.go │ ├── init.go │ ├── user.go │ ├── comment.go │ ├── video.go │ ├── user_follow.go │ └── favorite.go ├── model │ ├── common.go │ ├── relation.go │ ├── user_actions.go │ ├── favorite.go │ ├── message.go │ ├── comment.go │ ├── video.go │ └── user.go ├── init.go ├── mongo │ ├── init.go │ └── message.go └── graphdb │ ├── init.go │ └── user_follow.go ├── constant ├── biz │ ├── comment.go │ ├── message.go │ ├── relation.go │ ├── favorite.go │ ├── user.go │ └── video.go └── service.go ├── service ├── user │ ├── kitex_info.yaml │ ├── build.sh │ ├── script │ │ └── bootstrap.sh │ ├── pack │ │ └── response.go │ └── main.go ├── video │ ├── kitex_info.yaml │ ├── build.sh │ ├── script │ │ └── bootstrap.sh │ └── main.go ├── comment │ ├── kitex_info.yaml │ ├── build.sh │ ├── script │ │ └── bootstrap.sh │ ├── pack │ │ └── response.go │ └── main.go ├── favorite │ ├── kitex_info.yaml │ ├── build.sh │ ├── script │ │ └── bootstrap.sh │ ├── favorite_mq.go │ └── main.go ├── message │ ├── kitex_info.yaml │ ├── build.sh │ ├── script │ │ └── bootstrap.sh │ ├── pack │ │ └── message.go │ ├── main.go │ └── handler.go ├── relation │ ├── kitex_info.yaml │ ├── build.sh │ ├── script │ │ └── bootstrap.sh │ └── main.go └── api │ ├── .hz │ ├── script │ └── bootstrap.sh │ ├── build.sh │ ├── mw │ ├── rate_limiter_based_ip.go │ ├── count_limiter.go │ └── jwt.go │ ├── router.go │ ├── biz │ ├── favorite │ │ └── favorite.go │ ├── message │ │ └── message.go │ ├── user │ │ └── user.go │ └── comment │ │ └── comment.go │ └── main.go ├── kitex_gen ├── user │ ├── k-consts.go │ └── userservice │ │ ├── server.go │ │ ├── invoker.go │ │ ├── client.go │ │ └── userservice.go ├── video │ ├── k-consts.go │ └── videoservice │ │ ├── server.go │ │ ├── invoker.go │ │ └── client.go ├── comment │ ├── k-consts.go │ └── commentservice │ │ ├── server.go │ │ ├── invoker.go │ │ ├── client.go │ │ └── commentservice.go ├── favorite │ ├── k-consts.go │ └── favoriteservice │ │ ├── server.go │ │ ├── invoker.go │ │ └── client.go ├── message │ ├── k-consts.go │ └── messageservice │ │ ├── server.go │ │ ├── invoker.go │ │ ├── client.go │ │ └── messageservice.go └── relation │ ├── k-consts.go │ └── relationservice │ ├── server.go │ ├── invoker.go │ └── client.go ├── common ├── snowflake.go ├── request.go ├── hertzlog.go ├── sensitive.go ├── passwordtool.go ├── ffmpegtool.go ├── jwt.go ├── response.go └── upload.go ├── stop_all_services.sh ├── start_all_services.sh ├── .gitignore ├── idl ├── message.thrift ├── comment.thrift ├── favorite.thrift ├── video.thrift ├── user.thrift └── relation.thrift ├── start.sh ├── README.md ├── add_new_service.sh ├── logger └── init.go ├── config ├── app.yaml └── settings.go ├── test ├── benchmark_test.go └── tools_test.go └── structure /mw/init.go: -------------------------------------------------------------------------------- 1 | package mw 2 | -------------------------------------------------------------------------------- /dal/mysql/message.go: -------------------------------------------------------------------------------- 1 | package mysql 2 | -------------------------------------------------------------------------------- /constant/biz/comment.go: -------------------------------------------------------------------------------- 1 | package biz 2 | -------------------------------------------------------------------------------- /constant/biz/message.go: -------------------------------------------------------------------------------- 1 | package biz 2 | -------------------------------------------------------------------------------- /constant/biz/relation.go: -------------------------------------------------------------------------------- 1 | package biz 2 | -------------------------------------------------------------------------------- /service/user/kitex_info.yaml: -------------------------------------------------------------------------------- 1 | kitexinfo: 2 | ServiceName: 'user' 3 | ToolVersion: 'v0.6.2' 4 | -------------------------------------------------------------------------------- /service/video/kitex_info.yaml: -------------------------------------------------------------------------------- 1 | kitexinfo: 2 | ServiceName: 'video' 3 | ToolVersion: 'v0.6.2' 4 | -------------------------------------------------------------------------------- /service/comment/kitex_info.yaml: -------------------------------------------------------------------------------- 1 | kitexinfo: 2 | ServiceName: 'comment' 3 | ToolVersion: 'v0.6.2' 4 | -------------------------------------------------------------------------------- /service/favorite/kitex_info.yaml: -------------------------------------------------------------------------------- 1 | kitexinfo: 2 | ServiceName: 'favorite' 3 | ToolVersion: 'v0.6.2' 4 | -------------------------------------------------------------------------------- /service/message/kitex_info.yaml: -------------------------------------------------------------------------------- 1 | kitexinfo: 2 | ServiceName: 'message' 3 | ToolVersion: 'v0.6.2' 4 | -------------------------------------------------------------------------------- /service/relation/kitex_info.yaml: -------------------------------------------------------------------------------- 1 | kitexinfo: 2 | ServiceName: 'relation' 3 | ToolVersion: 'v0.6.2' 4 | -------------------------------------------------------------------------------- /service/api/.hz: -------------------------------------------------------------------------------- 1 | // Code generated by hz. DO NOT EDIT. 2 | 3 | hz version: v0.6.5 4 | handlerDir: "" 5 | modelDir: "" 6 | routerDir: "" 7 | -------------------------------------------------------------------------------- /constant/biz/favorite.go: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | const ( 4 | FavoriteActionSuccess int = iota 5 | FavoriteActionError 6 | FavoriteActionRepeat 7 | ) 8 | -------------------------------------------------------------------------------- /dal/model/common.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type Response struct { 4 | StatusCode int32 `json:"status_code"` 5 | StatusMsg string `json:"status_msg"` 6 | } 7 | -------------------------------------------------------------------------------- /kitex_gen/user/k-consts.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | // KitexUnusedProtection is used to prevent 'imported and not used' error. 4 | var KitexUnusedProtection = struct{}{} 5 | -------------------------------------------------------------------------------- /kitex_gen/video/k-consts.go: -------------------------------------------------------------------------------- 1 | package video 2 | 3 | // KitexUnusedProtection is used to prevent 'imported and not used' error. 4 | var KitexUnusedProtection = struct{}{} 5 | -------------------------------------------------------------------------------- /kitex_gen/comment/k-consts.go: -------------------------------------------------------------------------------- 1 | package comment 2 | 3 | // KitexUnusedProtection is used to prevent 'imported and not used' error. 4 | var KitexUnusedProtection = struct{}{} 5 | -------------------------------------------------------------------------------- /kitex_gen/favorite/k-consts.go: -------------------------------------------------------------------------------- 1 | package favorite 2 | 3 | // KitexUnusedProtection is used to prevent 'imported and not used' error. 4 | var KitexUnusedProtection = struct{}{} 5 | -------------------------------------------------------------------------------- /kitex_gen/message/k-consts.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | // KitexUnusedProtection is used to prevent 'imported and not used' error. 4 | var KitexUnusedProtection = struct{}{} 5 | -------------------------------------------------------------------------------- /kitex_gen/relation/k-consts.go: -------------------------------------------------------------------------------- 1 | package relation 2 | 3 | // KitexUnusedProtection is used to prevent 'imported and not used' error. 4 | var KitexUnusedProtection = struct{}{} 5 | -------------------------------------------------------------------------------- /service/api/script/bootstrap.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | CURDIR=$(cd $(dirname $0); pwd) 3 | BinaryName="api" 4 | echo "$CURDIR/bin/${BinaryName}" 5 | exec $CURDIR/bin/${BinaryName} 6 | -------------------------------------------------------------------------------- /service/api/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | RUN_NAME="api" 3 | mkdir -p output/bin 4 | cp script/* output 2>/dev/null 5 | chmod +x output/bootstrap.sh 6 | go build -o output/bin/${RUN_NAME} 7 | -------------------------------------------------------------------------------- /constant/biz/user.go: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | const ( 4 | DEFAULTBG = "https://tiktok-1319971229.cos.ap-nanjing.myqcloud.com/defaultbg.jpg" 5 | DEFAULTAVATOR= "https://tiktok-1319971229.cos.ap-nanjing.myqcloud.com/defaultavator.jpg" 6 | ) -------------------------------------------------------------------------------- /dal/model/relation.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type UserFollow struct { 4 | // 用户的关注信息 5 | ID uint `gorm:"primaryKey"` 6 | UserId uint `gorm:"index;not null"` // 用户id 7 | FollowId uint `gorm:"index;not null"` // 关注用户id 8 | } 9 | 10 | func (*UserFollow) TableName() string { 11 | return "user_follow" 12 | } 13 | -------------------------------------------------------------------------------- /service/video/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | RUN_NAME="video" 3 | 4 | mkdir -p output/bin 5 | cp script/* output/ 6 | chmod +x output/bootstrap.sh 7 | 8 | if [ "$IS_SYSTEM_TEST_ENV" != "1" ]; then 9 | go build -o output/bin/${RUN_NAME} 10 | else 11 | go test -c -covermode=set -o output/bin/${RUN_NAME} -coverpkg=./... 12 | fi 13 | -------------------------------------------------------------------------------- /service/favorite/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | RUN_NAME="favorite" 3 | 4 | mkdir -p output/bin 5 | cp script/* output/ 6 | chmod +x output/bootstrap.sh 7 | 8 | if [ "$IS_SYSTEM_TEST_ENV" != "1" ]; then 9 | go build -o output/bin/${RUN_NAME} 10 | else 11 | go test -c -covermode=set -o output/bin/${RUN_NAME} -coverpkg=./... 12 | fi 13 | -------------------------------------------------------------------------------- /service/message/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | RUN_NAME="message" 3 | 4 | mkdir -p output/bin 5 | cp script/* output/ 6 | chmod +x output/bootstrap.sh 7 | 8 | if [ "$IS_SYSTEM_TEST_ENV" != "1" ]; then 9 | go build -o output/bin/${RUN_NAME} 10 | else 11 | go test -c -covermode=set -o output/bin/${RUN_NAME} -coverpkg=./... 12 | fi 13 | -------------------------------------------------------------------------------- /service/relation/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | RUN_NAME="relation" 3 | 4 | mkdir -p output/bin 5 | cp script/* output/ 6 | chmod +x output/bootstrap.sh 7 | 8 | if [ "$IS_SYSTEM_TEST_ENV" != "1" ]; then 9 | go build -o output/bin/${RUN_NAME} 10 | else 11 | go test -c -covermode=set -o output/bin/${RUN_NAME} -coverpkg=./... 12 | fi 13 | -------------------------------------------------------------------------------- /service/user/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | RUN_NAME="user" 3 | 4 | mkdir -p output/bin 5 | cp script/* output/ 6 | chmod +x output/bootstrap.sh 7 | 8 | if [ "$IS_SYSTEM_TEST_ENV" != "1" ]; then 9 | go build -o output/bin/${RUN_NAME} 10 | else 11 | go test -c -covermode=set -o output/bin/${RUN_NAME} -coverpkg=./... 12 | fi 13 | 14 | -------------------------------------------------------------------------------- /service/comment/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | RUN_NAME="comment" 3 | 4 | mkdir -p output/bin 5 | cp script/* output/ 6 | chmod +x output/bootstrap.sh 7 | 8 | if [ "$IS_SYSTEM_TEST_ENV" != "1" ]; then 9 | go build -o output/bin/${RUN_NAME} 10 | else 11 | go test -c -covermode=set -o output/bin/${RUN_NAME} -coverpkg=./... 12 | fi 13 | 14 | -------------------------------------------------------------------------------- /dal/init.go: -------------------------------------------------------------------------------- 1 | package dal 2 | 3 | import ( 4 | "douyin/config" 5 | "douyin/dal/mongo" 6 | "douyin/dal/mysql" 7 | ) 8 | 9 | func Init(appConfig *config.AppConfig) error { 10 | err := mongo.Init(appConfig) 11 | if err != nil { 12 | return err 13 | } 14 | err = mysql.Init(appConfig) 15 | if err != nil { 16 | return err 17 | } 18 | return nil 19 | } 20 | -------------------------------------------------------------------------------- /common/snowflake.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "github.com/bwmarrin/snowflake" 5 | ) 6 | 7 | var ( 8 | Node *snowflake.Node 9 | ) 10 | 11 | // InitSnowflake 初始化生成器 12 | func InitSnowflake(nodeId int64) (err error) { 13 | Node, err = snowflake.NewNode(nodeId) 14 | return 15 | } 16 | 17 | func GetUid() (id uint) { 18 | return uint(Node.Generate().Int64()) 19 | } 20 | -------------------------------------------------------------------------------- /constant/biz/video.go: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | const ( 4 | FileLocalPath = "./tmp/" 5 | OSS = "https://tiktok-1319971229.cos.ap-nanjing.myqcloud.com/" 6 | CuOSS = "https://tiktok-1319971229.ci.ap-nanjing.myqcloud.com" 7 | SecretId = "AKIDFKMQPakpcN6tkV9oJg6PanzAGC0hGkCZ" 8 | SecretKey = "MWXXLzQlutgMtLl5HH9pPp5CB0cfcMxR" 9 | SessionToken = "SECRETTOKEN" 10 | 11 | FileMode = 0600 // 本用户可读写,不可执行 12 | ) 13 | -------------------------------------------------------------------------------- /stop_all_services.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 要关闭的端口列表 4 | port_list=(8080 4001 4002 4003 4004 4005 4006) 5 | 6 | for port in "${port_list[@]}"; do 7 | # 查找端口上的进程 8 | pids=$(lsof -t -i :$port) 9 | 10 | if [ -n "$pids" ]; then 11 | # 终止进程 12 | echo "Terminating processes on port $port (PIDs: $pids)" 13 | kill -9 $pids 14 | else 15 | echo "No process found on port $port" 16 | fi 17 | done 18 | -------------------------------------------------------------------------------- /dal/model/user_actions.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | // UserActions 记录用户行为的,后面用来进行视频的推荐,字段暂定 4 | //type UserActions struct { 5 | // gorm.Model 6 | // UserId int64 // 操作者ID 7 | // VideoId int64 // 视频ID 8 | // ActionType int // 0 点赞 1 评论 2 分享 9 | // WhetherFinish int // 0 没看完 1 看完 10 | // WatchingCount int // 观看数量 11 | // ActionTime int64 // 观看时间 12 | //} 13 | // 14 | //func (*UserActions) TableName() string { 15 | // return "user_action" 16 | //} 17 | -------------------------------------------------------------------------------- /common/request.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "errors" 5 | "github.com/cloudwego/hertz/pkg/app" 6 | ) 7 | 8 | const ContextUserIDKey = "user_id" 9 | const TokenValid = "tokenValid" 10 | 11 | var ErrorUserNotLogin = errors.New("用户未登录") 12 | 13 | func GetCurrentUserID(rc *app.RequestContext) (userID uint, err error) { 14 | uid, ok := rc.Get(ContextUserIDKey) 15 | if !ok { 16 | err = ErrorUserNotLogin 17 | return 18 | } 19 | userID, ok = uid.(uint) 20 | return 21 | } 22 | -------------------------------------------------------------------------------- /dal/model/favorite.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type Favorite struct { 4 | ID uint `gorm:"primaryKey"` 5 | UserId uint `gorm:"index;not null"` 6 | VideoId uint `gorm:"index;not null"` 7 | } 8 | 9 | type FavoriteAction struct { 10 | UserId uint 11 | VideoId uint 12 | ActionType int 13 | } 14 | 15 | func (*Favorite) TableName() string { 16 | return "favorite" 17 | } 18 | 19 | type FavoriteListResponse struct { 20 | FavoriteRes Response 21 | VideoResponse []VideoResponse `json:"video_list"` 22 | } 23 | -------------------------------------------------------------------------------- /service/video/script/bootstrap.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | CURDIR=$(cd $(dirname $0); pwd) 3 | 4 | if [ "X$1" != "X" ]; then 5 | RUNTIME_ROOT=$1 6 | else 7 | RUNTIME_ROOT=${CURDIR} 8 | fi 9 | 10 | export KITEX_RUNTIME_ROOT=$RUNTIME_ROOT 11 | export KITEX_LOG_DIR="$RUNTIME_ROOT/log" 12 | 13 | if [ ! -d "$KITEX_LOG_DIR/app" ]; then 14 | mkdir -p "$KITEX_LOG_DIR/app" 15 | fi 16 | 17 | if [ ! -d "$KITEX_LOG_DIR/rpc" ]; then 18 | mkdir -p "$KITEX_LOG_DIR/rpc" 19 | fi 20 | 21 | exec "$CURDIR/bin/video" 22 | -------------------------------------------------------------------------------- /service/favorite/script/bootstrap.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | CURDIR=$(cd $(dirname $0); pwd) 3 | 4 | if [ "X$1" != "X" ]; then 5 | RUNTIME_ROOT=$1 6 | else 7 | RUNTIME_ROOT=${CURDIR} 8 | fi 9 | 10 | export KITEX_RUNTIME_ROOT=$RUNTIME_ROOT 11 | export KITEX_LOG_DIR="$RUNTIME_ROOT/log" 12 | 13 | if [ ! -d "$KITEX_LOG_DIR/app" ]; then 14 | mkdir -p "$KITEX_LOG_DIR/app" 15 | fi 16 | 17 | if [ ! -d "$KITEX_LOG_DIR/rpc" ]; then 18 | mkdir -p "$KITEX_LOG_DIR/rpc" 19 | fi 20 | 21 | exec "$CURDIR/bin/favorite" 22 | -------------------------------------------------------------------------------- /service/message/script/bootstrap.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | CURDIR=$(cd $(dirname $0); pwd) 3 | 4 | if [ "X$1" != "X" ]; then 5 | RUNTIME_ROOT=$1 6 | else 7 | RUNTIME_ROOT=${CURDIR} 8 | fi 9 | 10 | export KITEX_RUNTIME_ROOT=$RUNTIME_ROOT 11 | export KITEX_LOG_DIR="$RUNTIME_ROOT/log" 12 | 13 | if [ ! -d "$KITEX_LOG_DIR/app" ]; then 14 | mkdir -p "$KITEX_LOG_DIR/app" 15 | fi 16 | 17 | if [ ! -d "$KITEX_LOG_DIR/rpc" ]; then 18 | mkdir -p "$KITEX_LOG_DIR/rpc" 19 | fi 20 | 21 | exec "$CURDIR/bin/message" 22 | -------------------------------------------------------------------------------- /service/relation/script/bootstrap.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | CURDIR=$(cd $(dirname $0); pwd) 3 | 4 | if [ "X$1" != "X" ]; then 5 | RUNTIME_ROOT=$1 6 | else 7 | RUNTIME_ROOT=${CURDIR} 8 | fi 9 | 10 | export KITEX_RUNTIME_ROOT=$RUNTIME_ROOT 11 | export KITEX_LOG_DIR="$RUNTIME_ROOT/log" 12 | 13 | if [ ! -d "$KITEX_LOG_DIR/app" ]; then 14 | mkdir -p "$KITEX_LOG_DIR/app" 15 | fi 16 | 17 | if [ ! -d "$KITEX_LOG_DIR/rpc" ]; then 18 | mkdir -p "$KITEX_LOG_DIR/rpc" 19 | fi 20 | 21 | exec "$CURDIR/bin/relation" 22 | -------------------------------------------------------------------------------- /service/user/script/bootstrap.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | CURDIR=$(cd $(dirname $0); pwd) 3 | 4 | if [ "X$1" != "X" ]; then 5 | RUNTIME_ROOT=$1 6 | else 7 | RUNTIME_ROOT=${CURDIR} 8 | fi 9 | 10 | export KITEX_RUNTIME_ROOT=$RUNTIME_ROOT 11 | export KITEX_LOG_DIR="$RUNTIME_ROOT/log" 12 | 13 | if [ ! -d "$KITEX_LOG_DIR/app" ]; then 14 | mkdir -p "$KITEX_LOG_DIR/app" 15 | fi 16 | 17 | if [ ! -d "$KITEX_LOG_DIR/rpc" ]; then 18 | mkdir -p "$KITEX_LOG_DIR/rpc" 19 | fi 20 | 21 | exec "$CURDIR/bin/user" 22 | 23 | -------------------------------------------------------------------------------- /service/comment/script/bootstrap.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | CURDIR=$(cd $(dirname $0); pwd) 3 | 4 | if [ "X$1" != "X" ]; then 5 | RUNTIME_ROOT=$1 6 | else 7 | RUNTIME_ROOT=${CURDIR} 8 | fi 9 | 10 | export KITEX_RUNTIME_ROOT=$RUNTIME_ROOT 11 | export KITEX_LOG_DIR="$RUNTIME_ROOT/log" 12 | 13 | if [ ! -d "$KITEX_LOG_DIR/app" ]; then 14 | mkdir -p "$KITEX_LOG_DIR/app" 15 | fi 16 | 17 | if [ ! -d "$KITEX_LOG_DIR/rpc" ]; then 18 | mkdir -p "$KITEX_LOG_DIR/rpc" 19 | fi 20 | 21 | exec "$CURDIR/bin/comment" 22 | 23 | -------------------------------------------------------------------------------- /common/hertzlog.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "context" 5 | "github.com/cloudwego/hertz/pkg/app" 6 | "github.com/cloudwego/hertz/pkg/common/hlog" 7 | ) 8 | 9 | func AccessLog() app.HandlerFunc { 10 | return func(c context.Context, ctx *app.RequestContext) { 11 | hlog.CtxTracef(c, "status=%d method=%s full_path=%s QueryString=%s post=%s", 12 | ctx.Response.StatusCode(), 13 | ctx.Request.Header.Method(), ctx.Request.URI().PathOriginal(), ctx.Request.QueryString(), 14 | ctx.Request.PostArgString()) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /service/user/pack/response.go: -------------------------------------------------------------------------------- 1 | package pack 2 | 3 | import ( 4 | "douyin/constant/biz" 5 | user "douyin/kitex_gen/user" 6 | ) 7 | 8 | func User(userId int64) *user.User { 9 | return &user.User{ 10 | Id: userId, 11 | Name: "", 12 | FollowCount: 0, 13 | FollowerCount: 0, 14 | IsFollow: false, 15 | Avatar: biz.DEFAULTAVATOR, 16 | BackgroundImage: biz.DEFAULTBG, 17 | Signature: "", 18 | TotalFavorited: "0", 19 | WorkCount: 0, 20 | FavoriteCount: 0, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /common/sensitive.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "github.com/importcjj/sensitive" 5 | "go.uber.org/zap" 6 | ) 7 | 8 | var sensitiveFilter *sensitive.Filter 9 | 10 | func InitSensitiveFilter() (err error) { 11 | sensitiveFilter = sensitive.New() 12 | err = sensitiveFilter.LoadWordDict("./common/sensitive_word_dic.txt") 13 | if err != nil { 14 | zap.L().Error("Load sensitive dic error", zap.Error(err)) 15 | return err 16 | } 17 | return 18 | } 19 | 20 | func ReplaceWord(word string) string { 21 | //print(sensitiveFilter.Replace(word, '*')) 22 | return sensitiveFilter.Replace(word, '*') 23 | } 24 | -------------------------------------------------------------------------------- /kitex_gen/user/userservice/server.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.6.2. DO NOT EDIT. 2 | package userservice 3 | 4 | import ( 5 | user "douyin/kitex_gen/user" 6 | server "github.com/cloudwego/kitex/server" 7 | ) 8 | 9 | // NewServer creates a server.Server with the given handler and options. 10 | func NewServer(handler user.UserService, opts ...server.Option) server.Server { 11 | var options []server.Option 12 | 13 | options = append(options, opts...) 14 | 15 | svr := server.NewServer(options...) 16 | if err := svr.RegisterService(serviceInfo(), handler); err != nil { 17 | panic(err) 18 | } 19 | return svr 20 | } 21 | -------------------------------------------------------------------------------- /kitex_gen/video/videoservice/server.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.6.2. DO NOT EDIT. 2 | package videoservice 3 | 4 | import ( 5 | video "douyin/kitex_gen/video" 6 | server "github.com/cloudwego/kitex/server" 7 | ) 8 | 9 | // NewServer creates a server.Server with the given handler and options. 10 | func NewServer(handler video.VideoService, opts ...server.Option) server.Server { 11 | var options []server.Option 12 | 13 | options = append(options, opts...) 14 | 15 | svr := server.NewServer(options...) 16 | if err := svr.RegisterService(serviceInfo(), handler); err != nil { 17 | panic(err) 18 | } 19 | return svr 20 | } 21 | -------------------------------------------------------------------------------- /common/passwordtool.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "github.com/alexedwards/argon2id" 5 | "go.uber.org/zap" 6 | ) 7 | 8 | func MakePassword(pwd string) (string, error) { 9 | hash, err := argon2id.CreateHash(pwd, argon2id.DefaultParams) 10 | if err != nil { 11 | zap.L().Error("generate passwordHash fail!", zap.Error(err)) 12 | return "", err 13 | } 14 | 15 | return hash, err 16 | } 17 | 18 | func CheckPassword(pwd, hash string) bool { 19 | match, err := argon2id.ComparePasswordAndHash(pwd, hash) 20 | if err != nil { 21 | zap.L().Error("CheckPassword fail!", zap.Error(err)) 22 | return false 23 | } 24 | return match 25 | } 26 | -------------------------------------------------------------------------------- /kitex_gen/comment/commentservice/server.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.6.2. DO NOT EDIT. 2 | package commentservice 3 | 4 | import ( 5 | comment "douyin/kitex_gen/comment" 6 | server "github.com/cloudwego/kitex/server" 7 | ) 8 | 9 | // NewServer creates a server.Server with the given handler and options. 10 | func NewServer(handler comment.CommentService, opts ...server.Option) server.Server { 11 | var options []server.Option 12 | 13 | options = append(options, opts...) 14 | 15 | svr := server.NewServer(options...) 16 | if err := svr.RegisterService(serviceInfo(), handler); err != nil { 17 | panic(err) 18 | } 19 | return svr 20 | } 21 | -------------------------------------------------------------------------------- /kitex_gen/message/messageservice/server.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.6.2. DO NOT EDIT. 2 | package messageservice 3 | 4 | import ( 5 | message "douyin/kitex_gen/message" 6 | server "github.com/cloudwego/kitex/server" 7 | ) 8 | 9 | // NewServer creates a server.Server with the given handler and options. 10 | func NewServer(handler message.MessageService, opts ...server.Option) server.Server { 11 | var options []server.Option 12 | 13 | options = append(options, opts...) 14 | 15 | svr := server.NewServer(options...) 16 | if err := svr.RegisterService(serviceInfo(), handler); err != nil { 17 | panic(err) 18 | } 19 | return svr 20 | } 21 | -------------------------------------------------------------------------------- /kitex_gen/favorite/favoriteservice/server.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.6.2. DO NOT EDIT. 2 | package favoriteservice 3 | 4 | import ( 5 | favorite "douyin/kitex_gen/favorite" 6 | server "github.com/cloudwego/kitex/server" 7 | ) 8 | 9 | // NewServer creates a server.Server with the given handler and options. 10 | func NewServer(handler favorite.FavoriteService, opts ...server.Option) server.Server { 11 | var options []server.Option 12 | 13 | options = append(options, opts...) 14 | 15 | svr := server.NewServer(options...) 16 | if err := svr.RegisterService(serviceInfo(), handler); err != nil { 17 | panic(err) 18 | } 19 | return svr 20 | } 21 | -------------------------------------------------------------------------------- /kitex_gen/relation/relationservice/server.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.6.2. DO NOT EDIT. 2 | package relationservice 3 | 4 | import ( 5 | relation "douyin/kitex_gen/relation" 6 | server "github.com/cloudwego/kitex/server" 7 | ) 8 | 9 | // NewServer creates a server.Server with the given handler and options. 10 | func NewServer(handler relation.RelationService, opts ...server.Option) server.Server { 11 | var options []server.Option 12 | 13 | options = append(options, opts...) 14 | 15 | svr := server.NewServer(options...) 16 | if err := svr.RegisterService(serviceInfo(), handler); err != nil { 17 | panic(err) 18 | } 19 | return svr 20 | } 21 | -------------------------------------------------------------------------------- /constant/service.go: -------------------------------------------------------------------------------- 1 | package constant 2 | 3 | const EtcdAddr = "127.0.0.1:2379" 4 | const ApiServiceName = "api-service" 5 | const ApiServicePort = ":8080" 6 | const CommentServiceName = "comment-service" 7 | const CommentServicePort = ":4001" 8 | const UserServiceName = "user-service" 9 | const UserServicePort = ":4002" 10 | const VideoServiceName = "video-service" 11 | const VideoServicePort = ":4003" 12 | const RelationServiceName = "relation-service" 13 | const RelationServicePort = ":4004" 14 | const MessageServiceName = "message-service" 15 | const MessageServicePort = ":4005" 16 | const FavoriteServiceName = "favorite-service" 17 | const FavoriteServicePort = ":4006" 18 | -------------------------------------------------------------------------------- /kitex_gen/user/userservice/invoker.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.6.2. DO NOT EDIT. 2 | 3 | package userservice 4 | 5 | import ( 6 | user "douyin/kitex_gen/user" 7 | server "github.com/cloudwego/kitex/server" 8 | ) 9 | 10 | // NewInvoker creates a server.Invoker with the given handler and options. 11 | func NewInvoker(handler user.UserService, opts ...server.Option) server.Invoker { 12 | var options []server.Option 13 | 14 | options = append(options, opts...) 15 | 16 | s := server.NewInvoker(options...) 17 | if err := s.RegisterService(serviceInfo(), handler); err != nil { 18 | panic(err) 19 | } 20 | if err := s.Init(); err != nil { 21 | panic(err) 22 | } 23 | return s 24 | } 25 | -------------------------------------------------------------------------------- /service/comment/pack/response.go: -------------------------------------------------------------------------------- 1 | package pack 2 | 3 | import ( 4 | "douyin/dal/model" 5 | "douyin/kitex_gen/comment" 6 | user "douyin/kitex_gen/user" 7 | "time" 8 | ) 9 | 10 | func Comment(commentModel *model.Comment, userModel *user.User) *comment.Comment { 11 | if commentModel == nil { 12 | return nil 13 | } 14 | return &comment.Comment{ 15 | Id: int64(commentModel.ID), 16 | User: userModel, 17 | Content: commentModel.Content, 18 | CreateDate: TranslateTime(commentModel.CreatedAt.Unix()), 19 | } 20 | } 21 | 22 | // TranslateTime 返回mm-dd格式 23 | func TranslateTime(createTime int64) string { 24 | t := time.Unix(createTime, 0) 25 | return t.Format("01-02") 26 | } 27 | -------------------------------------------------------------------------------- /kitex_gen/video/videoservice/invoker.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.6.2. DO NOT EDIT. 2 | 3 | package videoservice 4 | 5 | import ( 6 | video "douyin/kitex_gen/video" 7 | server "github.com/cloudwego/kitex/server" 8 | ) 9 | 10 | // NewInvoker creates a server.Invoker with the given handler and options. 11 | func NewInvoker(handler video.VideoService, opts ...server.Option) server.Invoker { 12 | var options []server.Option 13 | 14 | options = append(options, opts...) 15 | 16 | s := server.NewInvoker(options...) 17 | if err := s.RegisterService(serviceInfo(), handler); err != nil { 18 | panic(err) 19 | } 20 | if err := s.Init(); err != nil { 21 | panic(err) 22 | } 23 | return s 24 | } 25 | -------------------------------------------------------------------------------- /dal/model/message.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type Message struct { 4 | ID int64 `bson:"id"` 5 | FromUserId uint `json:"from_user_id"` 6 | ToUserId uint `json:"to_user_id"` 7 | Content string `json:"content,omitempty"` 8 | CreateTime int64 `json:"create_time"` 9 | } 10 | 11 | type MessageChatResponse struct { 12 | Response 13 | MessageList []Message `json:"message_list"` 14 | } 15 | 16 | type MessageSendEvent struct { 17 | UserId int64 `json:"user_id"` 18 | ToUserId int64 `json:"to_user_id"` 19 | MsgContent string `json:"msg_content"` 20 | } 21 | 22 | type MessagePushEvent struct { 23 | FromUserId int64 `json:"user_id"` 24 | MsgContent string `json:"msg_content"` 25 | } 26 | -------------------------------------------------------------------------------- /mw/redis/monthly_active.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | "time" 7 | ) 8 | 9 | func SetMonthlyActiveBit(userId uint) { 10 | month := time.Now().Month().String() 11 | baseSlice := []string{MonthlyActive, month, strconv.Itoa(int(userId))} 12 | key := strings.Join(baseSlice, Delimiter) 13 | day := time.Now().Day() 14 | Rdb.SetBit(Ctx, key, int64(day), 1) 15 | return 16 | } 17 | 18 | func CountMonthlyActiveBit(month time.Month, userId uint) int64 { 19 | monthStr := month.String() 20 | baseSlice := []string{MonthlyActive, monthStr, strconv.Itoa(int(userId))} 21 | key := strings.Join(baseSlice, Delimiter) 22 | val := Rdb.BitCount(Ctx, key, nil).Val() 23 | return val 24 | } 25 | -------------------------------------------------------------------------------- /kitex_gen/comment/commentservice/invoker.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.6.2. DO NOT EDIT. 2 | 3 | package commentservice 4 | 5 | import ( 6 | comment "douyin/kitex_gen/comment" 7 | server "github.com/cloudwego/kitex/server" 8 | ) 9 | 10 | // NewInvoker creates a server.Invoker with the given handler and options. 11 | func NewInvoker(handler comment.CommentService, opts ...server.Option) server.Invoker { 12 | var options []server.Option 13 | 14 | options = append(options, opts...) 15 | 16 | s := server.NewInvoker(options...) 17 | if err := s.RegisterService(serviceInfo(), handler); err != nil { 18 | panic(err) 19 | } 20 | if err := s.Init(); err != nil { 21 | panic(err) 22 | } 23 | return s 24 | } 25 | -------------------------------------------------------------------------------- /kitex_gen/message/messageservice/invoker.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.6.2. DO NOT EDIT. 2 | 3 | package messageservice 4 | 5 | import ( 6 | message "douyin/kitex_gen/message" 7 | server "github.com/cloudwego/kitex/server" 8 | ) 9 | 10 | // NewInvoker creates a server.Invoker with the given handler and options. 11 | func NewInvoker(handler message.MessageService, opts ...server.Option) server.Invoker { 12 | var options []server.Option 13 | 14 | options = append(options, opts...) 15 | 16 | s := server.NewInvoker(options...) 17 | if err := s.RegisterService(serviceInfo(), handler); err != nil { 18 | panic(err) 19 | } 20 | if err := s.Init(); err != nil { 21 | panic(err) 22 | } 23 | return s 24 | } 25 | -------------------------------------------------------------------------------- /kitex_gen/favorite/favoriteservice/invoker.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.6.2. DO NOT EDIT. 2 | 3 | package favoriteservice 4 | 5 | import ( 6 | favorite "douyin/kitex_gen/favorite" 7 | server "github.com/cloudwego/kitex/server" 8 | ) 9 | 10 | // NewInvoker creates a server.Invoker with the given handler and options. 11 | func NewInvoker(handler favorite.FavoriteService, opts ...server.Option) server.Invoker { 12 | var options []server.Option 13 | 14 | options = append(options, opts...) 15 | 16 | s := server.NewInvoker(options...) 17 | if err := s.RegisterService(serviceInfo(), handler); err != nil { 18 | panic(err) 19 | } 20 | if err := s.Init(); err != nil { 21 | panic(err) 22 | } 23 | return s 24 | } 25 | -------------------------------------------------------------------------------- /kitex_gen/relation/relationservice/invoker.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.6.2. DO NOT EDIT. 2 | 3 | package relationservice 4 | 5 | import ( 6 | relation "douyin/kitex_gen/relation" 7 | server "github.com/cloudwego/kitex/server" 8 | ) 9 | 10 | // NewInvoker creates a server.Invoker with the given handler and options. 11 | func NewInvoker(handler relation.RelationService, opts ...server.Option) server.Invoker { 12 | var options []server.Option 13 | 14 | options = append(options, opts...) 15 | 16 | s := server.NewInvoker(options...) 17 | if err := s.RegisterService(serviceInfo(), handler); err != nil { 18 | panic(err) 19 | } 20 | if err := s.Init(); err != nil { 21 | panic(err) 22 | } 23 | return s 24 | } 25 | -------------------------------------------------------------------------------- /service/api/mw/rate_limiter_based_ip.go: -------------------------------------------------------------------------------- 1 | package mw 2 | 3 | import ( 4 | "context" 5 | "douyin/common" 6 | "douyin/mw/redis" 7 | "github.com/cloudwego/hertz/pkg/app" 8 | "net/http" 9 | "strings" 10 | ) 11 | 12 | // RateLimiter 限流器 13 | func RateLimiter() app.HandlerFunc { 14 | return func(ctx context.Context, c *app.RequestContext) { 15 | ipAddr := strings.Split(c.ClientIP(), ":")[0] 16 | permit, remain, err := redis.AcquireBucket(ipAddr) 17 | if err != nil || !permit { 18 | c.JSON(http.StatusBadRequest, Response{ 19 | StatusCode: common.CodeLimiterCount, 20 | StatusMsg: common.MapErrMsg(common.CodeLimiterCount), 21 | }) 22 | c.Abort() 23 | return 24 | } 25 | c.Set("reqCount", remain) 26 | c.Next(ctx) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /service/message/pack/message.go: -------------------------------------------------------------------------------- 1 | package pack 2 | 3 | import ( 4 | "douyin/dal/model" 5 | "douyin/kitex_gen/message" 6 | ) 7 | 8 | func Messages(messageModels []*model.Message) []*message.Message { 9 | if messageModels == nil { 10 | return nil 11 | } 12 | messages := make([]*message.Message, 0, len(messageModels)) 13 | for _, msg := range messageModels { 14 | messages = append(messages, Message(msg)) 15 | } 16 | return messages 17 | } 18 | 19 | func Message(messageModel *model.Message) *message.Message { 20 | if messageModel == nil { 21 | return nil 22 | } 23 | return &message.Message{ 24 | Id: messageModel.ID, 25 | ToUserId: int64(messageModel.ToUserId), 26 | FromUserId: int64(messageModel.FromUserId), 27 | Content: messageModel.Content, 28 | CreateTime: messageModel.CreateTime, 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /start_all_services.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 启动所有服务 4 | echo "Starting all services..." 5 | 6 | # 启动 api 服务 7 | echo "Starting API service..." 8 | bash start.sh --service api & 9 | 10 | # 启动 user 服务 11 | echo "Starting User service..." 12 | bash start.sh --service user & 13 | 14 | # 启动 comment 服务 15 | echo "Starting Comment service..." 16 | bash start.sh --service comment & 17 | 18 | # 启动 relation 服务 19 | echo "Starting Relation service..." 20 | bash start.sh --service relation & 21 | 22 | # 启动 message 服务 23 | echo "Starting Message service..." 24 | bash start.sh --service message & 25 | 26 | # 启动 favorite 服务 27 | echo "Starting Favorite service..." 28 | bash start.sh --service favorite & 29 | 30 | # 启动 favorite 服务 31 | echo "Starting Video service..." 32 | bash start.sh --service video & 33 | 34 | 35 | echo "All services started." 36 | -------------------------------------------------------------------------------- /mw/redis/token_maneger.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "go.uber.org/zap" 5 | "strconv" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | const expireTime = 7 * 24 * time.Hour // 7天 11 | 12 | // SetToken 设置token 13 | func SetToken(userId uint, token string) { 14 | // userId作为key 15 | baseSlice := []string{TokenKey, strconv.Itoa(int(userId))} 16 | key := strings.Join(baseSlice, Delimiter) 17 | err := Rdb.Set(Ctx, key, token, expireTime).Err() 18 | if err != nil { 19 | zap.L().Error("SetToken failed", zap.Error(err)) 20 | } 21 | } 22 | 23 | // TokenIsExisted 判断用户对应的token是否存在 24 | func TokenIsExisted(userId uint) bool { 25 | baseSlice := []string{TokenKey, strconv.Itoa(int(userId))} 26 | key := strings.Join(baseSlice, Delimiter) 27 | // 判断key是否存在 28 | exists, err := Rdb.Exists(Ctx, key).Result() 29 | if err != nil { 30 | return false 31 | } 32 | return err == nil && exists == 1 33 | } 34 | -------------------------------------------------------------------------------- /dal/model/comment.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "gorm.io/gorm" 5 | "time" 6 | ) 7 | 8 | // Comment 数据库Model 9 | type Comment struct { 10 | ID uint `gorm:"primaryKey"` 11 | VideoId uint `gorm:"index"` // 非唯一索引 12 | UserId uint 13 | Content string 14 | CreatedAt time.Time 15 | DeletedAt gorm.DeletedAt 16 | } 17 | 18 | func (*Comment) TableName() string { 19 | return "comments" 20 | } 21 | 22 | // CommentResponse 返回数据的Model 23 | type CommentResponse struct { 24 | Id int64 `json:"id"` 25 | User UserResponse `json:"user"` 26 | Content string `json:"content"` 27 | CreateDate string `json:"create_date"` 28 | } 29 | 30 | type CommentListResponse struct { 31 | Response 32 | CommentList []CommentResponse `json:"comment_list"` 33 | } 34 | 35 | type CommentActionResponse struct { 36 | Response 37 | Comment CommentResponse `json:"comment"` 38 | } 39 | -------------------------------------------------------------------------------- /mw/redis/video.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "douyin/dal/model" 5 | red "github.com/redis/go-redis/v9" 6 | "github.com/vmihailenco/msgpack" 7 | ) 8 | 9 | func AddVideos(videos []model.Video) { 10 | for _, video := range videos { 11 | marshal, _ := msgpack.Marshal(&video) 12 | Rdb.ZAdd(Ctx, VideoList, red.Z{ 13 | Score: float64(video.CreatedAt), Member: marshal, 14 | }) 15 | } 16 | } 17 | 18 | func AddVideo(video *model.Video) { 19 | marshal, _ := msgpack.Marshal(video) 20 | Rdb.ZAdd(Ctx, VideoList, red.Z{ 21 | Score: float64(video.CreatedAt), Member: marshal, 22 | }) 23 | } 24 | 25 | func GetVideos(time string) []string { 26 | videos, _ := Rdb.ZRangeArgs(Ctx, red.ZRangeArgs{ 27 | Key: VideoList, 28 | ByScore: true, 29 | Rev: true, 30 | Start: 0, 31 | Stop: "(" + time, //(0,time) 32 | Offset: 0, 33 | Count: 29, 34 | }).Result() 35 | return videos 36 | } 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | 23 | # IDE 24 | /.idea/ 25 | .idea/ 26 | /go_build_* 27 | out/ 28 | .vscode/ 29 | .vscode/settings.json 30 | *.sublime* 31 | __debug_bin 32 | .project 33 | tmp/ 34 | .tmp/ 35 | 36 | # Ignore .DS_Store files 37 | .DS_Store 38 | 39 | # Service build output 40 | **/output/ 41 | 42 | # Project ignore 43 | service/**/output/ 44 | .env 45 | /tls/ 46 | 47 | config 48 | -------------------------------------------------------------------------------- /mw/localcache/init.go: -------------------------------------------------------------------------------- 1 | package localcache 2 | 3 | import ( 4 | "context" 5 | "github.com/allegro/bigcache/v3" 6 | "log" 7 | "time" 8 | ) 9 | 10 | const ( 11 | WorkCount int32 = iota 12 | User 13 | FavoriteVideo 14 | ) 15 | 16 | func Init(rpcName int32) *bigcache.BigCache { 17 | 18 | var lifeWindow time.Duration 19 | switch rpcName { 20 | case WorkCount: 21 | lifeWindow = 10 * time.Second 22 | case User: 23 | lifeWindow = 30 * time.Minute 24 | case FavoriteVideo: 25 | lifeWindow = 30 * time.Minute 26 | default: 27 | lifeWindow = 10 * time.Second 28 | } 29 | 30 | config := bigcache.Config{ 31 | // 缓存条目数量 32 | Shards: 1024, 33 | 34 | // 单个缓存条目过期时间 35 | LifeWindow: lifeWindow, 36 | 37 | // 最大缓存值大小 38 | MaxEntrySize: 50, 39 | 40 | // 最大缓存总值大小 41 | MaxEntriesInWindow: 1000 * 10 * 60, 42 | } 43 | cache, initErr := bigcache.New(context.Background(), config) 44 | if initErr != nil { 45 | log.Fatal(initErr) 46 | } 47 | return cache 48 | } 49 | -------------------------------------------------------------------------------- /mw/redis/init.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | "douyin/config" 6 | "fmt" 7 | "github.com/redis/go-redis/v9" 8 | "time" 9 | ) 10 | 11 | var Ctx = context.Background() 12 | 13 | // Rdb Comment模块Rdb 14 | var Rdb *redis.Client 15 | 16 | // RdbExpireTime key的过期时间 17 | var RdbExpireTime time.Duration 18 | 19 | func Init(appConfig *config.AppConfig) (err error) { 20 | var conf *config.RedisConfig 21 | if appConfig.Mode == config.LocalMode { 22 | conf = appConfig.Local.RedisConfig 23 | } else { 24 | conf = appConfig.Remote.RedisConfig 25 | } 26 | // 获取conf中的过期时间, 单位为s 27 | RdbExpireTime = time.Duration(conf.ExpireTime) * time.Second 28 | 29 | Rdb = redis.NewClient(&redis.Options{ 30 | Addr: fmt.Sprintf("%s:%d", conf.Address, conf.Port), 31 | Password: conf.Password, // 密码 32 | DB: conf.CommentDB, // 数据库 33 | PoolSize: conf.PoolSize, // 连接池大小 34 | MinIdleConns: conf.MinIdleConns, 35 | }) 36 | if err = Rdb.Ping(Ctx).Err(); err != nil { 37 | return err 38 | } 39 | return 40 | } 41 | -------------------------------------------------------------------------------- /service/favorite/favorite_mq.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "douyin/dal/model" 6 | "encoding/json" 7 | "github.com/apache/rocketmq-client-go/v2/consumer" 8 | "github.com/apache/rocketmq-client-go/v2/primitive" 9 | "go.uber.org/zap" 10 | ) 11 | 12 | 13 | // Consume 消费点赞信息 14 | func Consume(ctx context.Context, msgs ...*primitive.MessageExt) (consumer.ConsumeResult, error) { 15 | for i := range msgs { 16 | result := msgs[i].Body 17 | 18 | message := new(model.FavoriteAction) 19 | if err := json.Unmarshal(result, message); err == nil { 20 | flushMutex.RLock() 21 | mutex.Lock() 22 | if favoriteData[message.UserId] == nil { 23 | favoriteData[message.UserId] = make(map[uint]int) 24 | } 25 | switch message.ActionType { 26 | case 1, 2: 27 | favoriteData[message.UserId][message.VideoId] = message.ActionType 28 | } 29 | mutex.Unlock() 30 | flushMutex.RUnlock() 31 | continue 32 | } 33 | zap.L().Error("[FavoriteMQ]解析消息失败:", zap.Binary("favorite", result)) 34 | } 35 | return consumer.ConsumeSuccess, nil 36 | } 37 | -------------------------------------------------------------------------------- /idl/message.thrift: -------------------------------------------------------------------------------- 1 | namespace go message 2 | 3 | struct Message { 4 | 1: i64 id, // 消息id 5 | 2: i64 to_user_id, // 该消息接收者的id 6 | 3: i64 from_user_id, // 该消息发送者的id 7 | 4: string content, // 消息内容 8 | 5: i64 create_time, // 消息创建时间 9 | } 10 | 11 | struct MessageChatRequest { 12 | 1: i64 from_user_id, // 发送方id 13 | 2: i64 to_user_id, // 对方用户id 14 | 3: i64 pre_msg_time, // 上次最新消息的时间(新增字段-apk更新中) 15 | } 16 | 17 | struct MessageChatResponse { 18 | 1: i32 status_code, // 状态码,0-成功,其他值-失败 19 | 2: string status_msg, // 返回状态描述 20 | 3: list message_list, // 消息列表 21 | } 22 | 23 | struct MessageActionRequest { 24 | 1: i64 from_user_id, // 用户id 25 | 2: i64 to_user_id, // 对方用户id 26 | 3: i32 action_type, // 1-发送消息 27 | 4: string content, // 消息内容 28 | } 29 | 30 | struct MessageActionResponse { 31 | 1: i32 status_code, // 状态码,0-成功,其他值-失败 32 | 2: string status_msg, // 返回状态描述 33 | } 34 | 35 | service MessageService { 36 | MessageChatResponse MessageChat(1: MessageChatRequest Request), 37 | MessageActionResponse MessageAction(1: MessageActionRequest Request), 38 | } 39 | -------------------------------------------------------------------------------- /dal/mongo/init.go: -------------------------------------------------------------------------------- 1 | package mongo 2 | 3 | import ( 4 | "context" 5 | "douyin/config" 6 | "fmt" 7 | "go.mongodb.org/mongo-driver/mongo" 8 | "go.mongodb.org/mongo-driver/mongo/options" 9 | "go.uber.org/zap" 10 | "time" 11 | ) 12 | 13 | var Mongo *mongo.Database 14 | var Ctx context.Context 15 | 16 | func Init(appConfig *config.AppConfig) (err error) { 17 | var conf *config.MongoConfig 18 | if appConfig.Mode == config.LocalMode { 19 | conf = appConfig.Local.MongoConfig 20 | } else { 21 | conf = appConfig.Remote.MongoConfig 22 | } 23 | Ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 24 | defer cancel() 25 | mongoUrl := fmt.Sprintf("mongodb://%s:%s@%s:%d/%s", conf.Username, conf.Password, conf.Address, conf.Port, conf.DB) 26 | client, err := mongo.Connect(Ctx, options.Client().ApplyURI(mongoUrl)) 27 | 28 | if err != nil { 29 | zap.L().Error("Connection MongoDB Error:", zap.Error(err)) 30 | return 31 | } 32 | 33 | // 检查连接 34 | err = client.Ping(Ctx, nil) 35 | if err != nil { 36 | zap.L().Error("Connection MongoDB Error:", zap.Error(err)) 37 | return 38 | } 39 | 40 | Mongo = client.Database("admin") 41 | return 42 | } 43 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # sh start.sh --service 启动任意服务 3 | output_dir="output" 4 | services_dir="service/" 5 | services=$(ls "$services_dir") 6 | 7 | while [[ $# -gt 0 ]] 8 | do 9 | case "$1" in 10 | --service) 11 | service_name="$2" 12 | shift 2 13 | ;; 14 | *) 15 | echo "Unknown option: $1" 16 | exit 1 17 | ;; 18 | esac 19 | done 20 | 21 | if [ -z "$service_name" ]; then 22 | echo "Error: --service option is required." 23 | exit 1 24 | fi 25 | 26 | found=false 27 | for s in $services; do 28 | if [ "$s" == "$service_name" ]; then 29 | found=true 30 | break 31 | fi 32 | done 33 | 34 | if [ "$found" = false ]; then 35 | echo "Error: Unrecognized service name: $service_name" 36 | printf 'Available service names:\n%s\n' "$services" 37 | exit 1 38 | fi 39 | 40 | command="$output_dir/bin/$service_name" 41 | 42 | # Check if the bootstrap.sh file exists 43 | if [ -f output/bootstrap-"${service_name}".sh ]; then 44 | command="$output_dir/bootstrap-${service_name}.sh" 45 | fi 46 | 47 | if [ ! -f "$command" ]; then 48 | echo "Error: Service binary not found: $command" 49 | exit 1 50 | fi 51 | 52 | "$command" -------------------------------------------------------------------------------- /common/ffmpegtool.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "go.uber.org/zap" 5 | "os/exec" 6 | ) 7 | 8 | // ffmpeg参数 9 | const ( 10 | inputVideoPathOption = "-i" 11 | startTimeOption = "-ss" 12 | startTime = "00:00:01" // 截取第1秒的帧 13 | ) 14 | 15 | //GetVideoFrames ffmpeg 实现,现弃用,改为使用oss的功能 16 | func GetVideoFrames(videoPath string, outputPath string) { 17 | if videoPath == "" || outputPath == "" { 18 | zap.L().Error("路径未指定") 19 | return 20 | } 21 | 22 | // 设置 ffmpeg 命令行参数,获取第1s的帧 23 | args := []string{inputVideoPathOption, videoPath, startTimeOption, startTime, "-vframes", "1", outputPath} 24 | 25 | // 创建 *exec.Cmd 26 | cmd := exec.Command("ffmpeg", args...) 27 | 28 | // 运行 ffmpeg 命令 29 | cmd.Run() 30 | } 31 | 32 | // Transcoding 转为h264 33 | func Transcoding(src string, dst string, overwrite bool) { 34 | args := []string{inputVideoPathOption, src, "-c:v", "libx264", "-strict", "-2", dst} 35 | if overwrite { 36 | args = append([]string{"-y"}, args...) 37 | } 38 | // 创建 *exec.Cmd 39 | cmd := exec.Command("ffmpeg", args...) 40 | 41 | // 运行 ffmpeg 命令 42 | if err := cmd.Run(); err != nil { 43 | zap.L().Error("ffmpeg出错",zap.Error(err)) 44 | return 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /dal/graphdb/init.go: -------------------------------------------------------------------------------- 1 | package graphdb 2 | 3 | import ( 4 | "douyin/config" 5 | "fmt" 6 | nebula "github.com/vesoft-inc/nebula-go/v3" 7 | ) 8 | 9 | // Initialize logger 10 | var log = nebula.DefaultLogger{} 11 | var sessionPool *nebula.SessionPool 12 | 13 | func Init(appConfig *config.AppConfig) (err error) { 14 | var conf *config.GraphDBConfig 15 | if appConfig.Mode == config.LocalMode { 16 | conf = appConfig.Local.GraphDBConfig 17 | } else { 18 | conf = appConfig.Remote.GraphDBConfig 19 | } 20 | 21 | hostAddress := nebula.HostAddress{Host: conf.Address, Port: conf.Port} 22 | 23 | // Create configs for session pool 24 | configs, err := nebula.NewSessionPoolConf( 25 | conf.Username, 26 | conf.Password, 27 | []nebula.HostAddress{hostAddress}, 28 | conf.Namespace, 29 | ) 30 | if err != nil { 31 | log.Fatal(fmt.Sprintf("failed to create graphDB session pool config, %s", err.Error())) 32 | return err 33 | } 34 | 35 | // create session pool 36 | sessionPool, err = nebula.NewSessionPool(*configs, nebula.DefaultLogger{}) 37 | if err != nil { 38 | log.Fatal(fmt.Sprintf("failed to initialize graphDB session pool, %s", err.Error())) 39 | return err 40 | } 41 | 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /mw/rocketMQ/favorite.go: -------------------------------------------------------------------------------- 1 | package rocketMQ 2 | 3 | import ( 4 | "douyin/dal/model" 5 | "github.com/apache/rocketmq-client-go/v2" 6 | "github.com/apache/rocketmq-client-go/v2/rlog" 7 | "log" 8 | ) 9 | 10 | type FavoriteMQ struct { 11 | MQ 12 | } 13 | 14 | var ( 15 | FavoriteMQInstance *FavoriteMQ 16 | ) 17 | 18 | func InitFavoriteMQ() rocketmq.PushConsumer { 19 | rlog.SetLogLevel("error") 20 | FavoriteMQInstance = &FavoriteMQ{ 21 | MQ{ 22 | Topic: "favorite", 23 | GroupId: "favorite_group", 24 | }, 25 | } 26 | 27 | // 创建 Comment 业务的生产者和消费者实例 28 | FavoriteMQInstance.Producer = rocketMQManager.NewProducer(FavoriteMQInstance.GroupId, FavoriteMQInstance.Topic) 29 | err := FavoriteMQInstance.Producer.Start() 30 | if err != nil { 31 | panic("启动favorite producer 失败") 32 | } 33 | 34 | FavoriteMQInstance.Consumer = rocketMQManager.NewConsumer(FavoriteMQInstance.GroupId) 35 | 36 | return FavoriteMQInstance.Consumer 37 | } 38 | 39 | // ProduceFavoriteMsg 生产点赞的消息 40 | func (m *FavoriteMQ) ProduceFavoriteMsg(message *model.FavoriteAction) error { 41 | _, err := rocketMQManager.ProduceMessage(m.Producer, message, m.Topic) 42 | 43 | if err != nil { 44 | log.Println("rocketMQ 发送favorite的消息失败:", err) 45 | return err 46 | } 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /idl/comment.thrift: -------------------------------------------------------------------------------- 1 | namespace go comment 2 | 3 | include "user.thrift" 4 | 5 | struct Comment { 6 | 1: i64 id, // 视频评论id 7 | 2: user.User user, // 评论用户信息 8 | 3: string content, // 评论内容 9 | 4: string create_date, // 评论发布日期,格式 mm-dd 10 | } 11 | 12 | struct CommentActionRequest { 13 | 1: i64 user_id, // 用户鉴权token 14 | 2: i64 video_id, // 视频id 15 | 3: i32 action_type, // 1-发布评论,2-删除评论 16 | 4: optional string comment_text, // 用户填写的评论内容,在action_type=1的时候使用 17 | 5: optional i64 comment_id, // 要删除的评论id,在action_type=2的时候使用 18 | } 19 | 20 | struct CommentActionResponse { 21 | 1: i32 status_code, 22 | 2: string status_msg, 23 | 3: optional Comment comment, // 评论成功返回评论内容,不需要重新拉取整个列表 24 | } 25 | 26 | struct CommentListRequest { 27 | 1: i64 user_id, // 用户id 28 | 2: i64 video_id, // 视频id 29 | } 30 | 31 | struct CommentListResponse { 32 | 1: i32 status_code, // 状态码,0-成功,其他值-失败 33 | 2: string status_msg, // 返回状态描述 34 | 3: list comment_list, // 评论列表 35 | } 36 | 37 | service CommentService { 38 | CommentActionResponse CommentAction(1: CommentActionRequest Request), 39 | CommentListResponse GetCommentList(1: CommentListRequest Request), 40 | i32 GetCommentCount(1: i64 video_id), //根据video_id获取评论数 41 | } 42 | -------------------------------------------------------------------------------- /idl/favorite.thrift: -------------------------------------------------------------------------------- 1 | namespace go favorite 2 | 3 | include "video.thrift" 4 | 5 | struct FavoriteActionRequest { 6 | 1: i64 user_id, // 用户id 7 | 2: i64 video_id, // 视频id 8 | 3: i32 action_type, // 1-点赞,2-取消点赞 9 | } 10 | 11 | struct FavoriteActionResponse { 12 | 1: i32 status_code, // 状态码,0-成功,其他值-失败 13 | 2: string status_msg, // 返回状态描述 14 | } 15 | 16 | struct FavoriteListRequest { 17 | 1: i64 action_id, // 当前操作用户的用户id 18 | 2: i64 user_id, //列出user_id点赞的视频 19 | } 20 | 21 | struct FavoriteListResponse { 22 | 1: i32 status_code, // 状态码,0-成功,其他值-失败 23 | 2: string status_msg, // 返回状态描述 24 | 3: list video_list, // 用户点赞视频列表 25 | } 26 | 27 | struct IsUserFavoriteRequest{ 28 | 1: i64 user_id, // 当前操作用户的用户id 29 | 2: i64 video_id, // 视频id 30 | } 31 | 32 | 33 | service FavoriteService { 34 | FavoriteActionResponse FavoriteAction(1: FavoriteActionRequest Request), 35 | FavoriteListResponse GetFavoriteList(1: FavoriteListRequest Request), 36 | i32 GetVideoFavoriteCount(1: i64 video_id),//获取video_id的点赞总数 37 | i32 GetUserFavoriteCount(1:i64 user_id), //获取user_id的点赞数 38 | i32 GetUserTotalFavoritedCount(1:i64 user_id), //获取user_id的总获赞数量 39 | bool IsUserFavorite(1:IsUserFavoriteRequest Request), 40 | } 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Hello everyone, the project will undergo a refactoring in November. The main goals of the refactoring include the introduction of Redis Sentinel, traceability, observability, Docker, a recommendation system implemented with Spark and Flink, Feed streams, and im system, as well as restructuring the directory and interfaces.If you have any questions or good ideas, please contact me via QQ 1914163770. 2 | 3 | 大家好,该项目将会在11月进行重构,主要重构的目标包括引入Redis Sentinel、链路追踪、可观测性、Docker、Spark及Flink实现的推荐系统、Feed流、im系统等,并对目录及接口进行重构。如果你有任何的疑问或者好的想法,请联系QQ 1914163770 4 | 5 | # 第六届字节跳动青训营 后端 极简版抖音项目 6 | 项目荣获第六届青训营二等奖,感谢队友的支持
7 | 本项目是使用Go语言开发,基于Hertz + Kitex + MySQL + MongoDB + Redis + Kafka + Gorm + Zap + Etcd +OSS等技术实现的极简版抖音APP后端项目,该项目部署在华为云服务器上,实现了基础功能以及互动和社交方向的全部功能。
8 | 项目团队:起名起了3min
9 | 项目文档:https://vish8y9znlg.feishu.cn/docx/XffIdI4sso6oGNx2yWEc4DV4nrh
10 | 在架构选型上,项目演进从gin -> hertz+kitex
11 | 其中main分支为稳定大版本
12 | develop-rpc 为目前正在开发的rpc分支
13 | develop分支为最初的gin单体原型设计
14 | 目前main分支和本地分支不同步,因为在找工作暂时搁置了,后面等不忙了再重新优化 15 | 16 | **如何运行本项目?**
17 | 使用 Linux 环境
18 | 安装go 1.20 版本
19 | 安装MySQL 8.0 及以上版本
20 | 安装Redis 6.2 及以上版本
21 | 安装Kafka 3.0 及以上版本
22 | 安装MongoDB 4.4 及以上版本
23 | 24 | 并在config/app.yaml 中修改配置
25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /dal/model/video.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "gorm.io/gorm" 5 | ) 6 | 7 | type Video struct { 8 | ID uint `gorm:"primaryKey"` 9 | AuthorId uint `gorm:"index"` 10 | VideoUrl string `gorm:"not null"` 11 | CoverUrl string `gorm:"not null"` 12 | Title string 13 | CreatedAt int64 `gorm:"autoCreateTime"` 14 | DeletedAt gorm.DeletedAt 15 | } 16 | 17 | type VideoResponse struct { 18 | ID uint `json:"id"` 19 | Author UserResponse `json:"author"` 20 | PlayUrl string `json:"play_url"` 21 | CoverUrl string `json:"cover_url"` 22 | FavoriteCount int64 `json:"favorite_count"` //点赞数 23 | CommentCount int64 `json:"comment_count"` //评论数 24 | IsFavorite bool `json:"is_favorite"` //是否点赞 25 | Title string `json:"title"` //视频标题 26 | } 27 | 28 | // VideoListResponse 用户所有投稿过的视频 29 | type VideoListResponse struct { 30 | Response 31 | VideoResponse []VideoResponse `json:"video_list"` 32 | } 33 | 34 | // FeedListResponse 投稿时间倒序的视频列表 35 | type FeedListResponse struct { 36 | Response 37 | NextTime int64 `json:"next_time"` 38 | VideoResponse []VideoResponse `json:"video_list"` 39 | } 40 | 41 | func (*Video) TableName() string { 42 | return "video" 43 | } 44 | -------------------------------------------------------------------------------- /dal/mysql/init.go: -------------------------------------------------------------------------------- 1 | package mysql 2 | 3 | import ( 4 | "douyin/config" 5 | "fmt" 6 | "gorm.io/driver/mysql" 7 | "gorm.io/gorm" 8 | "gorm.io/gorm/logger" 9 | "log" 10 | "os" 11 | "time" 12 | ) 13 | 14 | var ( 15 | DB *gorm.DB 16 | ) 17 | 18 | func Init(appConfig *config.AppConfig) (err error) { 19 | var conf *config.MySQLConfig 20 | if appConfig.Mode == config.LocalMode { 21 | conf = appConfig.Local.MySQLConfig 22 | } else { 23 | conf = appConfig.Remote.MySQLConfig 24 | } 25 | dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", 26 | conf.Username, 27 | conf.Password, 28 | conf.Address, 29 | conf.Port, 30 | conf.Database, 31 | ) 32 | 33 | mysqlLog := logger.New( 34 | log.New(os.Stdout, "\r\n", log.LstdFlags), 35 | logger.Config{ 36 | SlowThreshold: time.Second, 37 | IgnoreRecordNotFoundError: true, 38 | LogLevel: logger.Error, 39 | Colorful: false, 40 | }) 41 | 42 | DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{Logger: mysqlLog, PrepareStmt: true}) 43 | if err != nil { 44 | log.Println(dsn) 45 | log.Fatal("connect to mysql failed:", err) 46 | } 47 | //err = DB.AutoMigrate(&models.User{}) 48 | //if err != nil { 49 | // return 50 | //} 51 | return nil 52 | } 53 | -------------------------------------------------------------------------------- /add_new_service.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 检查 go 命令是否存在于 PATH 环境变量中 4 | if ! command -v go &>/dev/null; then 5 | echo "错误:go 命令未在 PATH 中找到。请安装或将其添加到 PATH 中。" 6 | exit 1 7 | fi 8 | 9 | ## 检查 protoc 命令是否存在于 PATH 环境变量中 10 | #if ! command -v protoc &>/dev/null; then 11 | # echo "错误:protoc 命令未在 PATH 中找到。请安装或将其添加到 PATH 中。" 12 | # # 检查操作系统是否为 macOS 13 | # if [[ $(uname) == "Darwin" ]]; then 14 | # # 如果是macOS,则检查brew是否在PATH中 15 | # if ! command -v brew &>/dev/null; then 16 | # echo "错误:brew 命令未在 PATH 中找到。请安装或将其添加到 PATH 中。" 17 | # exit 1 18 | # else 19 | # echo "尝试安装 protoc......" 20 | # brew install protobuf 21 | # fi 22 | # fi 23 | #fi 24 | # 25 | ## 再次检查 protoc 命令是否存在于 PATH 环境变量中 26 | #if ! command -v protoc &>/dev/null; then 27 | # echo "错误:protoc 命令未在 PATH 中找到,看起来安装失败了。请手动安装。" 28 | # exit 1 29 | #fi 30 | 31 | # 检查 kitex 命令是否存在于 PATH 环境变量中 32 | if ! command -v kitex &>/dev/null; then 33 | echo "错误:kitex 命令未在 PATH 中找到,尝试安装......" 34 | go install github.com/cloudwego/kitex/tool/cmd/kitex@latest 35 | fi 36 | 37 | # 再次检查 kitex 命令是否存在于 PATH 环境变量中 38 | if ! command -v kitex &>/dev/null; then 39 | echo "错误:kitex 命令未在 PATH 中找到,看起来安装失败了。请手动安装。" 40 | exit 1 41 | fi 42 | 43 | # 在kitex_gen目录生成struct传输和调用方法等文件 44 | mkdir -p kitex_gen 45 | kitex -module douyin -I idl/ idl/"$1".thrift 46 | 47 | # 生成service文件 48 | mkdir -p service/"$1" 49 | cd service/"$1" && kitex -module "douyin" -service "$1" -use douyin/kitex_gen/ -I ../../idl/ ../../idl/"$1".thrift 50 | 51 | go mod tidy -------------------------------------------------------------------------------- /dal/mysql/user.go: -------------------------------------------------------------------------------- 1 | package mysql 2 | 3 | import ( 4 | "douyin/dal/model" 5 | "go.uber.org/zap" 6 | 7 | "gorm.io/gorm" 8 | ) 9 | 10 | func FindUserByName(name string) (user model.User, exist bool, err error) { 11 | user = model.User{} 12 | if err = DB.Where("name = ?", name).First(&user).Error; err != nil { 13 | if err == gorm.ErrRecordNotFound { 14 | return user, false, nil 15 | } 16 | // 处理其他查询错误 17 | zap.L().Error("Database err", zap.Error(err)) 18 | return user, false, err 19 | } 20 | return user, true, nil 21 | } 22 | 23 | func FindUserByUserID(id uint) (user model.User, exist bool, err error) { 24 | user = model.User{} 25 | if err = DB.Where("id = ?", id).First(&user).Error; err != nil { 26 | if err == gorm.ErrRecordNotFound { 27 | return user, false, err 28 | } 29 | // 处理其他查询错误 30 | zap.L().Error("Database err", zap.Error(err)) 31 | return user, false, err 32 | } 33 | return user, true, nil 34 | } 35 | 36 | func CreateUser(user *model.User) error { 37 | userInfo := model.UserInfo{ 38 | ID: user.ID, 39 | Name: user.Name, 40 | } 41 | tx := DB.Begin() 42 | defer func() { 43 | if r := recover(); r != nil { 44 | tx.Rollback() 45 | } 46 | }() 47 | 48 | if err := tx.Error; err != nil { 49 | return err 50 | } 51 | 52 | if err := tx.Create(user).Error; err != nil { 53 | tx.Rollback() 54 | return err 55 | } 56 | 57 | if err := tx.Create(&userInfo).Error; err != nil { 58 | tx.Rollback() 59 | return err 60 | } 61 | 62 | return tx.Commit().Error 63 | } 64 | -------------------------------------------------------------------------------- /dal/mongo/message.go: -------------------------------------------------------------------------------- 1 | package mongo 2 | 3 | import ( 4 | "douyin/dal/model" 5 | "encoding/json" 6 | "go.mongodb.org/mongo-driver/bson" 7 | _ "go.mongodb.org/mongo-driver/bson" 8 | "log" 9 | ) 10 | 11 | func SendMessage(data []byte) (err error) { 12 | message := new(model.Message) 13 | err = json.Unmarshal(data, message) 14 | if err != nil { 15 | log.Println("kafka获取message反序列化失败:", err) 16 | } 17 | collection := Mongo.Collection("messages") 18 | _, err = collection.InsertOne(Ctx, message) 19 | if err != nil { 20 | log.Println("消息插入到 MongoDB失败。") 21 | return err 22 | } 23 | return 24 | } 25 | 26 | func GetMessageList(fromUserId, toUserId uint, preMsgTime int64) ([]*model.Message, error) { 27 | collection := Mongo.Collection("messages") 28 | filter := bson.M{ 29 | "$and": []bson.M{ 30 | {"$or": []bson.M{ 31 | {"fromuserid": fromUserId, "touserid": toUserId}, 32 | {"fromuserid": toUserId, "touserid": fromUserId}, 33 | }}, 34 | {"createtime": bson.M{"$gt": preMsgTime}}, // 添加时间戳条件 35 | }, 36 | } 37 | 38 | cursor, err := collection.Find(Ctx, filter) 39 | if err != nil { 40 | log.Fatal(err) 41 | } 42 | defer cursor.Close(Ctx) 43 | 44 | var messages []*model.Message 45 | for cursor.Next(Ctx) { 46 | var message model.Message 47 | if err := cursor.Decode(&message); err != nil { 48 | log.Println("解码错误:", err) 49 | continue 50 | } 51 | messages = append(messages, &message) 52 | } 53 | 54 | if err := cursor.Err(); err != nil { 55 | log.Fatal(err) 56 | } 57 | 58 | return messages, nil 59 | } 60 | -------------------------------------------------------------------------------- /common/jwt.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "github.com/dgrijalva/jwt-go" 5 | "go.uber.org/zap" 6 | "time" 7 | ) 8 | 9 | type Claims struct { 10 | ID uint `json:"id"` 11 | Username string `json:"username"` 12 | jwt.StandardClaims 13 | } 14 | 15 | // 签名密钥 16 | var jwtSecretKey = []byte("DouShenNo1") 17 | 18 | // GenerateToken 创建token, 过期时间设置已注释 19 | func GenerateToken(userId uint, username string) string { 20 | 21 | nowTime := time.Now() 22 | //expireTime := nowTime.Add(24 * time.Hour).Unix() 23 | //log.Println("expireTime:", expireTime) 24 | 25 | claims := Claims{ 26 | ID: userId, 27 | Username: username, 28 | StandardClaims: jwt.StandardClaims{ 29 | //ExpiresAt: expireTime, 30 | IssuedAt: nowTime.Unix(), 31 | Issuer: "DouShen", 32 | }, 33 | } 34 | // 使用用于签名的算法和令牌 35 | tokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 36 | // 创建JWT字符串 37 | if token, err := tokenClaims.SignedString(jwtSecretKey); err != nil { 38 | zap.L().Error("generate token fail!", zap.Error(err)) 39 | return "fail" 40 | } else { 41 | zap.L().Info("generate token success!") 42 | return token 43 | } 44 | } 45 | 46 | // ParseToken 解析token 47 | func ParseToken(token string) (*Claims, error) { 48 | tokenClaims, err := jwt.ParseWithClaims(token, &Claims{}, 49 | func(token *jwt.Token) (interface{}, error) { 50 | return jwtSecretKey, nil 51 | }) 52 | if err != nil { 53 | return nil, err 54 | } 55 | if tokenClaims != nil { 56 | if claims, ok := tokenClaims.Claims.(*Claims); ok && tokenClaims.Valid { 57 | return claims, nil 58 | } 59 | } 60 | return nil, err 61 | } 62 | -------------------------------------------------------------------------------- /mw/redis/comment.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | _ "github.com/redis/go-redis/v9" 5 | "math/rand" 6 | "strconv" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | // GetCommentCountByVideoId 根据videoId查找评论数 12 | func GetCommentCountByVideoId(videoId uint) (int64, error) { 13 | baseSliceVideo := []string{VideoKey, strconv.Itoa(int(videoId))} 14 | key := strings.Join(baseSliceVideo, Delimiter) 15 | 16 | count, err := Rdb.HGet(Ctx, key, CommentCountField).Result() 17 | commentCount, _ := strconv.ParseInt(count, 10, 64) 18 | return commentCount, err 19 | } 20 | 21 | // IncrementCommentCountByVideoId 给videoId对应的评论数加一 22 | func IncrementCommentCountByVideoId(videoId uint) error { 23 | baseSliceVideo := []string{VideoKey, strconv.Itoa(int(videoId))} 24 | key := strings.Join(baseSliceVideo, Delimiter) 25 | //增加并返回评论数 26 | _, err := Rdb.HIncrBy(Ctx, key, CommentCountField, 1).Result() 27 | return err 28 | } 29 | 30 | // DecrementCommentCountByVideoId 给videoId对应的评论数减一 31 | func DecrementCommentCountByVideoId(videoId uint) error { 32 | baseSliceVideo := []string{VideoKey, strconv.Itoa(int(videoId))} 33 | key := strings.Join(baseSliceVideo, Delimiter) 34 | //减少并返回评论数 35 | _, err := Rdb.HIncrBy(Ctx, key, CommentCountField, -1).Result() 36 | return err 37 | } 38 | 39 | func SetCommentCountByVideoId(videoId uint, commentCount int64) error { 40 | baseSliceVideo := []string{VideoKey, strconv.Itoa(int(videoId))} 41 | key := strings.Join(baseSliceVideo, Delimiter) 42 | err := Rdb.HSet(Ctx, key, CommentCountField, commentCount).Err() 43 | randomSeconds := 600 + rand.Intn(31) // 600秒到630秒之间的随机数 44 | expiration := time.Duration(randomSeconds) * time.Second 45 | Rdb.Expire(Ctx, key, expiration) 46 | return err 47 | } 48 | -------------------------------------------------------------------------------- /logger/init.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "douyin/config" 5 | "github.com/natefinch/lumberjack" 6 | "go.uber.org/zap" 7 | "go.uber.org/zap/zapcore" 8 | "os" 9 | ) 10 | 11 | var lg *zap.Logger 12 | 13 | // Init 初始化lg 14 | func Init(cfg *config.LogConfig, mode string) (err error) { 15 | writeSyncer := getLogWriter(cfg.Filename, cfg.MaxSize, cfg.MaxBackups, cfg.MaxAge) 16 | encoder := getEncoder() 17 | var l = new(zapcore.Level) 18 | err = l.UnmarshalText([]byte(cfg.Level)) 19 | if err != nil { 20 | return 21 | } 22 | var core zapcore.Core 23 | if mode == "local" { 24 | // 进入开发模式,日志输出到终端 25 | consoleEncoder := zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig()) 26 | core = zapcore.NewTee( 27 | zapcore.NewCore(consoleEncoder, zapcore.Lock(os.Stdout), zapcore.DebugLevel), 28 | ) 29 | } else { 30 | core = zapcore.NewCore(encoder, writeSyncer, l) 31 | } 32 | 33 | lg = zap.New(core, zap.AddCaller()) 34 | zap.ReplaceGlobals(lg) 35 | zap.L().Info("init logger success") 36 | return 37 | } 38 | 39 | func getEncoder() zapcore.Encoder { 40 | encoderConfig := zap.NewProductionEncoderConfig() 41 | encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder 42 | encoderConfig.TimeKey = "time" 43 | encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder 44 | encoderConfig.EncodeDuration = zapcore.SecondsDurationEncoder 45 | encoderConfig.EncodeCaller = zapcore.ShortCallerEncoder 46 | return zapcore.NewJSONEncoder(encoderConfig) 47 | } 48 | 49 | func getLogWriter(filename string, maxSize, maxBackup, maxAge int) zapcore.WriteSyncer { 50 | lumberJackLogger := &lumberjack.Logger{ 51 | Filename: filename, 52 | MaxSize: maxSize, 53 | MaxBackups: maxBackup, 54 | MaxAge: maxAge, 55 | } 56 | return zapcore.AddSync(lumberJackLogger) 57 | } 58 | -------------------------------------------------------------------------------- /dal/mysql/comment.go: -------------------------------------------------------------------------------- 1 | package mysql 2 | 3 | import ( 4 | "douyin/dal/model" 5 | "go.uber.org/zap" 6 | "gorm.io/gorm" 7 | ) 8 | 9 | func AddComment(comment *model.Comment) (uint, error) { 10 | result := DB.Model(model.Comment{}).Create(comment) 11 | // 判断是否创建成功 12 | if result.Error != nil { 13 | zap.L().Error("创建 Comment 失败:", zap.Error(result.Error)) 14 | return 0, result.Error 15 | } else { 16 | return comment.ID, nil 17 | } 18 | } 19 | 20 | func FindCommentsByVideoId(videoId uint) ([]model.Comment, error) { 21 | comments := make([]model.Comment, 0) 22 | result := DB.Where("video_id = ?", videoId).Order("created_at desc").Find(&comments) 23 | if result.Error != nil && result.Error == gorm.ErrRecordNotFound { 24 | return nil, result.Error 25 | } 26 | return comments, nil 27 | } 28 | 29 | func FindCommentById(commentId uint) (model.Comment, error) { 30 | comment := model.Comment{} 31 | result := DB.Find(&comment, commentId) 32 | if result.Error != nil && result.Error == gorm.ErrRecordNotFound { 33 | return comment, result.Error 34 | } 35 | return comment, nil 36 | } 37 | 38 | func DeleteCommentById(commentId uint) error { 39 | result := DB.Delete(&model.Comment{}, commentId) 40 | if result.Error != nil && result.Error == gorm.ErrRecordNotFound { 41 | return result.Error 42 | } 43 | return nil 44 | } 45 | 46 | func GetCommentCnt(videoId uint) (int64, error) { 47 | var cnt int64 48 | err := DB.Model(&model.Comment{}).Where("video_id = ?", videoId).Count(&cnt).Error 49 | // 返回评论数和是否查询成功 50 | return cnt, err 51 | } 52 | 53 | func IsCommentBelongsToUser(commentId *int64, userId int64) (bool, error) { 54 | var cnt int64 55 | err := DB.Model(&model.Comment{}).Where("id = ? and user_id = ?", commentId, userId).Count(&cnt).Error 56 | return cnt != 0, err 57 | } 58 | -------------------------------------------------------------------------------- /idl/video.thrift: -------------------------------------------------------------------------------- 1 | namespace go video 2 | 3 | include "user.thrift" 4 | 5 | struct Video { 6 | 1: i64 id, // 视频唯一标识 7 | 2: user.User author, // 视频作者信息 8 | 3: string play_url, // 视频播放地址 9 | 4: string cover_url, // 视频封面地址 10 | 5: i32 favorite_count, // 视频的点赞总数 11 | 6: i32 comment_count, // 视频的评论总数 12 | 7: bool is_favorite, // true-已点赞,false-未点赞 13 | 8: string title, // 视频标题 14 | } 15 | 16 | struct VideoFeedRequest { 17 | 1: optional string latest_time, // 可选参数,限制返回视频的最新投稿时间戳,精确到秒,不填表示当前时间 18 | 2: i64 user_id, // 可选参数,登录用户设置 19 | } 20 | 21 | struct VideoFeedResponse { 22 | 1: i32 status_code, // 状态码,0-成功,其他值-失败 23 | 2: string status_msg, // 返回状态描述 24 | 3: list