├── scripts ├── microservice │ ├── api.sh │ ├── user.sh │ ├── comment.sh │ ├── message.sh │ ├── video.sh │ ├── favorite.sh │ └── relation.sh ├── etcd │ ├── network.sh │ ├── client.sh │ └── server.sh ├── ffmpeg.sh ├── etcd.sh ├── rabbitmq.sh ├── redis.sh ├── minio.sh └── nginx.sh ├── config ├── log.yml ├── crypt.yml ├── user.yml ├── message.yml ├── comment.yml ├── favorite.yml ├── relation.yml ├── video.yml ├── rabbitmq.yml ├── minio.yml ├── api.yml ├── nginx │ └── conf │ │ ├── conf.d │ │ ├── api_https_proxy.conf │ │ └── api_http_proxy.conf │ │ └── nginx.conf └── db.yml ├── pic ├── 抖声_ER图.png ├── 抖声_后端架构图.png ├── 测试-feed-1.png ├── 测试-feed-2.png ├── 点赞关注同步机制.png ├── 测试-favorite-1.png ├── 测试-favorite-2.png ├── 测试-favorite-3.png └── 测试-relation-1.png ├── internal ├── response │ ├── base.go │ ├── favorite.go │ ├── message.go │ ├── comment.go │ ├── video.go │ ├── user.go │ └── relation.go └── tool │ ├── snapshot.go │ ├── crypt_test.go │ └── crypt.go ├── cmd ├── user │ ├── service │ │ └── init.go │ ├── build.sh │ ├── script │ │ └── bootstrap.sh │ └── main.go ├── video │ ├── service │ │ ├── init.go │ │ └── upload.go │ ├── build.sh │ ├── script │ │ └── bootstrap.sh │ └── main.go ├── comment │ ├── service │ │ └── init.go │ ├── build.sh │ ├── script │ │ └── bootstrap.sh │ └── main.go ├── favorite │ ├── build.sh │ ├── script │ │ └── bootstrap.sh │ ├── service │ │ ├── init.go │ │ └── timer.go │ └── main.go ├── message │ ├── build.sh │ ├── service │ │ ├── init.go │ │ └── handler.go │ ├── script │ │ └── bootstrap.sh │ └── main.go ├── relation │ ├── build.sh │ ├── script │ │ └── bootstrap.sh │ ├── service │ │ ├── init.go │ │ └── timer.go │ └── main.go └── api │ ├── rpc │ ├── init.go │ ├── comment.go │ ├── message.go │ ├── favorite.go │ ├── user.go │ ├── video.go │ └── relation.go │ ├── handler │ ├── favorite.go │ ├── message.go │ ├── comment.go │ ├── user.go │ ├── video.go │ └── relation.go │ └── main.go ├── dockerfiles ├── rpc │ ├── comment │ │ └── Dockerfile │ ├── user │ │ └── Dockerfile │ ├── video │ │ └── Dockerfile │ ├── relation │ │ └── Dockerfile │ ├── favorite │ │ └── Dockerfile │ └── message │ │ └── Dockerfile └── api │ └── Dockerfile ├── shutdown.sh ├── pkg ├── gocron │ ├── gocron.go │ └── gocron_test.go ├── middleware │ ├── tls.go │ ├── limit.go │ ├── log.go │ ├── limit_test.go │ ├── server.go │ ├── client.go │ ├── auth_test.go │ ├── tls_test.go │ ├── common.go │ ├── auth.go │ ├── limit_init.go │ └── test_util.go ├── etcd │ ├── common.go │ ├── discovery.go │ └── registry.go ├── viper │ └── viper.go ├── async │ └── async.go ├── rabbitmq │ ├── rabbitmq_test.go │ ├── init.go │ ├── emit_logs.go │ ├── receive_logs.go │ └── rabbitmq.go ├── jwt │ ├── jwt_test.go │ └── jwt.go ├── minio │ ├── init.go │ └── minio.go ├── zap │ └── zap.go └── errno │ └── errno.go ├── kitex ├── kitex.sh ├── kitex_gen │ ├── user │ │ └── userservice │ │ │ ├── server.go │ │ │ ├── invoker.go │ │ │ └── client.go │ ├── video │ │ └── videoservice │ │ │ ├── server.go │ │ │ ├── invoker.go │ │ │ └── client.go │ ├── comment │ │ └── commentservice │ │ │ ├── server.go │ │ │ ├── invoker.go │ │ │ └── client.go │ ├── message │ │ └── messageservice │ │ │ ├── server.go │ │ │ ├── invoker.go │ │ │ └── client.go │ ├── favorite │ │ └── favoriteservice │ │ │ ├── server.go │ │ │ ├── invoker.go │ │ │ └── client.go │ └── relation │ │ └── relationservice │ │ ├── server.go │ │ ├── invoker.go │ │ └── client.go ├── favorite.proto ├── message.proto ├── comment.proto ├── user.proto ├── video.proto └── relation.proto ├── .gitignore ├── dal ├── redis │ ├── message.go │ └── init.go └── db │ ├── publish.go │ ├── init.go │ ├── feed.go │ ├── comment.go │ ├── message.go │ └── user.go ├── startup.sh ├── LICENSE └── docker-compose.yml /scripts/microservice/api.sh: -------------------------------------------------------------------------------- 1 | go run ../../cmd/api/main.go -------------------------------------------------------------------------------- /scripts/microservice/user.sh: -------------------------------------------------------------------------------- 1 | go run ../../cmd/user/main.go -------------------------------------------------------------------------------- /config/log.yml: -------------------------------------------------------------------------------- 1 | info: logs/info.log 2 | error: logs/error.log -------------------------------------------------------------------------------- /scripts/microservice/comment.sh: -------------------------------------------------------------------------------- 1 | go run ../../cmd/comment/main.go -------------------------------------------------------------------------------- /scripts/microservice/message.sh: -------------------------------------------------------------------------------- 1 | go run ../../cmd/message/main.go -------------------------------------------------------------------------------- /scripts/microservice/video.sh: -------------------------------------------------------------------------------- 1 | go run ../../cmd/video/main.go -------------------------------------------------------------------------------- /scripts/microservice/favorite.sh: -------------------------------------------------------------------------------- 1 | go run ../../cmd/favorite/main.go -------------------------------------------------------------------------------- /scripts/microservice/relation.sh: -------------------------------------------------------------------------------- 1 | go run ../../cmd/relation/main.go -------------------------------------------------------------------------------- /scripts/etcd/network.sh: -------------------------------------------------------------------------------- 1 | docker network create app-tier --driver bridge 2 | -------------------------------------------------------------------------------- /pic/抖声_ER图.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytedance-youthcamp-jbzx/tiktok/HEAD/pic/抖声_ER图.png -------------------------------------------------------------------------------- /pic/抖声_后端架构图.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytedance-youthcamp-jbzx/tiktok/HEAD/pic/抖声_后端架构图.png -------------------------------------------------------------------------------- /pic/测试-feed-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytedance-youthcamp-jbzx/tiktok/HEAD/pic/测试-feed-1.png -------------------------------------------------------------------------------- /pic/测试-feed-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytedance-youthcamp-jbzx/tiktok/HEAD/pic/测试-feed-2.png -------------------------------------------------------------------------------- /pic/点赞关注同步机制.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytedance-youthcamp-jbzx/tiktok/HEAD/pic/点赞关注同步机制.png -------------------------------------------------------------------------------- /config/crypt.yml: -------------------------------------------------------------------------------- 1 | rsa: 2 | tiktok_message_encrypt_public_key: "" 3 | tiktok_message_decrypt_private_key: "" -------------------------------------------------------------------------------- /pic/测试-favorite-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytedance-youthcamp-jbzx/tiktok/HEAD/pic/测试-favorite-1.png -------------------------------------------------------------------------------- /pic/测试-favorite-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytedance-youthcamp-jbzx/tiktok/HEAD/pic/测试-favorite-2.png -------------------------------------------------------------------------------- /pic/测试-favorite-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytedance-youthcamp-jbzx/tiktok/HEAD/pic/测试-favorite-3.png -------------------------------------------------------------------------------- /pic/测试-relation-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytedance-youthcamp-jbzx/tiktok/HEAD/pic/测试-relation-1.png -------------------------------------------------------------------------------- /scripts/ffmpeg.sh: -------------------------------------------------------------------------------- 1 | sudo apt-get install ffmpeg && go get -u github.com/u2takey/ffmpeg-go && go get -u github.com/disintegration/imaging -------------------------------------------------------------------------------- /internal/response/base.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | type Base struct { 4 | StatusCode int `json:"status_code"` 5 | StatusMsg string `json:"status_msg"` 6 | } 7 | -------------------------------------------------------------------------------- /scripts/etcd.sh: -------------------------------------------------------------------------------- 1 | docker run -d -p 2379:2379 -p 2380:2380 appcelerator/etcd --listen-client-urls http://127.0.0.1:2379 --advertise-client-urls http://127.0.0.1:2379 2 | 3 | -------------------------------------------------------------------------------- /scripts/etcd/client.sh: -------------------------------------------------------------------------------- 1 | docker run -it --rm \ 2 | --network app-tier \ 3 | --env ALLOW_NONE_AUTHENTICATION=yes \ 4 | bitnami/etcd:latest etcdctl --endpoints http://1.12.68.184:2379 put /message Hello 5 | -------------------------------------------------------------------------------- /scripts/rabbitmq.sh: -------------------------------------------------------------------------------- 1 | docker run -d --hostname rabbitmq --name rabbitmq -p 15672:15672 -p 5672:5672 -e RABBITMQ_DEFAULT_USER=tiktokRMQ -e RABBITMQ_DEFAULT_PASS=tiktokRMQ -e RABBITMQ_DEFAULT_VHOST=tiktokRMQ rabbitmq:3.11.8-management -------------------------------------------------------------------------------- /config/user.yml: -------------------------------------------------------------------------------- 1 | server: 2 | name: "TiktokUserServer" 3 | host: 0.0.0.0 4 | port: 8085 5 | 6 | rpc: 7 | host: 127.0.0.1 8 | port: 50055 9 | 10 | JWT: 11 | signingKey: "signingKey" 12 | 13 | etcd: 14 | host: 0.0.0.0 15 | port: 2379 -------------------------------------------------------------------------------- /config/message.yml: -------------------------------------------------------------------------------- 1 | server: 2 | name: "TiktokMessageServer" 3 | host: 0.0.0.0 4 | port: 8083 5 | 6 | rpc: 7 | host: 127.0.0.1 8 | port: 50053 9 | 10 | JWT: 11 | signingKey: "signingKey" 12 | 13 | etcd: 14 | host: 0.0.0.0 15 | port: 2379 -------------------------------------------------------------------------------- /cmd/user/service/init.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/jwt" 5 | ) 6 | 7 | var ( 8 | Jwt *jwt.JWT 9 | ) 10 | 11 | func Init(signingKey string) { 12 | Jwt = jwt.NewJWT([]byte(signingKey)) 13 | } 14 | -------------------------------------------------------------------------------- /cmd/video/service/init.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/jwt" 5 | ) 6 | 7 | var ( 8 | Jwt *jwt.JWT 9 | ) 10 | 11 | func Init(signingKey string) { 12 | Jwt = jwt.NewJWT([]byte(signingKey)) 13 | } 14 | -------------------------------------------------------------------------------- /config/comment.yml: -------------------------------------------------------------------------------- 1 | server: 2 | name: "TiktokCommentServer" 3 | host: 0.0.0.0 4 | port: 8081 5 | 6 | rpc: 7 | host: 127.0.0.1 8 | port: 50051 9 | 10 | JWT: 11 | signingKey: "signingKey" 12 | 13 | etcd: 14 | host: 0.0.0.0 15 | port: 2379 16 | -------------------------------------------------------------------------------- /config/favorite.yml: -------------------------------------------------------------------------------- 1 | server: 2 | name: "TiktokFavoriteServer" 3 | host: 0.0.0.0 4 | port: 8082 5 | 6 | rpc: 7 | host: 127.0.0.1 8 | port: 50052 9 | 10 | JWT: 11 | signingKey: "signingKey" 12 | 13 | etcd: 14 | host: 0.0.0.0 15 | port: 2379 -------------------------------------------------------------------------------- /config/relation.yml: -------------------------------------------------------------------------------- 1 | server: 2 | name: "TiktokRelationServer" 3 | host: 0.0.0.0 4 | port: 8084 5 | 6 | rpc: 7 | host: 127.0.0.1 8 | port: 50054 9 | 10 | JWT: 11 | signingKey: "signingKey" 12 | 13 | etcd: 14 | host: 0.0.0.0 15 | port: 2379 -------------------------------------------------------------------------------- /cmd/comment/service/init.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/jwt" 5 | ) 6 | 7 | var ( 8 | Jwt *jwt.JWT 9 | ) 10 | 11 | func Init(signingKey string) { 12 | Jwt = jwt.NewJWT([]byte(signingKey)) 13 | } 14 | -------------------------------------------------------------------------------- /dockerfiles/rpc/comment/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.19 AS builder 2 | 3 | LABEL stage=gobuilder 4 | 5 | ENV CGO_ENABLED 0 6 | ENV GOPROXY https://goproxy.cn,direct 7 | 8 | WORKDIR /app 9 | ADD commentsrv . 10 | COPY config/ config/ 11 | EXPOSE 8081 12 | CMD ["./commentsrv"] -------------------------------------------------------------------------------- /dockerfiles/rpc/user/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.19 AS builder 2 | 3 | LABEL stage=gobuilder 4 | 5 | ENV CGO_ENABLED 0 6 | ENV GOPROXY https://goproxy.cn,direct 7 | 8 | 9 | WORKDIR /app 10 | ADD usersrv . 11 | COPY config/ config/ 12 | EXPOSE 8085 13 | CMD ["./usersrv"] 14 | -------------------------------------------------------------------------------- /dockerfiles/rpc/video/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.19 AS builder 2 | 3 | LABEL stage=gobuilder 4 | 5 | ENV CGO_ENABLED 0 6 | ENV GOPROXY https://goproxy.cn,direct 7 | 8 | 9 | WORKDIR /app 10 | ADD videosrv . 11 | COPY config/ config/ 12 | EXPOSE 8086 13 | CMD ["./videosrv"] 14 | -------------------------------------------------------------------------------- /scripts/redis.sh: -------------------------------------------------------------------------------- 1 | docker run --log-opt max-size=100m --log-opt max-file=2 -p 6379:6379 --name redis -v /home/redis.conf:/etc/redis/redis.conf -v /home/redis/data:/data -d redis redis-server /etc/redis/redis.conf --appendonly yes --requirepass tiktokRedis && docker exec -it redis redis-cli -------------------------------------------------------------------------------- /scripts/etcd/server.sh: -------------------------------------------------------------------------------- 1 | docker run -d --name etcd-server \ 2 | --network app-tier \ 3 | --publish 2379:2379 \ 4 | --publish 2380:2380 \ 5 | --env ALLOW_NONE_AUTHENTICATION=yes \ 6 | --env ETCD_ADVERTISE_CLIENT_URLS=http://1.12.68.184:2379 \ 7 | bitnami/etcd:latest 8 | -------------------------------------------------------------------------------- /dockerfiles/rpc/relation/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.19 AS builder 2 | 3 | LABEL stage=gobuilder 4 | 5 | ENV CGO_ENABLED 0 6 | ENV GOPROXY https://goproxy.cn,direct 7 | 8 | 9 | WORKDIR /app 10 | ADD relationsrv . 11 | COPY config/ config/ 12 | EXPOSE 8084 13 | CMD ["./relationsrv"] 14 | -------------------------------------------------------------------------------- /dockerfiles/rpc/favorite/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.19 AS builder 2 | 3 | LABEL stage=gobuilder 4 | 5 | ENV CGO_ENABLED 0 6 | ENV GOPROXY https://goproxy.cn,direct 7 | 8 | 9 | WORKDIR /app 10 | ADD favoritesrv . 11 | COPY config/ config/ 12 | EXPOSE 8082 13 | CMD ["./favoritesrv"] 14 | 15 | -------------------------------------------------------------------------------- /internal/response/favorite.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | import "github.com/bytedance-youthcamp-jbzx/tiktok/kitex/kitex_gen/video" 4 | 5 | type FavoriteAction struct { 6 | Base 7 | } 8 | 9 | type FavoriteList struct { 10 | Base 11 | VideoList []*video.Video `json:"video_list"` 12 | } 13 | -------------------------------------------------------------------------------- /shutdown.sh: -------------------------------------------------------------------------------- 1 | session_name=dousheng 2 | 3 | tmux has-session -t $session_name 4 | if [ $? -eq 0 ];then 5 | for i in $(seq 0 6) 6 | do 7 | echo "closing window: $i" 8 | tmux send-keys -t $session_name:$i C-c C-m "exit" C-m 9 | done 10 | fi 11 | echo "tmux has stopped." -------------------------------------------------------------------------------- /pkg/gocron/gocron.go: -------------------------------------------------------------------------------- 1 | package gocron 2 | 3 | import ( 4 | "github.com/go-co-op/gocron" 5 | "time" 6 | ) 7 | 8 | // Schedule 定时任务 gocron 9 | type Schedule struct { 10 | *gocron.Scheduler 11 | } 12 | 13 | func NewSchedule() *gocron.Scheduler { 14 | return gocron.NewScheduler(time.Local) 15 | } 16 | -------------------------------------------------------------------------------- /internal/response/message.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | import ( 4 | "github.com/bytedance-youthcamp-jbzx/tiktok/kitex/kitex_gen/message" 5 | ) 6 | 7 | type MessageChat struct { 8 | Base 9 | MessageList []*message.Message `json:"message_list"` 10 | } 11 | 12 | type MessageAction struct { 13 | Base 14 | } 15 | -------------------------------------------------------------------------------- /config/video.yml: -------------------------------------------------------------------------------- 1 | server: 2 | name: "TiktokFeedServer" 3 | host: 0.0.0.0 4 | port: 8086 5 | 6 | rpc: 7 | host: 127.0.0.1 8 | port: 50056 9 | 10 | JWT: 11 | signingKey: "signingKey" 12 | 13 | video: 14 | maxSizeLimit: 50 15 | 16 | etcd: 17 | host: 0.0.0.0 18 | port: 2379 19 | password: 20 | salt: "jbzx" -------------------------------------------------------------------------------- /config/rabbitmq.yml: -------------------------------------------------------------------------------- 1 | server: 2 | name: "TikTokRabbitMQServer" 3 | host: 0.0.0.0 4 | port: 5672 5 | username: tiktokRMQ 6 | password: tiktokRMQ 7 | vhost: tiktokRMQ 8 | 9 | consumer: 10 | favorite: 11 | autoAck: true 12 | prefetchCount: 1000 13 | relation: 14 | autoAck: true 15 | prefetchCount: 1000 -------------------------------------------------------------------------------- /cmd/user/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | RUN_NAME="usersrv" 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 | -------------------------------------------------------------------------------- /cmd/video/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | RUN_NAME="videosrv" 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 | -------------------------------------------------------------------------------- /cmd/comment/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | RUN_NAME="commentsrv" 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 | -------------------------------------------------------------------------------- /cmd/favorite/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | RUN_NAME="favoritesrv" 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 | -------------------------------------------------------------------------------- /cmd/message/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | RUN_NAME="messagesrv" 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 | -------------------------------------------------------------------------------- /cmd/relation/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | RUN_NAME="relationsrv" 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 | -------------------------------------------------------------------------------- /internal/response/comment.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | import ( 4 | "github.com/bytedance-youthcamp-jbzx/tiktok/kitex/kitex_gen/comment" 5 | ) 6 | 7 | type CommentAction struct { 8 | Base 9 | Comment *comment.Comment `json:"comment"` 10 | } 11 | 12 | type CommentList struct { 13 | Base 14 | CommentList []*comment.Comment `json:"comment_list"` 15 | } 16 | -------------------------------------------------------------------------------- /dockerfiles/rpc/message/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.19 AS builder 2 | 3 | LABEL stage=gobuilder 4 | 5 | ENV CGO_ENABLED 0 6 | ENV GOPROXY https://goproxy.cn,direct 7 | 8 | RUN mkdir -p /home/crypt/ 9 | COPY rsa_public_key.pem /home/crypt/ 10 | COPY rsa_private_key.pem /home/crypt/ 11 | 12 | WORKDIR /app 13 | ADD messagesrv . 14 | COPY config/ config/ 15 | EXPOSE 8083 16 | CMD ["./messagesrv"] -------------------------------------------------------------------------------- /scripts/minio.sh: -------------------------------------------------------------------------------- 1 | docker run -p 9000:9000 -p 9090:9090 \ 2 | --net=bridge \ 3 | --name minio \ 4 | -d --restart=always \ 5 | -e "MINIO_ACCESS_KEY=tiktokMinio" \ 6 | -e "MINIO_SECRET_KEY=tiktokMinio" \ 7 | -v /home/minio/data:/data \ 8 | -v /home/minio/config:/root/.minio \ 9 | minio/minio server \ 10 | /data --console-address ":9090" -address ":9000" 11 | -------------------------------------------------------------------------------- /config/minio.yml: -------------------------------------------------------------------------------- 1 | Global: 2 | Source: "config(local)" 3 | ChangeMe: "v3" 4 | 5 | Minio: 6 | Endpoint: 0.0.0.0:9000 7 | AccessKeyId: minio 8 | SecretAccessKey: minio 9 | UseSSL: false 10 | VideoBucketName: tiktok-videos 11 | CoverBucketName: tiktok-video-covers 12 | AvatarBucketName: tiktok-user-avatars 13 | BackgroundImageBucketName: tiktok-user-backgrounds 14 | ExpireTime: 3600 # 视频临时链接过期秒数 15 | -------------------------------------------------------------------------------- /internal/response/video.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | import ( 4 | "github.com/bytedance-youthcamp-jbzx/tiktok/kitex/kitex_gen/video" 5 | ) 6 | 7 | type PublishAction struct { 8 | Base 9 | } 10 | 11 | type PublishList struct { 12 | Base 13 | VideoList []*video.Video `json:"video_list"` 14 | } 15 | 16 | type Feed struct { 17 | Base 18 | NextTime int64 `json:"next_time"` 19 | VideoList []*video.Video `json:"video_list"` 20 | } 21 | -------------------------------------------------------------------------------- /internal/response/user.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | import ( 4 | "github.com/bytedance-youthcamp-jbzx/tiktok/kitex/kitex_gen/user" 5 | ) 6 | 7 | type Register struct { 8 | Base 9 | UserID int64 `json:"user_id"` 10 | Token string `json:"token"` 11 | } 12 | 13 | type Login struct { 14 | Base 15 | UserID int64 `json:"user_id"` 16 | Token string `json:"token"` 17 | } 18 | 19 | type UserInfo struct { 20 | Base 21 | User *user.User `json:"user"` 22 | } 23 | -------------------------------------------------------------------------------- /cmd/message/service/init.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/bytedance-youthcamp-jbzx/tiktok/internal/tool" 5 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/jwt" 6 | ) 7 | 8 | var ( 9 | Jwt *jwt.JWT 10 | publicKey string 11 | privateKey string 12 | ) 13 | 14 | func Init(signingKey string) { 15 | Jwt = jwt.NewJWT([]byte(signingKey)) 16 | publicKey, _ = tool.ReadKeyFromFile(tool.PublicKeyFilePath) 17 | privateKey, _ = tool.ReadKeyFromFile(tool.PrivateKeyFilePath) 18 | } 19 | -------------------------------------------------------------------------------- /cmd/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/usersrv" 22 | 23 | -------------------------------------------------------------------------------- /cmd/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/videosrv" 22 | 23 | -------------------------------------------------------------------------------- /cmd/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/commentsrv" 22 | 23 | -------------------------------------------------------------------------------- /cmd/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/favoritesrv" 22 | 23 | -------------------------------------------------------------------------------- /cmd/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/messagesrv" 22 | 23 | -------------------------------------------------------------------------------- /cmd/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/relationsrv" 22 | 23 | -------------------------------------------------------------------------------- /config/api.yml: -------------------------------------------------------------------------------- 1 | server: 2 | name: "TiktokAPIServer" 3 | host: 0.0.0.0 4 | port: 8089 5 | limit: 6 | capacity: 200 7 | rate: 20 8 | tokenInit: 128 # 初始令牌个数 9 | 10 | JWT: 11 | signingKey: "signingKey" 12 | 13 | Etcd: 14 | enable: true 15 | host: 0.0.0.0 16 | port: 2379 17 | 18 | Hertz: 19 | useNetPoll: false 20 | tls: 21 | enable: false 22 | keyFile: "" 23 | certFile: "" 24 | ALPN: true 25 | http2: 26 | enable: false 27 | keyFile: "" 28 | certFile: "" 29 | ALPN: true -------------------------------------------------------------------------------- /pkg/middleware/tls.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/unrolled/secure" 6 | ) 7 | 8 | func TLSSupportMiddleware(host string) gin.HandlerFunc { 9 | return func(c *gin.Context) { 10 | secureMiddleware := secure.New(secure.Options{ 11 | SSLRedirect: true, 12 | SSLHost: host, 13 | }) 14 | 15 | err := secureMiddleware.Process(c.Writer, c.Request) 16 | 17 | // If there was an error, do not continue. 18 | if err != nil { 19 | return 20 | } 21 | 22 | c.Next() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pkg/gocron/gocron_test.go: -------------------------------------------------------------------------------- 1 | package gocron_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/bytedance-youthcamp-jbzx/dousheng/pkg/gocron" 8 | ) 9 | 10 | func task() { 11 | println("hello world") 12 | ch := make(chan bool) 13 | go func() { 14 | time.Sleep(1 * time.Second) 15 | ch <- true 16 | }() 17 | <-ch 18 | println("waibi waibi") 19 | } 20 | func TestGocron(t *testing.T) { 21 | s := gocron.NewSchedule() 22 | s.Every(2).Second().Do(task) 23 | s.StartAsync() 24 | } 25 | 26 | func TestZuSe(t *testing.T) { 27 | task() 28 | } 29 | -------------------------------------------------------------------------------- /config/nginx/conf/conf.d/api_https_proxy.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 443; 3 | server_name 127.0.0.1; 4 | ssl on; 5 | ssl_certificate /usr/share/nginx/server.crt; 6 | ssl_certificate_key /usr/share/nginx/rsa_private_key.pem; 7 | location / { 8 | proxy_set_header Host $host; 9 | proxy_set_header X-Real-IP $remote_addr; 10 | proxy_set_header X-Forwarded-For $remote_addr; 11 | proxy_pass http://127.0.0.1:8080$request_uri; 12 | root /usr/share/nginx/html; 13 | index index.html index.htm; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /internal/response/relation.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | import ( 4 | "github.com/bytedance-youthcamp-jbzx/tiktok/kitex/kitex_gen/relation" 5 | "github.com/bytedance-youthcamp-jbzx/tiktok/kitex/kitex_gen/user" 6 | ) 7 | 8 | type RelationAction struct { 9 | Base 10 | } 11 | 12 | type FollowerList struct { 13 | Base 14 | UserList []*user.User `json:"user_list"` 15 | } 16 | 17 | type FollowList struct { 18 | Base 19 | UserList []*user.User `json:"user_list"` 20 | } 21 | 22 | type FriendList struct { 23 | Base 24 | UserList []*relation.FriendUser `json:"user_list"` 25 | } 26 | -------------------------------------------------------------------------------- /kitex/kitex.sh: -------------------------------------------------------------------------------- 1 | kitex -module github.com/bytedance-youthcamp-jbzx/tiktok -I ./ -v -service usersrv user.proto 2 | 3 | kitex -module github.com/bytedance-youthcamp-jbzx/tiktok -I ./ -v -service commentsrv comment.proto 4 | 5 | kitex -module github.com/bytedance-youthcamp-jbzx/tiktok -I ./ -v -service relationsrv relation.proto 6 | 7 | kitex -module github.com/bytedance-youthcamp-jbzx/tiktok -I ./ -v -service favoritesrv favorite.proto 8 | 9 | kitex -module github.com/bytedance-youthcamp-jbzx/tiktok -I ./ -v -service messagesrv message.proto 10 | 11 | kitex -module github.com/bytedance-youthcamp-jbzx/tiktok -I ./ -v -service videosrv video.proto 12 | -------------------------------------------------------------------------------- /pkg/middleware/limit.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "context" 5 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/zap" 6 | "github.com/cloudwego/hertz/pkg/app" 7 | "net/http" 8 | ) 9 | 10 | // 限流中间件,使用令牌桶的方式处理请求。Note: auth中间件需在其前面 11 | func TokenLimitMiddleware() app.HandlerFunc { 12 | logger := zap.InitLogger() 13 | 14 | return func(ctx context.Context, c *app.RequestContext) { 15 | token := c.GetString("Token") 16 | 17 | if !CurrentLimiter.Allow(token) { 18 | responseWithError(ctx, c, http.StatusForbidden, "request too fast") 19 | logger.Errorln("403: Request too fast.") 20 | return 21 | } 22 | c.Next(ctx) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pkg/middleware/log.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "context" 5 | "github.com/cloudwego/hertz/pkg/app" 6 | "github.com/cloudwego/hertz/pkg/common/hlog" 7 | "time" 8 | ) 9 | 10 | func AccessLog() app.HandlerFunc { 11 | return func(c context.Context, ctx *app.RequestContext) { 12 | start := time.Now() 13 | ctx.Next(c) 14 | end := time.Now() 15 | latency := end.Sub(start).Microseconds 16 | hlog.CtxTracef(c, "status=%d cost=%d method=%s full_path=%s client_ip=%s host=%s", 17 | ctx.Response.StatusCode(), latency, 18 | ctx.Request.Header.Method(), ctx.Request.URI().PathOriginal(), ctx.ClientIP(), ctx.Request.Host()) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /kitex/kitex_gen/user/userservice/server.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.4.4. DO NOT EDIT. 2 | package userservice 3 | 4 | import ( 5 | user "github.com/bytedance-youthcamp-jbzx/tiktok/kitex/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/kitex_gen/video/videoservice/server.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.4.4. DO NOT EDIT. 2 | package videoservice 3 | 4 | import ( 5 | video "github.com/bytedance-youthcamp-jbzx/tiktok/kitex/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 | -------------------------------------------------------------------------------- /pkg/etcd/common.go: -------------------------------------------------------------------------------- 1 | package etcd 2 | 3 | const ( 4 | etcdPrefix = "kitex/registry-etcd" 5 | ) 6 | 7 | func serviceKeyPrefix(serviceName string) string { 8 | return etcdPrefix + "/" + serviceName 9 | } 10 | 11 | // serviceKey generates the key used to stored in etcd. 12 | func serviceKey(serviceName, addr string) string { 13 | return serviceKeyPrefix(serviceName) + "/" + addr 14 | } 15 | 16 | // instanceInfo used to stored service basic info in etcd. 17 | type instanceInfo struct { 18 | Network string `json:"network"` 19 | Address string `json:"address"` 20 | Weight int `json:"weight"` 21 | Tags map[string]string `json:"tags"` 22 | } 23 | -------------------------------------------------------------------------------- /kitex/kitex_gen/comment/commentservice/server.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.4.4. DO NOT EDIT. 2 | package commentservice 3 | 4 | import ( 5 | comment "github.com/bytedance-youthcamp-jbzx/tiktok/kitex/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/kitex_gen/message/messageservice/server.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.4.4. DO NOT EDIT. 2 | package messageservice 3 | 4 | import ( 5 | message "github.com/bytedance-youthcamp-jbzx/tiktok/kitex/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 | -------------------------------------------------------------------------------- /pkg/middleware/limit_test.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "sync" 5 | "sync/atomic" 6 | "testing" 7 | ) 8 | 9 | func TestLimit(t *testing.T) { 10 | // 启动测试服务器,安装限流中间件 11 | runTestServer(TokenLimitMiddleware()) 12 | 13 | token := getAuthToken("123", t) 14 | nums := 20 15 | var forbidden int32 16 | var worker sync.WaitGroup 17 | worker.Add(nums) 18 | 19 | for i := 0; i < nums; i++ { 20 | go func(t *testing.T) { 21 | code, _ := doAuth(token, t) 22 | if code == 403 { 23 | atomic.AddInt32(&forbidden, 1) 24 | } 25 | worker.Done() 26 | }(t) 27 | } 28 | 29 | worker.Wait() 30 | 31 | if forbidden == 0 { 32 | t.Fatalf("forbidden must > 0") 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /pkg/middleware/server.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "context" 5 | "github.com/cloudwego/kitex/pkg/endpoint" 6 | "github.com/cloudwego/kitex/pkg/rpcinfo" 7 | ) 8 | 9 | var ( 10 | _ endpoint.Middleware = ServerMiddleware 11 | ) 12 | 13 | // ServerMiddleware server middleware print client address 14 | func ServerMiddleware(next endpoint.Endpoint) endpoint.Endpoint { 15 | return func(ctx context.Context, req, resp interface{}) (err error) { 16 | ri := rpcinfo.GetRPCInfo(ctx) 17 | // get client information 18 | logger.Infof("client address: %v", ri.From().Address()) 19 | if err = next(ctx, req, resp); err != nil { 20 | return err 21 | } 22 | return nil 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /kitex/kitex_gen/favorite/favoriteservice/server.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.4.4. DO NOT EDIT. 2 | package favoriteservice 3 | 4 | import ( 5 | favorite "github.com/bytedance-youthcamp-jbzx/tiktok/kitex/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/kitex_gen/relation/relationservice/server.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.4.4. DO NOT EDIT. 2 | package relationservice 3 | 4 | import ( 5 | relation "github.com/bytedance-youthcamp-jbzx/tiktok/kitex/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 | -------------------------------------------------------------------------------- /cmd/favorite/service/init.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/jwt" 5 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/rabbitmq" 6 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/viper" 7 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/zap" 8 | ) 9 | 10 | var ( 11 | Jwt *jwt.JWT 12 | logger = zap.InitLogger() 13 | config = viper.Init("rabbitmq") 14 | autoAck = config.Viper.GetBool("consumer.favorite.autoAck") 15 | FavoriteMq = rabbitmq.NewRabbitMQSimple("favorite", autoAck) 16 | err error 17 | ) 18 | 19 | func Init(signingKey string) { 20 | Jwt = jwt.NewJWT([]byte(signingKey)) 21 | //GoCron() 22 | go consume() 23 | } 24 | -------------------------------------------------------------------------------- /cmd/api/rpc/init.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/viper" 4 | 5 | func init() { 6 | // comment rpc 7 | commentConfig := viper.Init("comment") 8 | InitComment(&commentConfig) 9 | 10 | // favorite rpc 11 | favoriteConfig := viper.Init("favorite") 12 | InitFavorite(&favoriteConfig) 13 | 14 | // message rpc 15 | messageConfig := viper.Init("message") 16 | InitMessage(&messageConfig) 17 | 18 | // relation rpc 19 | relationConfig := viper.Init("relation") 20 | InitRelation(&relationConfig) 21 | 22 | // user rpc 23 | userConfig := viper.Init("user") 24 | InitUser(&userConfig) 25 | 26 | // video rpc 27 | videoConfig := viper.Init("video") 28 | InitVideo(&videoConfig) 29 | } 30 | -------------------------------------------------------------------------------- /config/db.yml: -------------------------------------------------------------------------------- 1 | mysql: 2 | source: # 主数据库 3 | driverName: mysql 4 | host: 127.0.0.1 5 | port: 3309 6 | database: db_tiktok 7 | username: tiktokDB 8 | password: tiktokDB 9 | charset: utf8mb4 10 | 11 | replica1: # 从数据库 12 | driverName: mysql 13 | host: 127.0.0.1 14 | port: 3310 15 | database: db_tiktok 16 | username: tiktokDB 17 | password: tiktokDB 18 | charset: utf8mb4 19 | replica2: # 从数据库 20 | driverName: mysql 21 | host: 127.0.0.1 22 | port: 3308 23 | database: db_tiktok 24 | username: tiktokDB 25 | password: tiktokDB 26 | charset: utf8mb4 27 | 28 | redis: 29 | addr: 127.0.0.1 30 | port: 6379 31 | password: tiktokRedis 32 | db: 0 # 数据库编号 -------------------------------------------------------------------------------- /kitex/kitex_gen/user/userservice/invoker.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.4.4. DO NOT EDIT. 2 | 3 | package userservice 4 | 5 | import ( 6 | user "github.com/bytedance-youthcamp-jbzx/tiktok/kitex/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 | -------------------------------------------------------------------------------- /kitex/kitex_gen/video/videoservice/invoker.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.4.4. DO NOT EDIT. 2 | 3 | package videoservice 4 | 5 | import ( 6 | video "github.com/bytedance-youthcamp-jbzx/tiktok/kitex/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 | -------------------------------------------------------------------------------- /pkg/viper/viper.go: -------------------------------------------------------------------------------- 1 | package viper 2 | 3 | import ( 4 | "log" 5 | 6 | V "github.com/spf13/viper" 7 | ) 8 | 9 | // Config 公有变量,获取Viper 10 | type Config struct { 11 | Viper *V.Viper 12 | } 13 | 14 | // Init 初始化Viper配置 15 | func Init(configName string) Config { 16 | config := Config{Viper: V.New()} 17 | v := config.Viper 18 | v.SetConfigType("yml") //设置配置文件类型 19 | v.SetConfigName(configName) //设置配置文件名 20 | v.AddConfigPath("./config") //设置配置文件路径 !!!注意路径问题 21 | v.AddConfigPath("../config") 22 | v.AddConfigPath("../../config") 23 | //读取配置文件 24 | if err := v.ReadInConfig(); err != nil { 25 | //global.SugarLogger.Fatalf("read config files failed,errors is %+v", err) 26 | log.Fatalf("errno is %+v", err) 27 | } 28 | return config 29 | } 30 | -------------------------------------------------------------------------------- /kitex/kitex_gen/comment/commentservice/invoker.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.4.4. DO NOT EDIT. 2 | 3 | package commentservice 4 | 5 | import ( 6 | comment "github.com/bytedance-youthcamp-jbzx/tiktok/kitex/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/kitex_gen/message/messageservice/invoker.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.4.4. DO NOT EDIT. 2 | 3 | package messageservice 4 | 5 | import ( 6 | message "github.com/bytedance-youthcamp-jbzx/tiktok/kitex/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/kitex_gen/favorite/favoriteservice/invoker.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.4.4. DO NOT EDIT. 2 | 3 | package favoriteservice 4 | 5 | import ( 6 | favorite "github.com/bytedance-youthcamp-jbzx/tiktok/kitex/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/kitex_gen/relation/relationservice/invoker.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.4.4. DO NOT EDIT. 2 | 3 | package relationservice 4 | 5 | import ( 6 | relation "github.com/bytedance-youthcamp-jbzx/tiktok/kitex/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 | -------------------------------------------------------------------------------- /config/nginx/conf/nginx.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | worker_processes auto; 3 | 4 | error_log /var/log/nginx/error.log notice; 5 | pid /var/run/nginx.pid; 6 | 7 | 8 | events { 9 | worker_connections 1024; 10 | } 11 | 12 | 13 | http { 14 | include /etc/nginx/mime.types; 15 | default_type application/octet-stream; 16 | 17 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 18 | '$status $body_bytes_sent "$http_referer" ' 19 | '"$http_user_agent" "$http_x_forwarded_for"'; 20 | 21 | access_log /var/log/nginx/access.log main; 22 | 23 | sendfile on; 24 | #tcp_nopush on; 25 | 26 | keepalive_timeout 65; 27 | 28 | #gzip on; 29 | 30 | include /etc/nginx/conf.d/*.conf; 31 | } 32 | -------------------------------------------------------------------------------- /dockerfiles/api/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.19 AS builder 2 | 3 | LABEL stage=gobuilder 4 | 5 | ENV CGO_ENABLED 0 6 | ENV GOPROXY https://goproxy.cn,direct 7 | 8 | WORKDIR /build 9 | 10 | ADD go.mod . 11 | ADD go.sum . 12 | RUN go mod download 13 | COPY . . 14 | RUN go build -ldflags="-s -w" -o /app/main cmd/api/main.go 15 | 16 | 17 | FROM scratch 18 | 19 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt 20 | COPY --from=builder /usr/share/zoneinfo/Asia/Shanghai /usr/share/zoneinfo/Asia/Shanghai 21 | ENV TZ Asia/Shanghai 22 | 23 | WORKDIR /app 24 | COPY --from=builder /app/main /app/main 25 | # 复制配置文件,证书配置也应该在根目录 26 | COPY ./config/ /app/config/ 27 | # 保证证书在项目根目录 28 | ADD server.crt . 29 | # 保证密钥在项目根目录 30 | ADD rsa_private_key.pem . 31 | EXPOSE 8089 32 | CMD ["./main"] -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | # Default ignored files 18 | /shelf/ 19 | /workspace.xml 20 | # Editor-based HTTP Client requests 21 | /httpRequests/ 22 | # Datasource local storage ignored files 23 | /dataSources/ 24 | /dataSources.local.xml 25 | 26 | 27 | /.idea/ 28 | #/idl/ 29 | /logs/ 30 | /.vscode/ 31 | # /config/db.yml 32 | /scripts/microservice/logs/ 33 | /cmd/*/logs/ 34 | /pkg/*/logs/ 35 | /dal/db/logs/ 36 | /scripts/mysql.md 37 | /*/*/logs 38 | /cmd/api/config 39 | /.DS_Store 40 | /cmd/.DS_Store -------------------------------------------------------------------------------- /pkg/async/async.go: -------------------------------------------------------------------------------- 1 | package async 2 | 3 | import "context" 4 | 5 | // Future interface has the method signature for await 6 | 7 | type Future interface { 8 | Await() interface{} 9 | } 10 | 11 | type future struct { 12 | await func(ctx context.Context) interface{} 13 | } 14 | 15 | func (f future) Await() interface{} { 16 | return f.await(context.Background()) 17 | } 18 | 19 | // Exec executes the async function 20 | func Exec(f func() interface{}) Future { 21 | var result interface{} 22 | c := make(chan struct{}) 23 | 24 | go func() { 25 | defer close(c) 26 | result = f() 27 | }() 28 | 29 | return future{ 30 | await: func(ctx context.Context) interface{} { 31 | select { 32 | case <-ctx.Done(): 33 | return ctx.Err() 34 | case <-c: 35 | return result 36 | } 37 | }, 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /pkg/rabbitmq/rabbitmq_test.go: -------------------------------------------------------------------------------- 1 | package rabbitmq 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "runtime/debug" 7 | "strconv" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | func ExpectEqual(left interface{}, right interface{}, t *testing.T) { 13 | if left != right { 14 | t.Fatalf("expected %v == %v\n%s", left, right, debug.Stack()) 15 | } 16 | } 17 | 18 | func ExpectUnEqual(left interface{}, right interface{}, t *testing.T) { 19 | if left == right { 20 | t.Fatalf("expected %v != %v\n%s", left, right, debug.Stack()) 21 | } 22 | } 23 | 24 | func TestPublish(t *testing.T) { 25 | ctx := context.Background() 26 | rabbitmq := NewRabbitMQSimple("newProduct") 27 | for i := 0; i < 20; i++ { 28 | rabbitmq.PublishSimple(ctx, []byte("订阅模式生产第"+strconv.Itoa(i)+"条"+"数据")) 29 | fmt.Println("订阅模式生产第" + strconv.Itoa(i) + "条" + "数据") 30 | time.Sleep(1 * time.Second) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /cmd/relation/service/init.go: -------------------------------------------------------------------------------- 1 | // Package service /* 2 | package service 3 | 4 | import ( 5 | "github.com/bytedance-youthcamp-jbzx/tiktok/internal/tool" 6 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/jwt" 7 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/rabbitmq" 8 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/viper" 9 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/zap" 10 | ) 11 | 12 | var ( 13 | Jwt *jwt.JWT 14 | logger = zap.InitLogger() 15 | config = viper.Init("rabbitmq") 16 | autoAck = config.Viper.GetBool("consumer.relation.autoAck") 17 | RelationMq = rabbitmq.NewRabbitMQSimple("relation", autoAck) 18 | err error 19 | privateKey string 20 | ) 21 | 22 | func Init(signingKey string) { 23 | Jwt = jwt.NewJWT([]byte(signingKey)) 24 | privateKey, _ = tool.ReadKeyFromFile(tool.PrivateKeyFilePath) 25 | //GoCron() 26 | go consume() 27 | } 28 | -------------------------------------------------------------------------------- /scripts/nginx.sh: -------------------------------------------------------------------------------- 1 | # 拉取镜像 2 | docker pull nginx 3 | 4 | # 创建挂载目录 5 | mkdir -p /home/nginx/conf 6 | mkdir -p /home/nginx/log 7 | mkdir -p /home/nginx/html 8 | 9 | # 生成容器 10 | docker run --name nginx -p 80:80 -d nginx 11 | # 将容器nginx.conf文件复制到宿主机 12 | docker cp nginx:/etc/nginx/nginx.conf /home/nginx/conf/nginx.conf 13 | # 将容器conf.d文件夹下内容复制到宿主机 14 | docker cp nginx:/etc/nginx/conf.d /home/nginx/conf/conf.d 15 | # 将容器中的html文件夹复制到宿主机 16 | docker cp nginx:/usr/share/nginx/html /home/nginx/ 17 | 18 | # 关闭临时容器 19 | docker stop nginx 20 | 21 | # 删除临时容器 22 | docker rm nginx 23 | 24 | # 启动容器 25 | docker run \ 26 | -p 80:80 -p 443:443 \ 27 | --name nginx \ 28 | -v /home/nginx/conf/nginx.conf:/etc/nginx/nginx.conf \ 29 | -v /home/nginx/conf/conf.d:/etc/nginx/conf.d \ 30 | -v /home/nginx/log:/var/log/nginx \ 31 | -v /home/nginx/html:/usr/share/nginx/html \ 32 | -v /home/nginx/certs:/usr/share/nginx/ \ 33 | -d nginx:latest -------------------------------------------------------------------------------- /pkg/middleware/client.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "context" 5 | "github.com/cloudwego/kitex/pkg/endpoint" 6 | "github.com/cloudwego/kitex/pkg/rpcinfo" 7 | ) 8 | 9 | // 让编译器去检查ClientMiddleware和endpoint.Middleware是不是相同类型,如果不是则会报错 10 | var _ endpoint.Middleware = ClientMiddleware 11 | 12 | // ClientMiddleware client middleware print server address 、rpc timeout and connection timeout 13 | // 相当于对Endpoint进行包装,在调用前输出一些信息 14 | func ClientMiddleware(next endpoint.Endpoint) endpoint.Endpoint { 15 | return func(ctx context.Context, req, resp interface{}) (err error) { 16 | ri := rpcinfo.GetRPCInfo(ctx) 17 | // get server information 18 | logger.Infof("server address: %v, rpc timeout: %v, readwrite timeout: %v", ri.To().Address(), ri.Config().RPCTimeout(), ri.Config().ConnectTimeout()) 19 | if err = next(ctx, req, resp); err != nil { 20 | return err 21 | } 22 | return nil 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pkg/rabbitmq/init.go: -------------------------------------------------------------------------------- 1 | package rabbitmq 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/viper" 7 | z "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/zap" 8 | amqp "github.com/rabbitmq/amqp091-go" 9 | "go.uber.org/zap" 10 | ) 11 | 12 | var ( 13 | config = viper.Init("rabbitmq") 14 | logger *zap.SugaredLogger 15 | conn *amqp.Connection 16 | err error 17 | MqUrl = fmt.Sprintf("amqp://%s:%s@%s:%d/%v", 18 | config.Viper.GetString("server.username"), 19 | config.Viper.GetString("server.password"), 20 | config.Viper.GetString("server.host"), 21 | config.Viper.GetInt("server.port"), 22 | config.Viper.GetString("server.vhost"), 23 | ) 24 | ) 25 | 26 | func init() { 27 | logger = z.InitLogger() 28 | } 29 | 30 | func failOnError(err error, msg string) { 31 | if err != nil { 32 | logger.Errorf("%s: %s", msg, err.Error()) 33 | panic(fmt.Sprintf("%s: %s", msg, err.Error())) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /pkg/middleware/auth_test.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | sjwt "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/jwt" 8 | ) 9 | 10 | func TestAuth(t *testing.T) { 11 | // 启动测试服务器,安装token鉴权中间件 12 | signKey := []byte{0x12, 0x34, 0x56, 0x78, 0x9a} 13 | userJwt := sjwt.NewJWT(signKey) 14 | runTestServer(TokenAuthMiddleware(*userJwt, "/login")) 15 | // 不存在的token 16 | token := "aaabbcccdd" 17 | _, statusCode := doAuth(token, t) 18 | if statusCode != -1 { 19 | t.Fatalf("expected %d but got %d", -1, statusCode) 20 | } 21 | 22 | // 在有效期里的token 23 | token = getAuthToken("123", t) 24 | _, statusCode = doAuth(token, t) 25 | if statusCode != 0 { 26 | t.Fatalf("expected %d but got %d", 0, statusCode) 27 | } 28 | 29 | // token过期 30 | time.Sleep(8 * time.Second) 31 | _, statusCode = doAuth(token, t) 32 | if statusCode != -1 { 33 | t.Fatalf("expected %d but got %d", -1, statusCode) 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /pkg/middleware/tls_test.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | "os" 6 | "testing" 7 | "time" 8 | 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | func TestTLSServer(t *testing.T) { 13 | sync := make(chan int, 1) 14 | tlsKey := os.Getenv("tiktok_tls_key") 15 | tlsCert := os.Getenv("tiktok_tls_cert") 16 | 17 | if len(tlsCert) == 0 || len(tlsKey) == 0 { 18 | 19 | t.Fatalf("key or cert not found in environment") 20 | } 21 | 22 | go func(t *testing.T) { 23 | r := gin.Default() 24 | 25 | r.GET("/", func(c *gin.Context) { 26 | c.JSON(200, gin.H{ 27 | "status_code": 1, 28 | }) 29 | }) 30 | 31 | r.Use(TLSSupportMiddleware("1.12.68.184:4001")) 32 | 33 | sync <- 1 34 | r.RunTLS("1.12.68.184:4001", tlsCert, tlsKey) 35 | }(t) 36 | 37 | <-sync 38 | time.Sleep(time.Second * 2) 39 | _, err := http.Get("https://1.12.68.184:4001/") 40 | 41 | if err != nil { 42 | t.Fatalf("https request error: %v", err) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /dal/redis/message.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strconv" 7 | "time" 8 | ) 9 | 10 | /* 11 | 轮询获取message需要redis记录前一次获取信息的最后一条消息的时间戳,键是用户的令牌, 12 | 值是上次消息的最后条消息的时间戳,规定键的过期时间为两秒。每次轮询的请求都需要去更新 13 | redis里的键值,即便没有新的消息传来。 14 | */ 15 | 16 | func GetMessageTimestamp(ctx context.Context, token string, toUserID int64) (int, error) { 17 | key := fmt.Sprintf("%s_%d", token, toUserID) 18 | if ec, err := GetRedisHelper().Exists(ctx, key).Result(); err != nil { 19 | return -1, err 20 | } else if ec == 0 { 21 | return -1, nil //errors.New("key not found") 22 | } 23 | 24 | val, err := GetRedisHelper().Get(ctx, key).Result() 25 | if err != nil { 26 | return -1, err 27 | } 28 | 29 | return strconv.Atoi(val) 30 | } 31 | 32 | func SetMessageTimestamp(ctx context.Context, token string, toUserID int64, timestamp int) error { 33 | key := fmt.Sprintf("%s_%d", token, toUserID) 34 | return GetRedisHelper().Set(ctx, key, timestamp, 2*time.Second).Err() 35 | } 36 | -------------------------------------------------------------------------------- /pkg/jwt/jwt_test.go: -------------------------------------------------------------------------------- 1 | package jwt 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/golang-jwt/jwt" 8 | ) 9 | 10 | func TestJWT(t *testing.T) { 11 | userJwt := NewJWT([]byte{0x12, 0x32, 0x4a, 0x53, 0x59, 0x45}) 12 | 13 | token, err := userJwt.CreateToken(CustomClaims{ 14 | 1234, 15 | jwt.StandardClaims{ 16 | ExpiresAt: time.Now().Add(time.Second * 5).Unix(), 17 | Issuer: "dousheng", 18 | }, 19 | }) 20 | 21 | if err != nil { 22 | t.Fatalf("create token error %v", err) 23 | } 24 | 25 | _, err = userJwt.ParseToken(token) 26 | 27 | if err != nil { 28 | t.Fatalf("token verified error %v", err) 29 | } 30 | 31 | otherJwt := NewJWT([]byte{0x12, 0x32, 0x4a, 0x53, 0x59, 0x45}) 32 | _, err = otherJwt.ParseToken(token) 33 | 34 | if err != nil { 35 | t.Fatalf("token verified error %v", err) 36 | } 37 | 38 | time.Sleep(time.Second * 7) 39 | 40 | _, err = userJwt.ParseToken(token) 41 | 42 | if err == nil { 43 | t.Fatalf("token expired but not got error") 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /startup.sh: -------------------------------------------------------------------------------- 1 | session_name=dousheng 2 | 3 | tmux has-session -t $session_name 4 | if [ $? != 0 ];then 5 | path_to_scrpit=scripts/microservice 6 | cd $path_to_scrpit 7 | 8 | tmux new-session -s $session_name -n comment -d 9 | tmux send-keys -t $session_name 'sh comment.sh' C-m 10 | tmux new-window -n favorite -t $session_name 11 | tmux send-keys -t $session_name:1 'sh favorite.sh' C-m 12 | tmux new-window -n message -t $session_name 13 | tmux send-keys -t $session_name:2 'sh message.sh' C-m 14 | tmux new-window -n relation -t $session_name 15 | tmux send-keys -t $session_name:3 'sh relation.sh' C-m 16 | tmux new-window -n user -t $session_name 17 | tmux send-keys -t $session_name:4 'sh user.sh' C-m 18 | tmux new-window -n video -t $session_name 19 | tmux send-keys -t $session_name:5 'sh video.sh' C-m 20 | tmux new-window -n api -t $session_name 21 | tmux send-keys -t $session_name:6 'sh api.sh' C-m 22 | tmux select-window -t $session_name:6 23 | fi 24 | tmux attach -t dousheng 25 | echo "tmux has started." -------------------------------------------------------------------------------- /kitex/favorite.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | option go_package = "favorite"; 3 | package favorite; 4 | 5 | import "video.proto"; 6 | // ===========================点赞or取消点赞==================================== 7 | message FavoriteActionRequest { 8 | int64 user_id = 1; // 用户id 9 | string token = 2; // 用户鉴权token 10 | int64 video_id = 3; // 视频id 11 | int32 action_type = 4; // 1-点赞,2-取消点赞 12 | } 13 | message FavoriteActionResponse { 14 | int32 status_code = 1; // 状态码,0-成功,其他值-失败 15 | string status_msg = 2; // 返回状态描述 16 | } 17 | 18 | // ==============================点赞列表======================================= 19 | message FavoriteListRequest { 20 | int64 user_id = 1; // 用户id 21 | string token = 2; // 用户鉴权token 22 | } 23 | message FavoriteListResponse { 24 | int32 status_code = 1; // 状态码,0-成功,其他值-失败 25 | string status_msg = 2; // 返回状态描述 26 | repeated video.Video video_list = 3; // 用户点赞视频列表 27 | } 28 | 29 | service FavoriteService { 30 | rpc FavoriteAction (FavoriteActionRequest) returns (FavoriteActionResponse); 31 | rpc FavoriteList (FavoriteListRequest) returns (FavoriteListResponse); 32 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 bytedance-youthcamp-jbzx 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /pkg/rabbitmq/emit_logs.go: -------------------------------------------------------------------------------- 1 | package rabbitmq 2 | 3 | import ( 4 | "context" 5 | amqp "github.com/rabbitmq/amqp091-go" 6 | "os" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | // EmitLogs 发布/订阅:生产者 12 | func EmitLogs() { 13 | ch, err := conn.Channel() 14 | failOnError(err, "Failed to open a channel") 15 | defer ch.Close() 16 | 17 | err = ch.ExchangeDeclare( 18 | "logs", // name 19 | "fanout", // type 20 | true, // durable 21 | false, // auto-deleted 22 | false, // internal 23 | false, // no-wait 24 | nil, // arguments 25 | ) 26 | failOnError(err, "Failed to declare an exchange") 27 | 28 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 29 | defer cancel() 30 | 31 | body := bodyFrom(os.Args) 32 | err = ch.PublishWithContext(ctx, 33 | "logs", // exchange 34 | "", // routing key 35 | false, // mandatory 36 | false, // immediate 37 | amqp.Publishing{ 38 | ContentType: "text/plain", 39 | Body: []byte(body), 40 | }) 41 | failOnError(err, "Failed to publish a message") 42 | 43 | logger.Infof(" [x] Sent %s", body) 44 | } 45 | 46 | func bodyFrom(args []string) string { 47 | var s string 48 | if (len(args) < 2) || os.Args[1] == "" { 49 | s = "hello" 50 | } else { 51 | s = strings.Join(args[1:], " ") 52 | } 53 | return s 54 | } 55 | -------------------------------------------------------------------------------- /kitex/message.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | option go_package = "message"; 3 | package message; 4 | 5 | // ====================================聊天记录=================================== 6 | message Message{ 7 | int64 id = 1; // 消息id 8 | int64 to_user_id = 2; // 该消息接收者的id 9 | int64 from_user_id = 3; // 该消息发送者的id 10 | string content = 4; // 消息内容 11 | int64 create_time = 5; // 消息创建时间 12 | } 13 | message MessageChatRequest{ 14 | string token = 1; // 用户鉴权token 15 | int64 to_user_id = 2; // 对方用户id 16 | int64 pre_msg_time = 3; // 上次最新消息时间 17 | } 18 | message MessageChatResponse{ 19 | int32 status_code = 1; // 状态码,0成功,其他值失败 20 | string status_msg = 2; // 返回状态描述 21 | repeated Message message_list = 3; // 消息列表 22 | } 23 | 24 | // =====================================消息发送=================================== 25 | message MessageActionRequest{ 26 | string token = 1; //用户鉴权token 27 | int64 to_user_id = 2; //对方用户id 28 | int32 action_type = 3; //1-发送消息 29 | string content = 4; 30 | } 31 | message MessageActionResponse{ 32 | int32 status_code = 1; // 状态码,0成功,其他值失败 33 | string status_msg = 2; // 返回状态描述 34 | } 35 | 36 | service MessageService{ 37 | rpc MessageChat (MessageChatRequest) returns (MessageChatResponse); 38 | rpc MessageAction (MessageActionRequest) returns (MessageActionResponse); 39 | } -------------------------------------------------------------------------------- /pkg/middleware/common.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "context" 5 | z "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/zap" 6 | "github.com/cloudwego/hertz/pkg/app" 7 | "github.com/cloudwego/kitex/pkg/endpoint" 8 | "github.com/cloudwego/kitex/pkg/rpcinfo" 9 | "github.com/gin-gonic/gin" 10 | "go.uber.org/zap" 11 | ) 12 | 13 | var ( 14 | _ endpoint.Middleware = CommonMiddleware 15 | logger *zap.SugaredLogger 16 | ) 17 | 18 | func init() { 19 | logger = z.InitLogger() 20 | defer logger.Sync() 21 | 22 | //klog.SetLogger(logger) 23 | } 24 | 25 | func responseWithError(ctx context.Context, c *app.RequestContext, code int, message interface{}) { 26 | c.AbortWithStatusJSON(code, gin.H{ 27 | "status_code": -1, // 业务码 400x错误,建议细化 28 | "status_msg": message, 29 | }) 30 | } 31 | 32 | func CommonMiddleware(next endpoint.Endpoint) endpoint.Endpoint { 33 | return func(ctx context.Context, req, resp interface{}) (err error) { 34 | ri := rpcinfo.GetRPCInfo(ctx) 35 | // get real request 36 | logger.Debugf("real request: %+v", req) 37 | // get remote service information 38 | logger.Debugf("remote service name: %s, remote method: %s", ri.To().ServiceName(), ri.To().Method()) 39 | if err := next(ctx, req, resp); err != nil { 40 | return err 41 | } 42 | // get real response 43 | logger.Infof("real response: %+v", resp) 44 | return nil 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /pkg/rabbitmq/receive_logs.go: -------------------------------------------------------------------------------- 1 | package rabbitmq 2 | 3 | // ReceiveLogs 发布/订阅:消费者 4 | func ReceiveLogs() { 5 | ch, err := conn.Channel() 6 | failOnError(err, "Failed to open a channel") 7 | defer ch.Close() 8 | 9 | err = ch.ExchangeDeclare( 10 | "logs", // name 11 | "fanout", // type 12 | true, // durable 13 | false, // auto-deleted 14 | false, // internal 15 | false, // no-wait 16 | nil, // arguments 17 | ) 18 | failOnError(err, "Failed to declare an exchange") 19 | 20 | q, err := ch.QueueDeclare( 21 | "", // name 22 | false, // durable 23 | false, // delete when unused 24 | true, // exclusive 25 | false, // no-wait 26 | nil, // arguments 27 | ) 28 | failOnError(err, "Failed to declare a queue") 29 | 30 | err = ch.QueueBind( 31 | q.Name, // queue name 32 | "", // routing key 33 | "logs", // exchange 34 | false, 35 | nil) 36 | failOnError(err, "Failed to bind a queue") 37 | 38 | msgs, err := ch.Consume( 39 | q.Name, // queue 40 | "", // consumer 41 | true, // auto-ack 42 | false, // exclusive 43 | false, // no-local 44 | false, // no-wait 45 | nil, // args 46 | ) 47 | failOnError(err, "Failed to register a consumer") 48 | 49 | var forever chan struct{} 50 | 51 | go func() { 52 | for d := range msgs { 53 | logger.Infof(" [x] %s", d.Body) 54 | } 55 | }() 56 | 57 | logger.Infof(" [*] Waiting for logs. To exit press CTRL+C") 58 | <-forever 59 | } 60 | -------------------------------------------------------------------------------- /pkg/minio/init.go: -------------------------------------------------------------------------------- 1 | package minio 2 | 3 | import ( 4 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/viper" 5 | 6 | "github.com/minio/minio-go/v7" 7 | "github.com/minio/minio-go/v7/pkg/credentials" 8 | ) 9 | 10 | var ( 11 | minioClient *minio.Client 12 | minioConfig = viper.Init("minio") 13 | MinioEndPoint = minioConfig.Viper.GetString("minio.Endpoint") 14 | MinioAccessKeyId = minioConfig.Viper.GetString("minio.AccessKeyId") 15 | MinioSecretAccessKey = minioConfig.Viper.GetString("minio.SecretAccessKey") 16 | UseSSL = minioConfig.Viper.GetBool("minio.UseSSL") 17 | VideoBucketName = minioConfig.Viper.GetString("minio.VideoBucketName") 18 | CoverBucketName = minioConfig.Viper.GetString("minio.CoverBucketName") 19 | AvatarBucketName = minioConfig.Viper.GetString("minio.AvatarBucketName") 20 | BackgroundImageBucketName = minioConfig.Viper.GetString("minio.BackgroundImageBucketName") 21 | ExpireTime = minioConfig.Viper.GetUint32("minio.ExpireTime") 22 | ) 23 | 24 | func init() { 25 | s3client, err := minio.New(MinioEndPoint, &minio.Options{ 26 | Creds: credentials.NewStaticV4(MinioAccessKeyId, MinioSecretAccessKey, ""), 27 | Secure: UseSSL, 28 | }) 29 | 30 | if err != nil { 31 | panic(err) 32 | } 33 | 34 | minioClient = s3client 35 | 36 | if err := CreateBucket(VideoBucketName); err != nil { 37 | panic(err) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /kitex/comment.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | option go_package = "comment"; 3 | package comment; 4 | 5 | import "user.proto"; 6 | // ===========================发布or删除评论================================== 7 | message CommentActionRequest { 8 | string token = 1; //用户鉴权token 9 | int64 video_id = 2; //评论的视频id 10 | int32 action_type = 3; //1-发布评论,2-删除评论 11 | string comment_text = 4; //用户填写的评论内容,action_type=1时使用 12 | int64 comment_id = 5; //要删除的评论id,action_type=2时使用 13 | } 14 | message CommentActionResponse { 15 | int32 status_code = 1; //状态码,0成功,其他值失败 16 | string status_msg = 2; //返回状态描述 17 | Comment comment = 3; //评论成功返回评论内容,不需要重新拉取整个评论列表 18 | } 19 | message Comment { 20 | int64 id = 1; // 评论的视频id 21 | user.User user = 2; // 评论用户信息 22 | string content = 3; // 评论内容 23 | string create_date = 4; // 评论发布日期,格式mm-dd 24 | int64 like_count = 5; // 该评论点赞数量 25 | int64 tease_count = 6; // 该评论点踩数量 26 | } 27 | 28 | // ==============================评论列表======================================== 29 | message CommentListRequest { 30 | string token = 1; 31 | int64 video_id = 2; 32 | } 33 | message CommentListResponse { 34 | int32 status_code = 1; 35 | string status_msg = 2; 36 | repeated Comment comment_list = 3; 37 | } 38 | 39 | service CommentService { 40 | rpc CommentAction(CommentActionRequest) returns(CommentActionResponse); 41 | rpc CommentList(CommentListRequest) returns(CommentListResponse); 42 | } -------------------------------------------------------------------------------- /config/nginx/conf/conf.d/api_http_proxy.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | listen [::]:80; 4 | server_name 127.0.0.1; 5 | 6 | access_log /var/log/nginx/host.access.log main; 7 | 8 | location / { 9 | proxy_set_header Host $host; 10 | proxy_set_header X-Real-IP $remote_addr; 11 | proxy_set_header X-Forwarded-For $remote_addr; 12 | proxy_pass http://127.0.0.1:8080$request_uri; 13 | root /usr/share/nginx/html; 14 | index index.html index.htm; 15 | } 16 | 17 | #error_page 404 /404.html; 18 | 19 | # redirect server error pages to the static page /50x.html 20 | # 21 | error_page 500 502 503 504 /50x.html; 22 | location = /50x.html { 23 | root /usr/share/nginx/html; 24 | } 25 | 26 | # proxy the PHP scripts to Apache listening on 127.0.0.1:80 27 | # 28 | #location ~ \.php$ { 29 | # proxy_pass http://127.0.0.1; 30 | #} 31 | 32 | # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 33 | # 34 | #location ~ \.php$ { 35 | # root html; 36 | # fastcgi_pass 127.0.0.1:9000; 37 | # fastcgi_index index.php; 38 | # fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name; 39 | # include fastcgi_params; 40 | #} 41 | 42 | # deny access to .htaccess files, if Apache's document root 43 | # concurs with nginx's one 44 | # 45 | #location ~ /\.ht { 46 | # deny all; 47 | #} 48 | } 49 | -------------------------------------------------------------------------------- /internal/tool/snapshot.go: -------------------------------------------------------------------------------- 1 | package tool 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/bytedance-youthcamp-jbzx/dousheng/pkg/zap" 7 | "github.com/disintegration/imaging" 8 | ffmpeg "github.com/u2takey/ffmpeg-go" 9 | "os" 10 | ) 11 | 12 | func GetSnapshot(videoPath, snapshotPath string, frameNum int) (ImagePath string, err error) { 13 | logger := zap.InitLogger() 14 | 15 | buf := bytes.NewBuffer(nil) 16 | err = ffmpeg.Input(videoPath).Filter("select", ffmpeg.Args{fmt.Sprintf("gte(n,%d)", frameNum)}). 17 | Output("pipe:", ffmpeg.KwArgs{"vframes": 1, "format": "image2", "vcodec": "mjpeg"}). 18 | WithOutput(buf, os.Stdout). 19 | Run() 20 | 21 | if err != nil { 22 | logger.Errorln("生成缩略图失败:", err) 23 | return "", err 24 | } 25 | 26 | img, err := imaging.Decode(buf) 27 | if err != nil { 28 | logger.Errorln("生成缩略图失败:", err) 29 | return "", err 30 | } 31 | 32 | err = imaging.Save(img, snapshotPath+".png") 33 | if err != nil { 34 | logger.Errorln("生成缩略图失败:", err) 35 | return "", err 36 | } 37 | 38 | imgPath := snapshotPath + ".png" 39 | 40 | return imgPath, nil 41 | } 42 | 43 | func GetSnapshotImageBuffer(videoPath string, frameNum int) (*bytes.Buffer, error) { 44 | logger := zap.InitLogger() 45 | 46 | buf := bytes.NewBuffer(nil) 47 | err := ffmpeg.Input(videoPath).Filter("select", ffmpeg.Args{fmt.Sprintf("gte(n,%d)", frameNum)}). 48 | Output("pipe:", ffmpeg.KwArgs{"vframes": 1, "format": "image2", "vcodec": "mjpeg"}). 49 | WithOutput(buf, os.Stdout). 50 | Run() 51 | 52 | if err != nil { 53 | logger.Errorln("生成缩略图失败:", err) 54 | return nil, err 55 | } 56 | return buf, nil 57 | } 58 | -------------------------------------------------------------------------------- /kitex/user.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | option go_package = "user"; 3 | package user; 4 | 5 | // =========================用户注册============================ 6 | message UserRegisterRequest { 7 | string username = 1; // 注册用户名,最长32个字符 8 | string password = 2; // 密码,最长32个字符 9 | } 10 | message UserRegisterResponse { 11 | int32 status_code = 1; 12 | string status_msg = 2; 13 | int64 user_id = 3; 14 | string token = 4; 15 | } 16 | 17 | // ==========================用户登录============================ 18 | message UserLoginRequest { 19 | string username = 1; 20 | string password = 2; 21 | } 22 | message UserLoginResponse { 23 | int32 status_code = 1; 24 | string status_msg = 2; 25 | int64 user_id = 3; 26 | string token = 4; 27 | } 28 | 29 | // ===========================用户信息=========================== 30 | message User { 31 | int64 id = 1; // 用户id 32 | string name = 2; // 用户名称 33 | int64 follow_count = 3; // 关注总数 34 | int64 follower_count = 4; // 粉丝总数 35 | bool is_follow = 5; // true-已关注,false-未关注 36 | string avatar = 6; // 用户头像 37 | string background_image = 7; // 用户个人页顶部大图 38 | string signature = 8; // 个人简介 39 | int64 total_favorited = 9; // 获赞数量 40 | int64 work_count = 10; // 作品数量 41 | int64 favorite_count = 11; // 点赞数量 42 | } 43 | message UserInfoRequest { 44 | int64 user_id = 1; 45 | string token = 2; 46 | } 47 | message UserInfoResponse { 48 | int32 status_code = 1; 49 | string status_msg = 2; 50 | User user = 3; 51 | } 52 | 53 | service UserService { 54 | rpc Register(UserRegisterRequest) returns (UserRegisterResponse){} 55 | rpc Login(UserLoginRequest) returns (UserLoginResponse){} 56 | rpc UserInfo(UserInfoRequest) returns (UserInfoResponse) {} 57 | } -------------------------------------------------------------------------------- /internal/tool/crypt_test.go: -------------------------------------------------------------------------------- 1 | package tool 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "testing" 8 | ) 9 | 10 | func TestSha256Encrypt(t *testing.T) { 11 | plaintext := "helloworld" 12 | salt := os.Getenv("tiktok_password_sha256_salt") 13 | 14 | if len(salt) == 0 { 15 | t.Fatalf("salt not found in environment") 16 | } 17 | 18 | fmt.Println(Sha256Encrypt(plaintext, salt)) 19 | } 20 | 21 | func readKeyFromFile(filePath string) (string, error) { 22 | file, err := os.Open(filePath) 23 | if err != nil { 24 | return "", err 25 | } 26 | 27 | data, err := ioutil.ReadAll(file) 28 | 29 | if err != nil { 30 | return "", err 31 | } 32 | 33 | return string(data), nil 34 | } 35 | func TestRsaCrypt(t *testing.T) { 36 | publicKeyFilePath := PublicKeyFilePath 37 | privateKeyFilePath := PrivateKeyFilePath 38 | 39 | if len(publicKeyFilePath) == 0 || len(privateKeyFilePath) == 0 { 40 | t.Fatalf("key path not found in environment") 41 | } 42 | 43 | publicKey, err := readKeyFromFile(publicKeyFilePath) 44 | 45 | if err != nil { 46 | t.Fatalf("read public key error: %v", err) 47 | } 48 | 49 | privateKey, err := readKeyFromFile(privateKeyFilePath) 50 | 51 | if err != nil { 52 | t.Fatalf("read private key error: %v", err) 53 | } 54 | 55 | plaintext := "helloworld" 56 | 57 | ciphertext, err := RsaEncrypt([]byte(plaintext), publicKey) 58 | 59 | if err != nil { 60 | t.Fatalf("rsa encrypt error: %v", err) 61 | } 62 | 63 | decrypted, err := RsaDecrypt(ciphertext, privateKey) 64 | 65 | if err != nil { 66 | t.Fatalf("rsa decrypt error: %v", err) 67 | } 68 | 69 | if string(decrypted) != plaintext { 70 | t.Fatal("decrypted text is inconsistent with the original") 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /pkg/middleware/auth.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "context" 5 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/jwt" 6 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/zap" 7 | "github.com/cloudwego/hertz/pkg/app" 8 | "net/http" 9 | "strings" 10 | ) 11 | 12 | func TokenAuthMiddleware(jwt jwt.JWT, skipRoutes ...string) app.HandlerFunc { 13 | logger := zap.InitLogger() 14 | // TODO: signKey可以保存在环境变量中,而不是硬编码在代码里,可以通过获取环境变量的方式获得signkey 15 | return func(ctx context.Context, c *app.RequestContext) { 16 | // 对于skip的路由不对他进行token鉴权 17 | for _, skipRoute := range skipRoutes { 18 | if skipRoute == c.FullPath() { 19 | c.Next(ctx) 20 | return 21 | } 22 | } 23 | 24 | // 从处理get post请求中获取token 25 | var token string 26 | if string(c.Request.Method()[:]) == "GET" { 27 | token = c.Query("token") 28 | } else if string(c.Request.Method()[:]) == "POST" { 29 | if strings.Contains(c.Request.Header.Get("Content-Type"), "multipart/form-data") { 30 | token = c.PostForm("token") 31 | } else { 32 | token = c.Query("token") 33 | } 34 | } else { 35 | // Unsupport request method 36 | responseWithError(ctx, c, http.StatusBadRequest, "bad request") 37 | logger.Errorln("bad request") 38 | return 39 | } 40 | if token == "" { 41 | responseWithError(ctx, c, http.StatusUnauthorized, "token required") 42 | logger.Errorln("token required") 43 | // 提前返回 44 | return 45 | } 46 | 47 | claim, err := jwt.ParseToken(token) 48 | 49 | if err != nil { 50 | responseWithError(ctx, c, http.StatusUnauthorized, err.Error()) 51 | logger.Errorln(err.Error()) 52 | return 53 | } 54 | 55 | // 在上下文中向下游传递token 56 | c.Set("Token", token) 57 | c.Set("Id", claim.Id) 58 | 59 | c.Next(ctx) // 交给下游中间件 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /kitex/video.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | option go_package = "video"; 3 | package video; 4 | 5 | import "user.proto"; 6 | // ============================feed视频流====================================== 7 | message Video { 8 | int64 id = 1; // 视频唯一标识 9 | user.User author = 2; // 视频作者信息 10 | string play_url = 3; // 视频播放地址 11 | string cover_url = 4; // 视频封面地址 12 | int64 favorite_count = 5; // 视频的点赞总数 13 | int64 comment_count = 6; // 视频的评论总数 14 | bool is_favorite = 7; // true-已点赞,false-未点赞 15 | string title = 8; // 视频标题 16 | int64 share_count = 9; // 转发数量-本次暂不涉及 17 | } 18 | message FeedRequest { 19 | int64 latest_time = 1; // 可选参数,限制返回视频的最新投稿时间戳,精确到秒,不填表示当前时间 20 | string token = 2; // 可选参数,登录用户设置 21 | } 22 | message FeedResponse { 23 | int32 status_code = 1; // 状态码,0-成功,其他值-失败 24 | string status_msg = 2; // 返回状态描述 25 | repeated Video video_list = 3; // 视频列表 26 | int64 next_time = 4; // 本次返回的视频中,发布最早的时间,作为下次请求时的latest_time 27 | } 28 | 29 | // ===============================视频投稿================================== 30 | message PublishActionRequest{ 31 | string token = 1; 32 | bytes data = 2; 33 | string title = 3; 34 | } 35 | message PublishActionResponse { 36 | int32 status_code = 1; 37 | string status_msg = 2; 38 | } 39 | 40 | // ===============================发布列表================================== 41 | message PublishListRequest{ 42 | int64 user_id = 1; 43 | string token = 2; 44 | } 45 | message PublishListResponse{ 46 | int32 status_code = 1; 47 | string status_msg = 2; 48 | repeated Video video_list = 3; 49 | } 50 | service VideoService { 51 | rpc Feed (FeedRequest) returns (FeedResponse); 52 | rpc PublishAction (PublishActionRequest) returns (PublishActionResponse); 53 | rpc PublishList (PublishListRequest) returns (PublishListResponse); 54 | } 55 | 56 | 57 | -------------------------------------------------------------------------------- /pkg/jwt/jwt.go: -------------------------------------------------------------------------------- 1 | package jwt 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/golang-jwt/jwt" 7 | ) 8 | 9 | // JWT signing Key 10 | type JWT struct { 11 | SigningKey []byte 12 | } 13 | 14 | var ( 15 | ErrTokenExpired = errors.New("token expired") 16 | ErrTokenNotValidYet = errors.New("token is not active yet") 17 | ErrTokenMalformed = errors.New("that's not even a token") 18 | ErrTokenInvalid = errors.New("couldn't handle this token") 19 | ) 20 | 21 | // CustomClaims Structured version of Claims Section, as referenced at https://tools.ietf.org/html/rfc7519#section-4.1 See examples for how to use this with your own claim types 22 | type CustomClaims struct { 23 | Id int64 24 | jwt.StandardClaims 25 | } 26 | 27 | func NewJWT(SigningKey []byte) *JWT { 28 | return &JWT{ 29 | SigningKey, 30 | } 31 | } 32 | 33 | // create a new token 34 | func (j *JWT) CreateToken(claims CustomClaims) (string, error) { 35 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 36 | return token.SignedString(j.SigningKey) 37 | } 38 | 39 | // ParseToken parse the token. 40 | func (j *JWT) ParseToken(tokenString string) (*CustomClaims, error) { 41 | token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) { 42 | return j.SigningKey, nil 43 | }) 44 | if err != nil { 45 | if ve, ok := err.(*jwt.ValidationError); ok { 46 | if ve.Errors&jwt.ValidationErrorMalformed != 0 { 47 | return nil, ErrTokenMalformed 48 | } else if ve.Errors&jwt.ValidationErrorExpired != 0 { 49 | return nil, ErrTokenExpired 50 | } else if ve.Errors&jwt.ValidationErrorNotValidYet != 0 { 51 | return nil, ErrTokenNotValidYet 52 | } else { 53 | return nil, ErrTokenInvalid 54 | } 55 | 56 | } 57 | } 58 | if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid { 59 | return claims, nil 60 | } 61 | return nil, ErrTokenInvalid 62 | } 63 | -------------------------------------------------------------------------------- /cmd/user/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | 7 | "github.com/bytedance-youthcamp-jbzx/tiktok/cmd/user/service" 8 | "github.com/bytedance-youthcamp-jbzx/tiktok/kitex/kitex_gen/user/userservice" 9 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/etcd" 10 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/middleware" 11 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/viper" 12 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/zap" 13 | "github.com/cloudwego/kitex/pkg/rpcinfo" 14 | "github.com/cloudwego/kitex/server" 15 | ) 16 | 17 | var ( 18 | config = viper.Init("user") 19 | serviceName = config.Viper.GetString("server.name") 20 | serviceAddr = fmt.Sprintf("%s:%d", config.Viper.GetString("server.host"), config.Viper.GetInt("server.port")) 21 | etcdAddr = fmt.Sprintf("%s:%d", config.Viper.GetString("etcd.host"), config.Viper.GetInt("etcd.port")) 22 | signingKey = config.Viper.GetString("JWT.signingKey") 23 | logger = zap.InitLogger() 24 | ) 25 | 26 | func init() { 27 | service.Init(signingKey) 28 | } 29 | 30 | func main() { 31 | // defer logger.Sync() 32 | 33 | // 服务注册 34 | r, err := etcd.NewEtcdRegistry([]string{etcdAddr}) 35 | if err != nil { 36 | logger.Fatalln(err.Error()) 37 | } 38 | 39 | addr, err := net.ResolveTCPAddr("tcp", serviceAddr) 40 | if err != nil { 41 | logger.Fatalln(err.Error()) 42 | } 43 | 44 | // 初始化etcd 45 | s := userservice.NewServer(new(service.UserServiceImpl), 46 | server.WithServiceAddr(addr), 47 | server.WithMiddleware(middleware.CommonMiddleware), 48 | server.WithMiddleware(middleware.ServerMiddleware), 49 | server.WithRegistry(r), 50 | //server.WithLimit(&limit.Option{MaxConnections: 1000, MaxQPS: 100}), 51 | server.WithMuxTransport(), 52 | //server.WithSuite(tracing.NewServerSuite()), 53 | server.WithServerBasicInfo(&rpcinfo.EndpointBasicInfo{ServiceName: serviceName}), 54 | ) 55 | 56 | if err := s.Run(); err != nil { 57 | logger.Fatalf("%v stopped with error: %v", serviceName, err.Error()) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /cmd/video/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | 7 | "github.com/bytedance-youthcamp-jbzx/tiktok/cmd/video/service" 8 | 9 | "github.com/bytedance-youthcamp-jbzx/tiktok/kitex/kitex_gen/video/videoservice" 10 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/etcd" 11 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/middleware" 12 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/viper" 13 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/zap" 14 | "github.com/cloudwego/kitex/pkg/rpcinfo" 15 | "github.com/cloudwego/kitex/server" 16 | ) 17 | 18 | var ( 19 | config = viper.Init("video") 20 | serviceName = config.Viper.GetString("server.name") 21 | serviceAddr = fmt.Sprintf("%s:%d", config.Viper.GetString("server.host"), config.Viper.GetInt("server.port")) 22 | etcdAddr = fmt.Sprintf("%s:%d", config.Viper.GetString("etcd.host"), config.Viper.GetInt("etcd.port")) 23 | signingKey = config.Viper.GetString("JWT.signingKey") 24 | logger = zap.InitLogger() 25 | ) 26 | 27 | func init() { 28 | service.Init(signingKey) 29 | } 30 | 31 | func main() { 32 | // defer logger.Sync() 33 | 34 | // 服务注册 35 | r, err := etcd.NewEtcdRegistry([]string{etcdAddr}) 36 | if err != nil { 37 | logger.Fatalln(err.Error()) 38 | } 39 | 40 | addr, err := net.ResolveTCPAddr("tcp", serviceAddr) 41 | if err != nil { 42 | logger.Fatalln(err.Error()) 43 | } 44 | 45 | s := videoservice.NewServer(new(service.VideoServiceImpl), 46 | server.WithServiceAddr(addr), 47 | server.WithMiddleware(middleware.CommonMiddleware), 48 | server.WithMiddleware(middleware.ServerMiddleware), 49 | server.WithRegistry(r), 50 | //server.WithLimit(&limit.Option{MaxConnections: 1000, MaxQPS: 100}), 51 | server.WithMuxTransport(), 52 | // server.WithSuite(tracing.NewServerSuite()), 53 | server.WithServerBasicInfo(&rpcinfo.EndpointBasicInfo{ServiceName: serviceName}), 54 | ) 55 | 56 | if err := s.Run(); err != nil { 57 | logger.Fatalf("%v stopped with error: %v", serviceName, err.Error()) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /cmd/comment/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | 7 | "github.com/bytedance-youthcamp-jbzx/tiktok/cmd/comment/service" 8 | "github.com/bytedance-youthcamp-jbzx/tiktok/kitex/kitex_gen/comment/commentservice" 9 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/etcd" 10 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/middleware" 11 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/viper" 12 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/zap" 13 | "github.com/cloudwego/kitex/pkg/rpcinfo" 14 | "github.com/cloudwego/kitex/server" 15 | ) 16 | 17 | var ( 18 | config = viper.Init("comment") 19 | serviceName = config.Viper.GetString("server.name") 20 | serviceAddr = fmt.Sprintf("%s:%d", config.Viper.GetString("server.host"), config.Viper.GetInt("server.port")) 21 | etcdAddr = fmt.Sprintf("%s:%d", config.Viper.GetString("etcd.host"), config.Viper.GetInt("etcd.port")) 22 | signingKey = config.Viper.GetString("JWT.signingKey") 23 | logger = zap.InitLogger() 24 | ) 25 | 26 | func init() { 27 | service.Init(signingKey) 28 | } 29 | 30 | func main() { 31 | // defer logger.Sync() 32 | // 服务注册 33 | r, err := etcd.NewEtcdRegistry([]string{etcdAddr}) 34 | if err != nil { 35 | logger.Fatalln(err.Error()) 36 | } 37 | 38 | addr, err := net.ResolveTCPAddr("tcp", serviceAddr) 39 | if err != nil { 40 | logger.Fatalln(err.Error()) 41 | } 42 | 43 | // 初始化etcd 44 | s := commentservice.NewServer(new(service.CommentServiceImpl), 45 | server.WithServiceAddr(addr), 46 | server.WithMiddleware(middleware.CommonMiddleware), 47 | server.WithMiddleware(middleware.ServerMiddleware), 48 | server.WithRegistry(r), 49 | //server.WithLimit(&limit.Option{MaxConnections: 1000, MaxQPS: 100}), 50 | server.WithMuxTransport(), 51 | // server.WithSuite(tracing.NewServerSuite()), 52 | server.WithServerBasicInfo(&rpcinfo.EndpointBasicInfo{ServiceName: serviceName}), 53 | ) 54 | 55 | if err := s.Run(); err != nil { 56 | logger.Fatalf("%v stopped with error: %v", serviceName, err.Error()) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /cmd/message/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | 7 | "github.com/bytedance-youthcamp-jbzx/tiktok/cmd/message/service" 8 | "github.com/bytedance-youthcamp-jbzx/tiktok/kitex/kitex_gen/message/messageservice" 9 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/etcd" 10 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/middleware" 11 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/viper" 12 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/zap" 13 | "github.com/cloudwego/kitex/pkg/rpcinfo" 14 | "github.com/cloudwego/kitex/server" 15 | ) 16 | 17 | var ( 18 | config = viper.Init("message") 19 | serviceName = config.Viper.GetString("server.name") 20 | serviceAddr = fmt.Sprintf("%s:%d", config.Viper.GetString("server.host"), config.Viper.GetInt("server.port")) 21 | etcdAddr = fmt.Sprintf("%s:%d", config.Viper.GetString("etcd.host"), config.Viper.GetInt("etcd.port")) 22 | signingKey = config.Viper.GetString("JWT.signingKey") 23 | logger = zap.InitLogger() 24 | ) 25 | 26 | func init() { 27 | service.Init(signingKey) 28 | } 29 | 30 | func main() { 31 | // defer logger.Sync() 32 | 33 | // 服务注册 34 | r, err := etcd.NewEtcdRegistry([]string{etcdAddr}) 35 | if err != nil { 36 | logger.Fatalln(err.Error()) 37 | } 38 | 39 | addr, err := net.ResolveTCPAddr("tcp", serviceAddr) 40 | if err != nil { 41 | logger.Fatalln(err.Error()) 42 | } 43 | 44 | // 初始化etcd 45 | s := messageservice.NewServer(new(service.MessageServiceImpl), 46 | server.WithServiceAddr(addr), 47 | server.WithMiddleware(middleware.CommonMiddleware), 48 | server.WithMiddleware(middleware.ServerMiddleware), 49 | server.WithRegistry(r), 50 | //server.WithLimit(&limit.Option{MaxConnections: 1000, MaxQPS: 100}), 51 | server.WithMuxTransport(), 52 | // server.WithSuite(tracing.NewServerSuite()), 53 | server.WithServerBasicInfo(&rpcinfo.EndpointBasicInfo{ServiceName: serviceName}), 54 | ) 55 | 56 | if err := s.Run(); err != nil { 57 | logger.Fatalf("%v stopped with error: %v", serviceName, err.Error()) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /cmd/favorite/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | 7 | "github.com/bytedance-youthcamp-jbzx/tiktok/cmd/favorite/service" 8 | 9 | "github.com/bytedance-youthcamp-jbzx/tiktok/kitex/kitex_gen/favorite/favoriteservice" 10 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/etcd" 11 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/middleware" 12 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/viper" 13 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/zap" 14 | "github.com/cloudwego/kitex/pkg/rpcinfo" 15 | "github.com/cloudwego/kitex/server" 16 | ) 17 | 18 | var ( 19 | config = viper.Init("favorite") 20 | serviceName = config.Viper.GetString("server.name") 21 | serviceAddr = fmt.Sprintf("%s:%d", config.Viper.GetString("server.host"), config.Viper.GetInt("server.port")) 22 | etcdAddr = fmt.Sprintf("%s:%d", config.Viper.GetString("etcd.host"), config.Viper.GetInt("etcd.port")) 23 | signingKey = config.Viper.GetString("JWT.signingKey") 24 | logger = zap.InitLogger() 25 | ) 26 | 27 | func init() { 28 | service.Init(signingKey) 29 | } 30 | 31 | func main() { 32 | defer service.FavoriteMq.Destroy() 33 | 34 | // 服务注册 35 | r, err := etcd.NewEtcdRegistry([]string{etcdAddr}) 36 | if err != nil { 37 | logger.Fatalln(err.Error()) 38 | } 39 | 40 | addr, err := net.ResolveTCPAddr("tcp", serviceAddr) 41 | if err != nil { 42 | logger.Fatalln(err.Error()) 43 | } 44 | 45 | // 初始化etcd 46 | s := favoriteservice.NewServer(new(service.FavoriteServiceImpl), 47 | server.WithServiceAddr(addr), 48 | server.WithMiddleware(middleware.CommonMiddleware), 49 | server.WithMiddleware(middleware.ServerMiddleware), 50 | server.WithRegistry(r), 51 | //server.WithLimit(&limit.Option{MaxConnections: 1000, MaxQPS: 100}), 52 | server.WithMuxTransport(), 53 | // server.WithSuite(tracing.NewServerSuite()), 54 | server.WithServerBasicInfo(&rpcinfo.EndpointBasicInfo{ServiceName: serviceName}), 55 | ) 56 | 57 | if err := s.Run(); err != nil { 58 | logger.Fatalf("%v stopped with error: %v", serviceName, err.Error()) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /cmd/relation/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | 7 | "github.com/cloudwego/kitex/pkg/rpcinfo" 8 | 9 | "github.com/bytedance-youthcamp-jbzx/tiktok/cmd/relation/service" 10 | "github.com/bytedance-youthcamp-jbzx/tiktok/kitex/kitex_gen/relation/relationservice" 11 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/etcd" 12 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/middleware" 13 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/viper" 14 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/zap" 15 | "github.com/cloudwego/kitex/server" 16 | ) 17 | 18 | var ( 19 | config = viper.Init("relation") 20 | serviceName = config.Viper.GetString("server.name") 21 | serviceAddr = fmt.Sprintf("%s:%d", config.Viper.GetString("server.host"), config.Viper.GetInt("server.port")) 22 | etcdAddr = fmt.Sprintf("%s:%d", config.Viper.GetString("etcd.host"), config.Viper.GetInt("etcd.port")) 23 | signingKey = config.Viper.GetString("JWT.signingKey") 24 | logger = zap.InitLogger() 25 | ) 26 | 27 | func init() { 28 | service.Init(signingKey) 29 | } 30 | 31 | func main() { 32 | defer service.RelationMq.Destroy() 33 | 34 | // 服务注册 35 | r, err := etcd.NewEtcdRegistry([]string{etcdAddr}) 36 | if err != nil { 37 | logger.Fatalln(err.Error()) 38 | } 39 | 40 | addr, err := net.ResolveTCPAddr("tcp", serviceAddr) 41 | if err != nil { 42 | logger.Fatalln(err.Error()) 43 | } 44 | 45 | // 初始化etcd 46 | s := relationservice.NewServer(new(service.RelationServiceImpl), 47 | server.WithServiceAddr(addr), 48 | server.WithMiddleware(middleware.CommonMiddleware), 49 | server.WithMiddleware(middleware.ServerMiddleware), 50 | server.WithRegistry(r), 51 | //server.WithLimit(&limit.Option{MaxConnections: 1000, MaxQPS: 100}), 52 | server.WithMuxTransport(), 53 | // server.WithSuite(tracing.NewServerSuite()), 54 | server.WithServerBasicInfo(&rpcinfo.EndpointBasicInfo{ServiceName: serviceName}), 55 | ) 56 | 57 | if err := s.Run(); err != nil { 58 | logger.Fatalf("%v stopped with error: %v", serviceName, err.Error()) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /cmd/api/rpc/comment.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/bytedance-youthcamp-jbzx/tiktok/kitex/kitex_gen/comment" 9 | "github.com/bytedance-youthcamp-jbzx/tiktok/kitex/kitex_gen/comment/commentservice" 10 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/etcd" 11 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/middleware" 12 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/viper" 13 | "github.com/cloudwego/kitex/client" 14 | "github.com/cloudwego/kitex/pkg/retry" 15 | "github.com/cloudwego/kitex/pkg/rpcinfo" 16 | ) 17 | 18 | var ( 19 | commentClient commentservice.Client 20 | ) 21 | 22 | func InitComment(config *viper.Config) { 23 | etcdAddr := fmt.Sprintf("%s:%d", config.Viper.GetString("etcd.host"), config.Viper.GetInt("etcd.port")) 24 | serviceName := config.Viper.GetString("server.name") 25 | r, err := etcd.NewEtcdResolver([]string{etcdAddr}) 26 | if err != nil { 27 | panic(err) 28 | } 29 | 30 | c, err := commentservice.NewClient( 31 | serviceName, 32 | client.WithMiddleware(middleware.CommonMiddleware), 33 | client.WithInstanceMW(middleware.ClientMiddleware), 34 | client.WithMuxConnection(1), // mux 35 | client.WithRPCTimeout(30*time.Second), // rpc timeout 36 | client.WithConnectTimeout(30000*time.Millisecond), // conn timeout 37 | client.WithFailureRetry(retry.NewFailurePolicy()), // retry 38 | //client.WithSuite(tracing.NewClientSuite()), // tracer 39 | client.WithResolver(r), // resolver 40 | // Please keep the same as provider.WithServiceName 41 | client.WithClientBasicInfo(&rpcinfo.EndpointBasicInfo{ServiceName: serviceName}), 42 | ) 43 | if err != nil { 44 | panic(err) 45 | } 46 | commentClient = c 47 | } 48 | 49 | func CommentAction(ctx context.Context, req *comment.CommentActionRequest) (*comment.CommentActionResponse, error) { 50 | return commentClient.CommentAction(ctx, req) 51 | } 52 | 53 | func CommentList(ctx context.Context, req *comment.CommentListRequest) (*comment.CommentListResponse, error) { 54 | return commentClient.CommentList(ctx, req) 55 | } 56 | -------------------------------------------------------------------------------- /pkg/minio/minio.go: -------------------------------------------------------------------------------- 1 | package minio 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "io" 7 | "time" 8 | 9 | "github.com/minio/minio-go/v7" 10 | ) 11 | 12 | func CreateBucket(bucketName string) error { 13 | if len(bucketName) <= 0 { 14 | return errors.New("bucketName invalid") 15 | } 16 | ctx := context.Background() 17 | if err := minioClient.MakeBucket(ctx, bucketName, minio.MakeBucketOptions{}); err != nil { 18 | exists, errEx := minioClient.BucketExists(ctx, bucketName) 19 | if exists && errEx != nil { 20 | // nothing 21 | } else { 22 | return errEx 23 | } 24 | 25 | } 26 | return nil 27 | } 28 | 29 | func UploadFileByPath(bucketName, objectName, path, contentType string) (int64, error) { 30 | if len(bucketName) <= 0 || len(objectName) <= 0 || len(path) <= 0 { 31 | return -1, errors.New("invalid argument") 32 | } 33 | 34 | uploadInfo, err := minioClient.FPutObject(context.Background(), bucketName, objectName, path, minio.PutObjectOptions{ 35 | ContentType: contentType, 36 | }) 37 | 38 | if err != nil { 39 | return -1, err 40 | } 41 | 42 | return uploadInfo.Size, nil 43 | } 44 | 45 | func UploadFileByIO(bucketName, objectName string, reader io.Reader, size int64, contentType string) (int64, error) { 46 | if len(bucketName) <= 0 || len(objectName) <= 0 { 47 | return -1, errors.New("invalid argument") 48 | } 49 | 50 | uploadInfo, err := minioClient.PutObject(context.Background(), bucketName, objectName, reader, size, minio.PutObjectOptions{ 51 | ContentType: contentType, 52 | }) 53 | 54 | if err != nil { 55 | return -1, err 56 | } 57 | 58 | return uploadInfo.Size, nil 59 | } 60 | 61 | func GetFileTemporaryURL(bucketName, objectName string) (string, error) { 62 | if len(bucketName) <= 0 || len(objectName) <= 0 { 63 | return "", errors.New("invalid argument") 64 | } 65 | 66 | expiry := time.Second * time.Duration(ExpireTime) 67 | 68 | presignedURL, err := minioClient.PresignedGetObject(context.Background(), bucketName, objectName, expiry, nil) 69 | 70 | if err != nil { 71 | return "", err 72 | } 73 | 74 | return presignedURL.String(), nil 75 | 76 | } 77 | -------------------------------------------------------------------------------- /pkg/middleware/limit_init.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | 7 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/viper" 8 | ) 9 | 10 | type TokenBucket struct { 11 | Rate int64 // 固定的token放入速率,r/s 12 | Capacity int64 // 桶的容量 13 | Tokens int64 // 桶中当前token数量 14 | LastTokenSec int64 // 桶上次放token的时间 15 | 16 | lock sync.Mutex 17 | } 18 | 19 | func (tb *TokenBucket) Allow() bool { 20 | tb.lock.Lock() 21 | defer tb.lock.Unlock() 22 | now := time.Now().Unix() 23 | tb.Tokens = tb.Tokens + (now-tb.LastTokenSec)*tb.Rate 24 | if tb.Tokens > tb.Capacity { 25 | tb.Tokens = tb.Capacity 26 | } 27 | tb.LastTokenSec = now 28 | if tb.Tokens > 0 { 29 | tb.Tokens-- 30 | return true 31 | } 32 | return false 33 | } 34 | 35 | func MakeTokenBucket(c, r int64) *TokenBucket { 36 | return &TokenBucket{ 37 | Rate: r, 38 | Capacity: c, 39 | Tokens: int64(limiterTokenInit), 40 | LastTokenSec: time.Now().Unix(), 41 | } 42 | } 43 | 44 | type TokenBuckets struct { 45 | buckets map[string]*TokenBucket 46 | capacity int64 47 | rate int64 48 | 49 | lock sync.Mutex 50 | } 51 | 52 | func MakeTokenBuckets(capacity, rate int64) *TokenBuckets { 53 | return &TokenBuckets{ 54 | buckets: make(map[string]*TokenBucket), 55 | capacity: capacity, 56 | rate: rate, 57 | } 58 | } 59 | 60 | func (tbs *TokenBuckets) Allow(token string) bool { 61 | tbs.lock.Lock() 62 | defer tbs.lock.Unlock() 63 | if bucket, ok := tbs.buckets[token]; ok { 64 | return bucket.Allow() 65 | } else { 66 | tbs.buckets[token] = MakeTokenBucket(tbs.capacity, tbs.rate) 67 | return tbs.buckets[token].Allow() 68 | } 69 | } 70 | 71 | // 对每个token限流,不管它请求的API 72 | var ( 73 | apiConfig = viper.Init("api") 74 | limiterCapacity = apiConfig.Viper.GetInt("server.limit.capacity") 75 | limiterRate = apiConfig.Viper.GetInt("server.limit.rate") 76 | limiterTokenInit = apiConfig.Viper.GetInt("server.limit.tokenInit") 77 | CurrentLimiter = MakeTokenBuckets(int64(limiterCapacity), int64(limiterRate)) // 后续用redis缓存每个token对应的令牌桶 78 | ) 79 | 80 | func init() { 81 | 82 | } 83 | -------------------------------------------------------------------------------- /cmd/api/rpc/message.go: -------------------------------------------------------------------------------- 1 | // Package rpc /* 2 | package rpc 3 | 4 | import ( 5 | "context" 6 | "fmt" 7 | "time" 8 | 9 | message "github.com/bytedance-youthcamp-jbzx/tiktok/kitex/kitex_gen/message" 10 | "github.com/bytedance-youthcamp-jbzx/tiktok/kitex/kitex_gen/message/messageservice" 11 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/etcd" 12 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/middleware" 13 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/viper" 14 | "github.com/cloudwego/kitex/client" 15 | "github.com/cloudwego/kitex/pkg/retry" 16 | "github.com/cloudwego/kitex/pkg/rpcinfo" 17 | ) 18 | 19 | var ( 20 | messageClient messageservice.Client 21 | ) 22 | 23 | func InitMessage(config *viper.Config) { 24 | etcdAddr := fmt.Sprintf("%s:%d", config.Viper.GetString("etcd.host"), config.Viper.GetInt("etcd.port")) 25 | serviceName := config.Viper.GetString("server.name") 26 | r, err := etcd.NewEtcdResolver([]string{etcdAddr}) 27 | if err != nil { 28 | panic(err) 29 | } 30 | 31 | c, err := messageservice.NewClient( 32 | serviceName, 33 | client.WithMiddleware(middleware.CommonMiddleware), 34 | client.WithInstanceMW(middleware.ClientMiddleware), 35 | client.WithMuxConnection(1), // mux 36 | client.WithRPCTimeout(30*time.Second), // rpc timeout 37 | client.WithConnectTimeout(30000*time.Millisecond), // conn timeout 38 | client.WithFailureRetry(retry.NewFailurePolicy()), // retry 39 | //client.WithSuite(tracing.NewClientSuite()), // tracer 40 | client.WithResolver(r), // resolver 41 | // Please keep the same as provider.WithServiceName 42 | client.WithClientBasicInfo(&rpcinfo.EndpointBasicInfo{ServiceName: serviceName}), 43 | ) 44 | if err != nil { 45 | panic(err) 46 | } 47 | messageClient = c 48 | } 49 | 50 | func MessageAction(ctx context.Context, req *message.MessageActionRequest) (*message.MessageActionResponse, error) { 51 | return messageClient.MessageAction(ctx, req) 52 | } 53 | 54 | func MessageChat(ctx context.Context, req *message.MessageChatRequest) (*message.MessageChatResponse, error) { 55 | return messageClient.MessageChat(ctx, req) 56 | } 57 | -------------------------------------------------------------------------------- /cmd/api/rpc/favorite.go: -------------------------------------------------------------------------------- 1 | // Package rpc /* 2 | package rpc 3 | 4 | import ( 5 | "context" 6 | "fmt" 7 | "time" 8 | 9 | "github.com/bytedance-youthcamp-jbzx/tiktok/kitex/kitex_gen/favorite" 10 | "github.com/bytedance-youthcamp-jbzx/tiktok/kitex/kitex_gen/favorite/favoriteservice" 11 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/etcd" 12 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/middleware" 13 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/viper" 14 | "github.com/cloudwego/kitex/client" 15 | "github.com/cloudwego/kitex/pkg/retry" 16 | "github.com/cloudwego/kitex/pkg/rpcinfo" 17 | ) 18 | 19 | var ( 20 | favoriteClient favoriteservice.Client 21 | ) 22 | 23 | func InitFavorite(config *viper.Config) { 24 | etcdAddr := fmt.Sprintf("%s:%d", config.Viper.GetString("etcd.host"), config.Viper.GetInt("etcd.port")) 25 | serviceName := config.Viper.GetString("server.name") 26 | r, err := etcd.NewEtcdResolver([]string{etcdAddr}) 27 | if err != nil { 28 | panic(err) 29 | } 30 | 31 | c, err := favoriteservice.NewClient( 32 | serviceName, 33 | client.WithMiddleware(middleware.CommonMiddleware), 34 | client.WithInstanceMW(middleware.ClientMiddleware), 35 | client.WithMuxConnection(1), // mux 36 | client.WithRPCTimeout(30*time.Second), // rpc timeout 37 | client.WithConnectTimeout(30000*time.Millisecond), // conn timeout 38 | client.WithFailureRetry(retry.NewFailurePolicy()), // retry 39 | //client.WithSuite(tracing.NewClientSuite()), // tracer 40 | client.WithResolver(r), // resolver 41 | // Please keep the same as provider.WithServiceName 42 | client.WithClientBasicInfo(&rpcinfo.EndpointBasicInfo{ServiceName: serviceName}), 43 | ) 44 | if err != nil { 45 | panic(err) 46 | } 47 | favoriteClient = c 48 | } 49 | 50 | func FavoriteAction(ctx context.Context, req *favorite.FavoriteActionRequest) (*favorite.FavoriteActionResponse, error) { 51 | return favoriteClient.FavoriteAction(ctx, req) 52 | } 53 | 54 | func FavoriteList(ctx context.Context, req *favorite.FavoriteListRequest) (*favorite.FavoriteListResponse, error) { 55 | return favoriteClient.FavoriteList(ctx, req) 56 | } 57 | -------------------------------------------------------------------------------- /cmd/api/rpc/user.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | user "github.com/bytedance-youthcamp-jbzx/tiktok/kitex/kitex_gen/user" 9 | "github.com/bytedance-youthcamp-jbzx/tiktok/kitex/kitex_gen/user/userservice" 10 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/etcd" 11 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/middleware" 12 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/viper" 13 | "github.com/cloudwego/kitex/client" 14 | "github.com/cloudwego/kitex/pkg/retry" 15 | "github.com/cloudwego/kitex/pkg/rpcinfo" 16 | ) 17 | 18 | var ( 19 | userClient userservice.Client 20 | ) 21 | 22 | func InitUser(config *viper.Config) { 23 | etcdAddr := fmt.Sprintf("%s:%d", config.Viper.GetString("etcd.host"), config.Viper.GetInt("etcd.port")) 24 | serviceName := config.Viper.GetString("server.name") 25 | r, err := etcd.NewEtcdResolver([]string{etcdAddr}) 26 | if err != nil { 27 | panic(err) 28 | } 29 | 30 | c, err := userservice.NewClient( 31 | serviceName, 32 | client.WithMiddleware(middleware.CommonMiddleware), 33 | client.WithInstanceMW(middleware.ClientMiddleware), 34 | client.WithMuxConnection(1), // mux 35 | client.WithRPCTimeout(30*time.Second), // rpc timeout 36 | client.WithConnectTimeout(30000*time.Millisecond), // conn timeout 37 | client.WithFailureRetry(retry.NewFailurePolicy()), // retry 38 | //client.WithSuite(tracing.NewClientSuite()), // tracer 39 | client.WithResolver(r), // resolver 40 | // Please keep the same as provider.WithServiceName 41 | client.WithClientBasicInfo(&rpcinfo.EndpointBasicInfo{ServiceName: serviceName}), 42 | ) 43 | if err != nil { 44 | panic(err) 45 | } 46 | userClient = c 47 | } 48 | 49 | func Register(ctx context.Context, req *user.UserRegisterRequest) (*user.UserRegisterResponse, error) { 50 | return userClient.Register(ctx, req) 51 | } 52 | 53 | func Login(ctx context.Context, req *user.UserLoginRequest) (*user.UserLoginResponse, error) { 54 | return userClient.Login(ctx, req) 55 | } 56 | 57 | func UserInfo(ctx context.Context, req *user.UserInfoRequest) (*user.UserInfoResponse, error) { 58 | return userClient.UserInfo(ctx, req) 59 | } 60 | -------------------------------------------------------------------------------- /pkg/zap/zap.go: -------------------------------------------------------------------------------- 1 | package zap 2 | 3 | import ( 4 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/viper" 5 | 6 | "go.uber.org/zap" 7 | "go.uber.org/zap/zapcore" 8 | "gopkg.in/natefinch/lumberjack.v2" 9 | ) 10 | 11 | var ( 12 | config = viper.Init("log") 13 | infoPath = config.Viper.GetString("info") //INFO&DEBUG&WARN级别的日志输出位置 14 | errorPath = config.Viper.GetString("error") //ERROR和FATAL级别的日志输出位置 15 | ) 16 | 17 | // InitLogger 初始化zap 18 | func InitLogger() *zap.SugaredLogger { 19 | //规定日志级别 20 | highPriority := zap.LevelEnablerFunc(func(lev zapcore.Level) bool { 21 | return lev >= zap.ErrorLevel 22 | }) 23 | 24 | lowPriority := zap.LevelEnablerFunc(func(lev zapcore.Level) bool { 25 | return lev < zap.ErrorLevel && lev >= zap.DebugLevel 26 | }) 27 | 28 | //各级别通用的encoder 29 | encoder := getEncoder() 30 | 31 | //INFO级别的日志 32 | infoSyncer := getInfoWriter() 33 | infoCore := zapcore.NewCore(encoder, infoSyncer, lowPriority) 34 | 35 | //ERROR级别的日志 36 | errorSyncer := getErrorWriter() 37 | errorCore := zapcore.NewCore(encoder, errorSyncer, highPriority) 38 | 39 | //合并各种级别的日志 40 | core := zapcore.NewTee(infoCore, errorCore) 41 | logger := zap.New(core, zap.AddCaller()) 42 | sugarLogger := logger.Sugar() 43 | return sugarLogger 44 | } 45 | 46 | // 自定义日志输出格式 47 | func getEncoder() zapcore.Encoder { 48 | encoderConfig := zap.NewProductionEncoderConfig() 49 | encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder 50 | encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder 51 | return zapcore.NewConsoleEncoder(encoderConfig) 52 | } 53 | 54 | // 获取INFO的Writer 55 | func getInfoWriter() zapcore.WriteSyncer { 56 | //lumberJack:日志切割归档 57 | lumberJackLogger := &lumberjack.Logger{ 58 | Filename: infoPath, 59 | MaxSize: 100, 60 | MaxBackups: 5, 61 | MaxAge: 30, 62 | Compress: false, 63 | } 64 | return zapcore.AddSync(lumberJackLogger) 65 | } 66 | 67 | // 获取ERROR的Writer 68 | func getErrorWriter() zapcore.WriteSyncer { 69 | //lumberJack:日志切割归档 70 | lumberJackLogger := &lumberjack.Logger{ 71 | Filename: errorPath, 72 | MaxSize: 100, 73 | MaxBackups: 5, 74 | MaxAge: 30, 75 | Compress: false, 76 | } 77 | return zapcore.AddSync(lumberJackLogger) 78 | } 79 | -------------------------------------------------------------------------------- /cmd/api/rpc/video.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | video "github.com/bytedance-youthcamp-jbzx/tiktok/kitex/kitex_gen/video" 9 | "github.com/bytedance-youthcamp-jbzx/tiktok/kitex/kitex_gen/video/videoservice" 10 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/etcd" 11 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/middleware" 12 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/viper" 13 | "github.com/cloudwego/kitex/client" 14 | "github.com/cloudwego/kitex/pkg/retry" 15 | "github.com/cloudwego/kitex/pkg/rpcinfo" 16 | ) 17 | 18 | var ( 19 | videoClient videoservice.Client 20 | ) 21 | 22 | func InitVideo(config *viper.Config) { 23 | etcdAddr := fmt.Sprintf("%s:%d", config.Viper.GetString("etcd.host"), config.Viper.GetInt("etcd.port")) 24 | serviceName := config.Viper.GetString("server.name") 25 | r, err := etcd.NewEtcdResolver([]string{etcdAddr}) 26 | if err != nil { 27 | panic(err) 28 | } 29 | 30 | c, err := videoservice.NewClient( 31 | serviceName, 32 | client.WithMiddleware(middleware.CommonMiddleware), 33 | client.WithInstanceMW(middleware.ClientMiddleware), 34 | client.WithMuxConnection(1), // mux 35 | client.WithRPCTimeout(300*time.Second), // rpc timeout 36 | client.WithConnectTimeout(300000*time.Millisecond), // conn timeout 37 | client.WithFailureRetry(retry.NewFailurePolicy()), // retry 38 | //client.WithSuite(tracing.NewClientSuite()), // tracer 39 | client.WithResolver(r), // resolver 40 | // Please keep the same as provider.WithServiceName 41 | client.WithClientBasicInfo(&rpcinfo.EndpointBasicInfo{ServiceName: serviceName}), 42 | ) 43 | if err != nil { 44 | panic(err) 45 | } 46 | videoClient = c 47 | } 48 | 49 | func Feed(ctx context.Context, req *video.FeedRequest) (*video.FeedResponse, error) { 50 | 51 | return videoClient.Feed(ctx, req) 52 | } 53 | 54 | func PublishAction(ctx context.Context, req *video.PublishActionRequest) (*video.PublishActionResponse, error) { 55 | return videoClient.PublishAction(ctx, req) 56 | } 57 | 58 | func PublishList(ctx context.Context, req *video.PublishListRequest) (*video.PublishListResponse, error) { 59 | return videoClient.PublishList(ctx, req) 60 | } 61 | -------------------------------------------------------------------------------- /kitex/kitex_gen/comment/commentservice/client.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.4.4. DO NOT EDIT. 2 | 3 | package commentservice 4 | 5 | import ( 6 | "context" 7 | comment "github.com/bytedance-youthcamp-jbzx/tiktok/kitex/kitex_gen/comment" 8 | client "github.com/cloudwego/kitex/client" 9 | callopt "github.com/cloudwego/kitex/client/callopt" 10 | ) 11 | 12 | // Client is designed to provide IDL-compatible methods with call-option parameter for kitex framework. 13 | type Client interface { 14 | CommentAction(ctx context.Context, Req *comment.CommentActionRequest, callOptions ...callopt.Option) (r *comment.CommentActionResponse, err error) 15 | CommentList(ctx context.Context, Req *comment.CommentListRequest, callOptions ...callopt.Option) (r *comment.CommentListResponse, err error) 16 | } 17 | 18 | // NewClient creates a client for the service defined in IDL. 19 | func NewClient(destService string, opts ...client.Option) (Client, error) { 20 | var options []client.Option 21 | options = append(options, client.WithDestService(destService)) 22 | 23 | options = append(options, opts...) 24 | 25 | kc, err := client.NewClient(serviceInfo(), options...) 26 | if err != nil { 27 | return nil, err 28 | } 29 | return &kCommentServiceClient{ 30 | kClient: newServiceClient(kc), 31 | }, nil 32 | } 33 | 34 | // MustNewClient creates a client for the service defined in IDL. It panics if any error occurs. 35 | func MustNewClient(destService string, opts ...client.Option) Client { 36 | kc, err := NewClient(destService, opts...) 37 | if err != nil { 38 | panic(err) 39 | } 40 | return kc 41 | } 42 | 43 | type kCommentServiceClient struct { 44 | *kClient 45 | } 46 | 47 | func (p *kCommentServiceClient) CommentAction(ctx context.Context, Req *comment.CommentActionRequest, callOptions ...callopt.Option) (r *comment.CommentActionResponse, err error) { 48 | ctx = client.NewCtxWithCallOptions(ctx, callOptions) 49 | return p.kClient.CommentAction(ctx, Req) 50 | } 51 | 52 | func (p *kCommentServiceClient) CommentList(ctx context.Context, Req *comment.CommentListRequest, callOptions ...callopt.Option) (r *comment.CommentListResponse, err error) { 53 | ctx = client.NewCtxWithCallOptions(ctx, callOptions) 54 | return p.kClient.CommentList(ctx, Req) 55 | } 56 | -------------------------------------------------------------------------------- /kitex/kitex_gen/message/messageservice/client.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.4.4. DO NOT EDIT. 2 | 3 | package messageservice 4 | 5 | import ( 6 | "context" 7 | message "github.com/bytedance-youthcamp-jbzx/tiktok/kitex/kitex_gen/message" 8 | client "github.com/cloudwego/kitex/client" 9 | callopt "github.com/cloudwego/kitex/client/callopt" 10 | ) 11 | 12 | // Client is designed to provide IDL-compatible methods with call-option parameter for kitex framework. 13 | type Client interface { 14 | MessageChat(ctx context.Context, Req *message.MessageChatRequest, callOptions ...callopt.Option) (r *message.MessageChatResponse, err error) 15 | MessageAction(ctx context.Context, Req *message.MessageActionRequest, callOptions ...callopt.Option) (r *message.MessageActionResponse, err error) 16 | } 17 | 18 | // NewClient creates a client for the service defined in IDL. 19 | func NewClient(destService string, opts ...client.Option) (Client, error) { 20 | var options []client.Option 21 | options = append(options, client.WithDestService(destService)) 22 | 23 | options = append(options, opts...) 24 | 25 | kc, err := client.NewClient(serviceInfo(), options...) 26 | if err != nil { 27 | return nil, err 28 | } 29 | return &kMessageServiceClient{ 30 | kClient: newServiceClient(kc), 31 | }, nil 32 | } 33 | 34 | // MustNewClient creates a client for the service defined in IDL. It panics if any error occurs. 35 | func MustNewClient(destService string, opts ...client.Option) Client { 36 | kc, err := NewClient(destService, opts...) 37 | if err != nil { 38 | panic(err) 39 | } 40 | return kc 41 | } 42 | 43 | type kMessageServiceClient struct { 44 | *kClient 45 | } 46 | 47 | func (p *kMessageServiceClient) MessageChat(ctx context.Context, Req *message.MessageChatRequest, callOptions ...callopt.Option) (r *message.MessageChatResponse, err error) { 48 | ctx = client.NewCtxWithCallOptions(ctx, callOptions) 49 | return p.kClient.MessageChat(ctx, Req) 50 | } 51 | 52 | func (p *kMessageServiceClient) MessageAction(ctx context.Context, Req *message.MessageActionRequest, callOptions ...callopt.Option) (r *message.MessageActionResponse, err error) { 53 | ctx = client.NewCtxWithCallOptions(ctx, callOptions) 54 | return p.kClient.MessageAction(ctx, Req) 55 | } 56 | -------------------------------------------------------------------------------- /cmd/favorite/service/timer.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/bytedance-youthcamp-jbzx/tiktok/dal/redis" 8 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/gocron" 9 | ) 10 | 11 | const frequency = 10 12 | 13 | // 点赞服务消息队列消费者 14 | func consume() error { 15 | msgs, err := FavoriteMq.ConsumeSimple() 16 | if err != nil { 17 | fmt.Println(err.Error()) 18 | logger.Errorf("FavoriteMQ Err: %s", err.Error()) 19 | } 20 | // 将消息队列的消息全部取出 21 | for msg := range msgs { 22 | //err := redis.LockByMutex(context.Background(), redis.FavoriteMutex) 23 | //if err != nil { 24 | // logger.Errorf("Redis mutex lock error: %s", err.Error()) 25 | // return err 26 | //} 27 | fc := new(redis.FavoriteCache) 28 | // 解析json 29 | if err = json.Unmarshal(msg.Body, &fc); err != nil { 30 | logger.Errorf("json unmarshal error: %s", err.Error()) 31 | fmt.Println("json unmarshal error:" + err.Error()) 32 | //err = redis.UnlockByMutex(context.Background(), redis.FavoriteMutex) 33 | //if err != nil { 34 | // logger.Errorf("Redis mutex unlock error: %s", err.Error()) 35 | // return err 36 | //} 37 | continue 38 | } 39 | fmt.Printf("==> Get new message: %v\n", fc) 40 | // 将结构体存入redis 41 | if err = redis.UpdateFavorite(context.Background(), fc); err != nil { 42 | logger.Errorf("json unmarshal error: %s", err.Error()) 43 | fmt.Println("json unmarshal error:" + err.Error()) 44 | //err = redis.UnlockByMutex(context.Background(), redis.FavoriteMutex) 45 | //if err != nil { 46 | // logger.Errorf("Redis mutex unlock error: %s", err.Error()) 47 | // return err 48 | //} 49 | continue 50 | } 51 | //err = redis.UnlockByMutex(context.Background(), redis.FavoriteMutex) 52 | //if err != nil { 53 | // logger.Errorf("Redis mutex unlock error: %s", err.Error()) 54 | // return err 55 | //} 56 | if !autoAck { 57 | err := msg.Ack(true) 58 | if err != nil { 59 | logger.Errorf("ack error: %s", err.Error()) 60 | return err 61 | } 62 | } 63 | } 64 | return nil 65 | } 66 | 67 | // gocron定时任务,每隔一段时间就让Consumer消费消息队列的所有消息 68 | func GoCron() { 69 | s := gocron.NewSchedule() 70 | s.Every(frequency).Tag("favoriteMQ").Seconds().Do(consume) 71 | s.StartAsync() 72 | } 73 | -------------------------------------------------------------------------------- /cmd/relation/service/timer.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | 8 | "github.com/bytedance-youthcamp-jbzx/tiktok/dal/redis" 9 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/gocron" 10 | ) 11 | 12 | const frequency = 10 13 | 14 | // 点赞服务消息队列消费者 15 | func consume() error { 16 | msgs, err := RelationMq.ConsumeSimple() 17 | if err != nil { 18 | fmt.Println(err.Error()) 19 | logger.Errorf("RelationMQ Err: %s", err.Error()) 20 | return err 21 | } 22 | // 将消息队列的消息全部取出 23 | for msg := range msgs { 24 | //err := redis.LockByMutex(context.Background(), redis.RelationMutex) 25 | //if err != nil { 26 | // logger.Errorf("Redis mutex lock error: %s", err.Error()) 27 | // return err 28 | //} 29 | rc := new(redis.RelationCache) 30 | // 解析json 31 | if err = json.Unmarshal(msg.Body, &rc); err != nil { 32 | fmt.Println("json unmarshal error:" + err.Error()) 33 | logger.Errorf("RelationMQ Err: %s", err.Error()) 34 | //err = redis.UnlockByMutex(context.Background(), redis.RelationMutex) 35 | //if err != nil { 36 | // logger.Errorf("Redis mutex unlock error: %s", err.Error()) 37 | // return err 38 | //} 39 | continue 40 | } 41 | fmt.Printf("==> Get new message: %v\n", rc) 42 | // 将结构体存入redis 43 | if err = redis.UpdateRelation(context.Background(), rc); err != nil { 44 | fmt.Println("add to redis error:" + err.Error()) 45 | logger.Errorf("RelationMQ Err: %s", err.Error()) 46 | //err = redis.UnlockByMutex(context.Background(), redis.RelationMutex) 47 | //if err != nil { 48 | // logger.Errorf("Redis mutex unlock error: %s", err.Error()) 49 | // return err 50 | //} 51 | continue 52 | } 53 | //err = redis.UnlockByMutex(context.Background(), redis.RelationMutex) 54 | //if err != nil { 55 | // logger.Errorf("Redis mutex unlock error: %s", err.Error()) 56 | // return err 57 | //} 58 | if !autoAck { 59 | err := msg.Ack(true) 60 | if err != nil { 61 | logger.Errorf("ack error: %s", err.Error()) 62 | return err 63 | } 64 | } 65 | } 66 | return nil 67 | } 68 | 69 | // gocron定时任务,每隔一段时间就让Consumer消费消息队列的所有消息 70 | func GoCron() { 71 | s := gocron.NewSchedule() 72 | s.Every(frequency).Tag("relationMQ").Seconds().Do(consume) 73 | s.StartAsync() 74 | } 75 | -------------------------------------------------------------------------------- /kitex/kitex_gen/favorite/favoriteservice/client.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.4.4. DO NOT EDIT. 2 | 3 | package favoriteservice 4 | 5 | import ( 6 | "context" 7 | favorite "github.com/bytedance-youthcamp-jbzx/tiktok/kitex/kitex_gen/favorite" 8 | client "github.com/cloudwego/kitex/client" 9 | callopt "github.com/cloudwego/kitex/client/callopt" 10 | ) 11 | 12 | // Client is designed to provide IDL-compatible methods with call-option parameter for kitex framework. 13 | type Client interface { 14 | FavoriteAction(ctx context.Context, Req *favorite.FavoriteActionRequest, callOptions ...callopt.Option) (r *favorite.FavoriteActionResponse, err error) 15 | FavoriteList(ctx context.Context, Req *favorite.FavoriteListRequest, callOptions ...callopt.Option) (r *favorite.FavoriteListResponse, err error) 16 | } 17 | 18 | // NewClient creates a client for the service defined in IDL. 19 | func NewClient(destService string, opts ...client.Option) (Client, error) { 20 | var options []client.Option 21 | options = append(options, client.WithDestService(destService)) 22 | 23 | options = append(options, opts...) 24 | 25 | kc, err := client.NewClient(serviceInfo(), options...) 26 | if err != nil { 27 | return nil, err 28 | } 29 | return &kFavoriteServiceClient{ 30 | kClient: newServiceClient(kc), 31 | }, nil 32 | } 33 | 34 | // MustNewClient creates a client for the service defined in IDL. It panics if any error occurs. 35 | func MustNewClient(destService string, opts ...client.Option) Client { 36 | kc, err := NewClient(destService, opts...) 37 | if err != nil { 38 | panic(err) 39 | } 40 | return kc 41 | } 42 | 43 | type kFavoriteServiceClient struct { 44 | *kClient 45 | } 46 | 47 | func (p *kFavoriteServiceClient) FavoriteAction(ctx context.Context, Req *favorite.FavoriteActionRequest, callOptions ...callopt.Option) (r *favorite.FavoriteActionResponse, err error) { 48 | ctx = client.NewCtxWithCallOptions(ctx, callOptions) 49 | return p.kClient.FavoriteAction(ctx, Req) 50 | } 51 | 52 | func (p *kFavoriteServiceClient) FavoriteList(ctx context.Context, Req *favorite.FavoriteListRequest, callOptions ...callopt.Option) (r *favorite.FavoriteListResponse, err error) { 53 | ctx = client.NewCtxWithCallOptions(ctx, callOptions) 54 | return p.kClient.FavoriteList(ctx, Req) 55 | } 56 | -------------------------------------------------------------------------------- /cmd/video/service/upload.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/bytedance-youthcamp-jbzx/tiktok/internal/tool" 7 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/minio" 8 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/zap" 9 | ) 10 | 11 | // uploadVideo 上传视频至 Minio 12 | func uploadVideo(data []byte, videoTitle string) (string, error) { 13 | logger := zap.InitLogger() 14 | 15 | // 将视频数据上传至minio 16 | reader := bytes.NewReader(data) 17 | contentType := "application/mp4" 18 | 19 | uploadSize, err := minio.UploadFileByIO(minio.VideoBucketName, videoTitle, reader, int64(len(data)), contentType) 20 | if err != nil { 21 | logger.Errorf("视频上传至minio失败:%v", err.Error()) 22 | return "", err 23 | } 24 | logger.Infof("视频文件大小为:%v", uploadSize) 25 | fmt.Println("视频文件大小为:", uploadSize) 26 | 27 | // 获取上传文件的路径 28 | playUrl, err := minio.GetFileTemporaryURL(minio.VideoBucketName, videoTitle) 29 | if err != nil { 30 | logger.Errorf("服务器内部错误:视频获取失败:%s", err.Error()) 31 | return "", err 32 | } 33 | logger.Infof("上传视频路径:%v", playUrl) 34 | return playUrl, nil 35 | } 36 | 37 | // uploadCover 截取并上传封面至 Minio 38 | func uploadCover(playUrl string, coverTitle string) error { 39 | logger := zap.InitLogger() 40 | 41 | // 截取第一帧并将图像上传至minio 42 | imgBuffer, err := tool.GetSnapshotImageBuffer(playUrl, 1) 43 | if err != nil { 44 | logger.Errorf("服务器内部错误,封面获取失败:%s", err.Error()) 45 | return err 46 | } 47 | var imgByte []byte 48 | imgBuffer.Write(imgByte) 49 | contentType := "image/png" 50 | 51 | uploadSize, err := minio.UploadFileByIO(minio.CoverBucketName, coverTitle, imgBuffer, int64(imgBuffer.Len()), contentType) 52 | if err != nil { 53 | logger.Errorf("封面上传至minio失败:%v", err.Error()) 54 | return err 55 | } 56 | 57 | // 获取上传文件的路径 58 | coverUrl, err := minio.GetFileTemporaryURL(minio.CoverBucketName, coverTitle) 59 | if err != nil { 60 | logger.Errorf("封面获取链接失败:%v", err.Error()) 61 | return err 62 | } 63 | logger.Infof("上传封面路径:%v", coverUrl) 64 | logger.Infof("封面文件大小为:%v", uploadSize) 65 | fmt.Println("封面文件大小为:", uploadSize) 66 | 67 | return nil 68 | } 69 | 70 | // VideoPublish 上传视频并获取封面 71 | func VideoPublish(data []byte, videoTitle string, coverTitle string) error { 72 | playUrl, err := uploadVideo(data, videoTitle) 73 | if err != nil { 74 | return err 75 | } 76 | err = uploadCover(playUrl, coverTitle) 77 | if err != nil { 78 | return err 79 | } 80 | return nil 81 | } 82 | -------------------------------------------------------------------------------- /dal/db/publish.go: -------------------------------------------------------------------------------- 1 | // 2 | // Package db 3 | // @Description: 数据库数据库操作业务逻辑 4 | // @Author hehehhh 5 | // @Date 2023-01-21 14:33:47 6 | // @Update 7 | // 8 | 9 | package db 10 | 11 | import ( 12 | "context" 13 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/errno" 14 | 15 | "gorm.io/gorm" 16 | "gorm.io/plugin/dbresolver" 17 | ) 18 | 19 | // CreateVideo 20 | // 21 | // @Description: 发布一条视频 22 | // @Date 2023-01-21 16:26:19 23 | // @param ctx 数据库操作上下文 24 | // @param video 视频数据 25 | // @return error 26 | func CreateVideo(ctx context.Context, video *Video) error { 27 | err := GetDB().Clauses(dbresolver.Write).WithContext(ctx).Transaction(func(tx *gorm.DB) error { 28 | // 1. 在 video 表中创建视频记录 29 | err := tx.Create(video).Error 30 | if err != nil { 31 | return err 32 | } 33 | // 2. 同步 user 表中的作品数量 34 | res := tx.Model(&User{}).Where("id = ?", video.AuthorID).Update("work_count", gorm.Expr("work_count + ?", 1)) 35 | if res.Error != nil { 36 | return err 37 | } 38 | if res.RowsAffected != 1 { 39 | return errno.ErrDatabase 40 | } 41 | return nil 42 | }) 43 | 44 | return err 45 | } 46 | 47 | // GetVideosByUserID 48 | // 49 | // @Description: 获取用户发布的视频列表 50 | // @Date 2023-01-21 16:28:44 51 | // @param ctx 数据库操作上下文 52 | // @param authorId 作者的用户id 53 | // @return []*Video 视频列表 54 | // @return error 55 | func GetVideosByUserID(ctx context.Context, authorId int64) ([]*Video, error) { 56 | var pubList []*Video 57 | err := GetDB().Clauses(dbresolver.Read).WithContext(ctx).Model(&Video{}).Where(&Video{AuthorID: uint(authorId)}).Find(&pubList).Error 58 | if err != nil { 59 | return nil, err 60 | } 61 | return pubList, nil 62 | } 63 | 64 | // DelVideoByID 65 | // 66 | // @Description: 根据视频id和作者id删除视频 67 | // @Date 2023-02-22 23:34:45 68 | // @param ctx 数据库操作上下文 69 | // @param videoID 视频id 70 | // @param authorID 作者id 71 | // @return error 72 | func DelVideoByID(ctx context.Context, videoID int64, authorID int64) error { 73 | err := GetDB().Clauses(dbresolver.Read).WithContext(ctx).Transaction(func(tx *gorm.DB) error { 74 | // 1. 根据主键 video_id 删除 video 75 | err := tx.Unscoped().Delete(&Video{}, videoID).Error 76 | if err != nil { 77 | return err 78 | } 79 | // 2. 同步 user 表中的作品数量 80 | res := tx.Model(&User{}).Where("id = ?", authorID).Update("work_count", gorm.Expr("work_count - ?", 1)) 81 | if res.Error != nil { 82 | return err 83 | } 84 | if res.RowsAffected != 1 { 85 | return errno.ErrDatabase 86 | } 87 | return nil 88 | }) 89 | return err 90 | } 91 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | x-minio-common: &minio-common 4 | image: quay.io/minio/minio:v1.0.0 5 | command: server --console-address ":9001" http://minio{1...4}/data{1...2} 6 | expose: 7 | - "9000" 8 | environment: 9 | MINIO_ROOT_USER: tiktokMinio 10 | MINIO_ROOT_PASSWORD: tiktokMinio 11 | healthcheck: 12 | test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] 13 | interval: 30s 14 | timeout: 20s 15 | retries: 3 16 | 17 | services: 18 | Etcd: 19 | image: 'bitnami/etcd:v1.0.0' 20 | environment: 21 | - ALLOW_NONE_AUTHENTICATION=yes 22 | ports: 23 | - 2379:2380 24 | 25 | dousheng-api: 26 | image: '1.12.68.184:5000/dousheng-api-hertz:v1.0.0' 27 | volumes: 28 | - type: bind 29 | source: ./config 30 | target: /app/config 31 | ports: 32 | - 8089:8089 33 | - 8081:8081 34 | - 8082:8082 35 | - 8083:8083 36 | - 8084:8084 37 | - 8085:8085 38 | - 8086:8086 39 | 40 | dousheng-rpc-commentsrv: 41 | image: '1.12.68.184:5000/dousheng-rpc-commentsrv:v1.0.0' 42 | network_mode: 'service:dousheng-api' 43 | volumes: 44 | - type: bind 45 | source: ./config 46 | target: /app/config 47 | 48 | dousheng-rpc-messagesrv: 49 | image: '1.12.68.184:5000/dousheng-rpc-messagesrv:v1.0.0' 50 | network_mode: 'service:dousheng-api' 51 | volumes: 52 | - type: bind 53 | source: ./config 54 | target: /app/config 55 | 56 | dousheng-rpc-relationsrv: 57 | image: '1.12.68.184:5000/dousheng-rpc-relationsrv:v1.0.0' 58 | network_mode: 'service:dousheng-api' 59 | volumes: 60 | - type: bind 61 | source: ./config 62 | target: /app/config 63 | 64 | dousheng-rpc-videosrv: 65 | image: '1.12.68.184:5000/dousheng-rpc-videosrv:v1.0.0' 66 | network_mode: 'service:dousheng-api' 67 | volumes: 68 | - type: bind 69 | source: ./config 70 | target: /app/config 71 | 72 | dousheng-rpc-usersrv: 73 | image: '1.12.68.184:5000/dousheng-rpc-usersrv:v1.0.0' 74 | network_mode: 'service:dousheng-api' 75 | volumes: 76 | - type: bind 77 | source: ./config 78 | target: /app/config 79 | 80 | dousheng-rpc-favoritesrv: 81 | image: '1.12.68.184:5000/dousheng-rpc-favoritesrv:v1.0.0' 82 | 83 | network_mode: 'service:dousheng-api' 84 | volumes: 85 | - type: bind 86 | source: ./config 87 | target: /app/config 88 | -------------------------------------------------------------------------------- /cmd/api/handler/favorite.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "context" 5 | "github.com/cloudwego/hertz/pkg/app" 6 | "net/http" 7 | "strconv" 8 | 9 | "github.com/bytedance-youthcamp-jbzx/tiktok/cmd/api/rpc" 10 | "github.com/bytedance-youthcamp-jbzx/tiktok/internal/response" 11 | kitex "github.com/bytedance-youthcamp-jbzx/tiktok/kitex/kitex_gen/favorite" 12 | ) 13 | 14 | func FavoriteAction(ctx context.Context, c *app.RequestContext) { 15 | token := c.Query("token") 16 | actionType, err := strconv.ParseInt(c.Query("action_type"), 10, 64) 17 | if err != nil || (actionType != 1 && actionType != 2) { 18 | c.JSON(http.StatusOK, response.FavoriteAction{ 19 | Base: response.Base{ 20 | StatusCode: -1, 21 | StatusMsg: "action_type 不合法", 22 | }, 23 | }) 24 | return 25 | } 26 | vid, err := strconv.ParseInt(c.Query("video_id"), 10, 64) 27 | if err != nil { 28 | c.JSON(http.StatusOK, response.FavoriteAction{ 29 | Base: response.Base{ 30 | StatusCode: -1, 31 | StatusMsg: "video_id 不合法", 32 | }, 33 | }) 34 | return 35 | } 36 | req := &kitex.FavoriteActionRequest{ 37 | Token: token, 38 | VideoId: vid, 39 | ActionType: int32(actionType), 40 | } 41 | res, _ := rpc.FavoriteAction(ctx, req) 42 | if res.StatusCode == -1 { 43 | c.JSON(http.StatusOK, response.FavoriteAction{ 44 | Base: response.Base{ 45 | StatusCode: -1, 46 | StatusMsg: res.StatusMsg, 47 | }, 48 | }) 49 | return 50 | } 51 | c.JSON(http.StatusOK, response.FavoriteAction{ 52 | Base: response.Base{ 53 | StatusCode: 0, 54 | StatusMsg: res.StatusMsg, 55 | }, 56 | }) 57 | } 58 | 59 | func FavoriteList(ctx context.Context, c *app.RequestContext) { 60 | token := c.Query("token") 61 | 62 | uid, err := strconv.ParseInt(c.Query("user_id"), 10, 64) 63 | if err != nil { 64 | c.JSON(http.StatusOK, response.FavoriteList{ 65 | Base: response.Base{ 66 | StatusCode: -1, 67 | StatusMsg: "user_id 不合法", 68 | }, 69 | }) 70 | return 71 | } 72 | 73 | req := &kitex.FavoriteListRequest{ 74 | UserId: uid, 75 | Token: token, 76 | } 77 | res, _ := rpc.FavoriteList(ctx, req) 78 | if res.StatusCode == -1 { 79 | c.JSON(http.StatusOK, response.FavoriteList{ 80 | Base: response.Base{ 81 | StatusCode: -1, 82 | StatusMsg: res.StatusMsg, 83 | }, 84 | }) 85 | return 86 | } 87 | c.JSON(http.StatusOK, response.FavoriteList{ 88 | Base: response.Base{ 89 | StatusCode: 0, 90 | StatusMsg: res.StatusMsg, 91 | }, 92 | VideoList: res.VideoList, 93 | }) 94 | } 95 | -------------------------------------------------------------------------------- /kitex/kitex_gen/user/userservice/client.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.4.4. DO NOT EDIT. 2 | 3 | package userservice 4 | 5 | import ( 6 | "context" 7 | user "github.com/bytedance-youthcamp-jbzx/tiktok/kitex/kitex_gen/user" 8 | client "github.com/cloudwego/kitex/client" 9 | callopt "github.com/cloudwego/kitex/client/callopt" 10 | ) 11 | 12 | // Client is designed to provide IDL-compatible methods with call-option parameter for kitex framework. 13 | type Client interface { 14 | Register(ctx context.Context, Req *user.UserRegisterRequest, callOptions ...callopt.Option) (r *user.UserRegisterResponse, err error) 15 | Login(ctx context.Context, Req *user.UserLoginRequest, callOptions ...callopt.Option) (r *user.UserLoginResponse, err error) 16 | UserInfo(ctx context.Context, Req *user.UserInfoRequest, callOptions ...callopt.Option) (r *user.UserInfoResponse, err error) 17 | } 18 | 19 | // NewClient creates a client for the service defined in IDL. 20 | func NewClient(destService string, opts ...client.Option) (Client, error) { 21 | var options []client.Option 22 | options = append(options, client.WithDestService(destService)) 23 | 24 | options = append(options, opts...) 25 | 26 | kc, err := client.NewClient(serviceInfo(), options...) 27 | if err != nil { 28 | return nil, err 29 | } 30 | return &kUserServiceClient{ 31 | kClient: newServiceClient(kc), 32 | }, nil 33 | } 34 | 35 | // MustNewClient creates a client for the service defined in IDL. It panics if any error occurs. 36 | func MustNewClient(destService string, opts ...client.Option) Client { 37 | kc, err := NewClient(destService, opts...) 38 | if err != nil { 39 | panic(err) 40 | } 41 | return kc 42 | } 43 | 44 | type kUserServiceClient struct { 45 | *kClient 46 | } 47 | 48 | func (p *kUserServiceClient) Register(ctx context.Context, Req *user.UserRegisterRequest, callOptions ...callopt.Option) (r *user.UserRegisterResponse, err error) { 49 | ctx = client.NewCtxWithCallOptions(ctx, callOptions) 50 | return p.kClient.Register(ctx, Req) 51 | } 52 | 53 | func (p *kUserServiceClient) Login(ctx context.Context, Req *user.UserLoginRequest, callOptions ...callopt.Option) (r *user.UserLoginResponse, err error) { 54 | ctx = client.NewCtxWithCallOptions(ctx, callOptions) 55 | return p.kClient.Login(ctx, Req) 56 | } 57 | 58 | func (p *kUserServiceClient) UserInfo(ctx context.Context, Req *user.UserInfoRequest, callOptions ...callopt.Option) (r *user.UserInfoResponse, err error) { 59 | ctx = client.NewCtxWithCallOptions(ctx, callOptions) 60 | return p.kClient.UserInfo(ctx, Req) 61 | } 62 | -------------------------------------------------------------------------------- /kitex/kitex_gen/video/videoservice/client.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.4.4. DO NOT EDIT. 2 | 3 | package videoservice 4 | 5 | import ( 6 | "context" 7 | video "github.com/bytedance-youthcamp-jbzx/tiktok/kitex/kitex_gen/video" 8 | client "github.com/cloudwego/kitex/client" 9 | callopt "github.com/cloudwego/kitex/client/callopt" 10 | ) 11 | 12 | // Client is designed to provide IDL-compatible methods with call-option parameter for kitex framework. 13 | type Client interface { 14 | Feed(ctx context.Context, Req *video.FeedRequest, callOptions ...callopt.Option) (r *video.FeedResponse, err error) 15 | PublishAction(ctx context.Context, Req *video.PublishActionRequest, callOptions ...callopt.Option) (r *video.PublishActionResponse, err error) 16 | PublishList(ctx context.Context, Req *video.PublishListRequest, callOptions ...callopt.Option) (r *video.PublishListResponse, err error) 17 | } 18 | 19 | // NewClient creates a client for the service defined in IDL. 20 | func NewClient(destService string, opts ...client.Option) (Client, error) { 21 | var options []client.Option 22 | options = append(options, client.WithDestService(destService)) 23 | 24 | options = append(options, opts...) 25 | 26 | kc, err := client.NewClient(serviceInfo(), options...) 27 | if err != nil { 28 | return nil, err 29 | } 30 | return &kVideoServiceClient{ 31 | kClient: newServiceClient(kc), 32 | }, nil 33 | } 34 | 35 | // MustNewClient creates a client for the service defined in IDL. It panics if any error occurs. 36 | func MustNewClient(destService string, opts ...client.Option) Client { 37 | kc, err := NewClient(destService, opts...) 38 | if err != nil { 39 | panic(err) 40 | } 41 | return kc 42 | } 43 | 44 | type kVideoServiceClient struct { 45 | *kClient 46 | } 47 | 48 | func (p *kVideoServiceClient) Feed(ctx context.Context, Req *video.FeedRequest, callOptions ...callopt.Option) (r *video.FeedResponse, err error) { 49 | ctx = client.NewCtxWithCallOptions(ctx, callOptions) 50 | return p.kClient.Feed(ctx, Req) 51 | } 52 | 53 | func (p *kVideoServiceClient) PublishAction(ctx context.Context, Req *video.PublishActionRequest, callOptions ...callopt.Option) (r *video.PublishActionResponse, err error) { 54 | ctx = client.NewCtxWithCallOptions(ctx, callOptions) 55 | return p.kClient.PublishAction(ctx, Req) 56 | } 57 | 58 | func (p *kVideoServiceClient) PublishList(ctx context.Context, Req *video.PublishListRequest, callOptions ...callopt.Option) (r *video.PublishListResponse, err error) { 59 | ctx = client.NewCtxWithCallOptions(ctx, callOptions) 60 | return p.kClient.PublishList(ctx, Req) 61 | } 62 | -------------------------------------------------------------------------------- /cmd/api/rpc/relation.go: -------------------------------------------------------------------------------- 1 | // Package rpc /* 2 | package rpc 3 | 4 | import ( 5 | "context" 6 | "fmt" 7 | "time" 8 | 9 | relation "github.com/bytedance-youthcamp-jbzx/tiktok/kitex/kitex_gen/relation" 10 | "github.com/bytedance-youthcamp-jbzx/tiktok/kitex/kitex_gen/relation/relationservice" 11 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/etcd" 12 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/middleware" 13 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/viper" 14 | "github.com/cloudwego/kitex/client" 15 | "github.com/cloudwego/kitex/pkg/retry" 16 | "github.com/cloudwego/kitex/pkg/rpcinfo" 17 | ) 18 | 19 | var ( 20 | relationClient relationservice.Client 21 | ) 22 | 23 | func InitRelation(config *viper.Config) { 24 | etcdAddr := fmt.Sprintf("%s:%d", config.Viper.GetString("etcd.host"), config.Viper.GetInt("etcd.port")) 25 | serviceName := config.Viper.GetString("server.name") 26 | r, err := etcd.NewEtcdResolver([]string{etcdAddr}) 27 | if err != nil { 28 | panic(err) 29 | } 30 | 31 | c, err := relationservice.NewClient( 32 | serviceName, 33 | client.WithMiddleware(middleware.CommonMiddleware), 34 | client.WithInstanceMW(middleware.ClientMiddleware), 35 | client.WithMuxConnection(1), // mux 36 | client.WithRPCTimeout(30*time.Second), // rpc timeout 37 | client.WithConnectTimeout(30000*time.Millisecond), // conn timeout 38 | client.WithFailureRetry(retry.NewFailurePolicy()), // retry 39 | //client.WithSuite(tracing.NewClientSuite()), // tracer 40 | client.WithResolver(r), // resolver 41 | // Please keep the same as provider.WithServiceName 42 | client.WithClientBasicInfo(&rpcinfo.EndpointBasicInfo{ServiceName: serviceName}), 43 | ) 44 | if err != nil { 45 | panic(err) 46 | } 47 | relationClient = c 48 | } 49 | 50 | func RelationAction(ctx context.Context, req *relation.RelationActionRequest) (*relation.RelationActionResponse, error) { 51 | return relationClient.RelationAction(ctx, req) 52 | } 53 | 54 | func RelationFollowList(ctx context.Context, req *relation.RelationFollowListRequest) (*relation.RelationFollowListResponse, error) { 55 | return relationClient.RelationFollowList(ctx, req) 56 | } 57 | 58 | func RelationFollowerList(ctx context.Context, req *relation.RelationFollowerListRequest) (*relation.RelationFollowerListResponse, error) { 59 | return relationClient.RelationFollowerList(ctx, req) 60 | } 61 | 62 | func RelationFriendList(ctx context.Context, req *relation.RelationFriendListRequest) (*relation.RelationFriendListResponse, error) { 63 | return relationClient.RelationFriendList(ctx, req) 64 | } 65 | -------------------------------------------------------------------------------- /dal/db/init.go: -------------------------------------------------------------------------------- 1 | // Package db /* 2 | package db 3 | 4 | import ( 5 | "fmt" 6 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/zap" 7 | "gorm.io/gorm/logger" 8 | "time" 9 | 10 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/viper" 11 | "gorm.io/driver/mysql" 12 | "gorm.io/gorm" 13 | "gorm.io/plugin/dbresolver" 14 | ) 15 | 16 | var ( 17 | _db *gorm.DB 18 | config = viper.Init("db") 19 | zapLogger = zap.InitLogger() 20 | ) 21 | 22 | func getDsn(driverWithRole string) string { 23 | username := config.Viper.GetString(fmt.Sprintf("%s.username", driverWithRole)) 24 | password := config.Viper.GetString(fmt.Sprintf("%s.password", driverWithRole)) 25 | host := config.Viper.GetString(fmt.Sprintf("%s.host", driverWithRole)) 26 | port := config.Viper.GetInt(fmt.Sprintf("%s.port", driverWithRole)) 27 | Dbname := config.Viper.GetString(fmt.Sprintf("%s.database", driverWithRole)) 28 | 29 | // data source name 30 | dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", username, password, host, port, Dbname) 31 | 32 | return dsn 33 | } 34 | 35 | func init() { 36 | zapLogger.Info("Redis server connection successful!") 37 | 38 | dsn1 := getDsn("mysql.source") 39 | 40 | var err error 41 | _db, err = gorm.Open(mysql.Open(dsn1), &gorm.Config{ 42 | Logger: logger.Default.LogMode(logger.Info), 43 | PrepareStmt: true, 44 | SkipDefaultTransaction: true, 45 | }) 46 | if err != nil { 47 | panic(err.Error()) 48 | } 49 | 50 | dsn2 := getDsn("mysql.replica1") 51 | dsn3 := getDsn("mysql.replica2") 52 | // 配置dbresolver 53 | _db.Use(dbresolver.Register(dbresolver.Config{ 54 | // use `db1` as sources, `db2` as replicas 55 | Sources: []gorm.Dialector{mysql.Open(dsn1)}, 56 | Replicas: []gorm.Dialector{mysql.Open(dsn2), mysql.Open(dsn3)}, 57 | // sources/replicas load balancing policy 58 | Policy: dbresolver.RandomPolicy{}, 59 | // print sources/replicas mode in logger 60 | TraceResolverMode: false, 61 | })) 62 | // AutoMigrate会创建表,缺失的外键,约束,列和索引。如果大小,精度,是否为空,可以更改,则AutoMigrate会改变列的类型。出于保护您数据的目的,它不会删除未使用的列 63 | // 刷新数据库的表格,使其保持最新。即如果我在旧表的基础上增加一个字段age,那么调用autoMigrate后,旧表会自动多出一列age,值为空 64 | if err := _db.AutoMigrate(&User{}, &Video{}, &Comment{}, &FavoriteVideoRelation{}, &FollowRelation{}, &Message{}, &FavoriteCommentRelation{}); err != nil { 65 | zapLogger.Fatalln(err.Error()) 66 | } 67 | 68 | db, err := _db.DB() 69 | if err != nil { 70 | zapLogger.Fatalln(err.Error()) 71 | } 72 | db.SetMaxOpenConns(1000) 73 | db.SetMaxIdleConns(20) 74 | db.SetConnMaxLifetime(60 * time.Minute) 75 | } 76 | 77 | func GetDB() *gorm.DB { 78 | return _db 79 | } 80 | -------------------------------------------------------------------------------- /kitex/relation.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | option go_package = "relation"; 3 | package relation; 4 | 5 | import "user.proto"; 6 | // ============================关注or取消关注=================================== 7 | message RelationActionRequest { 8 | // int64 user_id = 1; // 用户id 9 | string token = 1; // 用户鉴权token 10 | int64 to_user_id = 2; // 对方用户id 11 | int32 action_type = 3; // 1-关注,2-取消关注 12 | } 13 | message RelationActionResponse { 14 | int32 status_code = 1; // 状态码,0-成功,其他值-失败 15 | string status_msg = 2; // 返回状态描述 16 | } 17 | 18 | // ==============================关注列表======================================== 19 | message RelationFollowListRequest { 20 | int64 user_id = 1; // 用户id 21 | string token = 2; // 用户鉴权token 22 | } 23 | message RelationFollowListResponse { 24 | int32 status_code = 1; // 状态码,0-成功,其他值-失败 25 | string status_msg = 2; // 返回状态描述 26 | repeated user.User user_list = 3; // 用户信息列表 27 | } 28 | 29 | // ==============================粉丝列表======================================= 30 | message RelationFollowerListRequest { 31 | int64 user_id = 1; // 用户id 32 | string token = 2; // 用户鉴权token 33 | } 34 | message RelationFollowerListResponse { 35 | int32 status_code = 1; // 状态码,0-成功,其他值-失败 36 | string status_msg = 2; // 返回状态描述 37 | repeated user.User user_list = 3; // 用户列表 38 | } 39 | 40 | // ==============================好友列表======================================= 41 | message RelationFriendListRequest{ 42 | int64 user_id = 1; // 用户id 43 | string token = 2; // 用户鉴权token 44 | } 45 | message RelationFriendListResponse{ 46 | int32 status_code = 1; // 状态码,0-成功,其他值-失败 47 | string status_msg = 2; // 返回状态描述 48 | repeated FriendUser user_list = 3; // 用户列表 49 | } 50 | 51 | message FriendUser { 52 | string message = 1; // 和该好友的最新聊天消息 53 | int64 msgType = 2; // message消息的类型,0 => 当前请求用户接收的消息, 1 => 当前请求用户发送的消息 54 | int64 id = 3; // 用户id 55 | string name = 4; // 用户名称 56 | int64 follow_count = 5; // 关注总数 57 | int64 follower_count = 6; // 粉丝总数 58 | bool is_follow = 7; //true-已关注,false-未关注 59 | string avatar = 8; // 用户头像 60 | string background_image = 9; // 用户个人页顶部大图 61 | string signature = 10; // 个人简介 62 | int64 total_favorited = 11; // 获赞数量 63 | int64 work_count = 12; // 作品数量 64 | int64 favorite_count = 13; // 点赞数量 65 | } 66 | 67 | service RelationService { 68 | rpc RelationAction(RelationActionRequest)returns(RelationActionResponse); 69 | rpc RelationFollowList(RelationFollowListRequest)returns(RelationFollowListResponse); 70 | rpc RelationFollowerList(RelationFollowerListRequest)returns(RelationFollowerListResponse); 71 | rpc RelationFriendList(RelationFriendListRequest)returns(RelationFriendListResponse); 72 | } -------------------------------------------------------------------------------- /internal/tool/crypt.go: -------------------------------------------------------------------------------- 1 | package tool 2 | 3 | import ( 4 | "crypto/md5" 5 | "crypto/rand" 6 | "crypto/rsa" 7 | "crypto/sha256" 8 | "crypto/x509" 9 | "encoding/base64" 10 | "encoding/hex" 11 | "encoding/pem" 12 | "errors" 13 | "fmt" 14 | "github.com/bytedance-youthcamp-jbzx/dousheng/pkg/viper" 15 | "io/ioutil" 16 | "os" 17 | ) 18 | 19 | // 提供各种加密算法 20 | const ( 21 | base64Table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" 22 | ) 23 | 24 | var ( 25 | coder = base64.NewEncoding(base64Table) 26 | config = viper.Init("crypt") 27 | PublicKeyFilePath = config.Viper.GetString("rsa.tiktok_message_encrypt_public_key") 28 | PrivateKeyFilePath = config.Viper.GetString("rsa.tiktok_message_decrypt_private_key") 29 | ) 30 | 31 | func Base64Encode(src []byte) []byte { 32 | return []byte(coder.EncodeToString(src)) 33 | } 34 | 35 | func Base64Decode(src []byte) ([]byte, error) { 36 | return coder.DecodeString(string(src)) 37 | } 38 | 39 | func RsaEncrypt(origData []byte, publicKey string) ([]byte, error) { 40 | block, _ := pem.Decode([]byte(publicKey)) 41 | if block == nil { 42 | return nil, errors.New("public key error") 43 | } 44 | pubInterface, err := x509.ParsePKIXPublicKey(block.Bytes) 45 | if err != nil { 46 | return nil, err 47 | } 48 | pub := pubInterface.(*rsa.PublicKey) 49 | return rsa.EncryptPKCS1v15(rand.Reader, pub, origData) 50 | } 51 | 52 | func RsaDecrypt(ciphertext []byte, privateKey string) ([]byte, error) { 53 | block, _ := pem.Decode([]byte(privateKey)) 54 | if block == nil { 55 | return nil, errors.New("private key error") 56 | } 57 | priv, err := x509.ParsePKCS8PrivateKey(block.Bytes) 58 | if err != nil { 59 | return nil, err 60 | } 61 | return rsa.DecryptPKCS1v15(rand.Reader, priv.(*rsa.PrivateKey), ciphertext) 62 | } 63 | 64 | func Sha256Encrypt(data string, salt string) string { 65 | sha256Ctx := sha256.New() //sha256 init 66 | sha256Ctx.Write([]byte(data + salt)) //sha256 updata 67 | cipherStr := sha256.Sum256(nil) //sha256 final 68 | return fmt.Sprintf("%x", cipherStr) 69 | } 70 | 71 | func Md5Encrypt(data string) string { 72 | md5Ctx := md5.New() //md5 init 73 | md5Ctx.Write([]byte(data)) //md5 updata 74 | cipherStr := md5Ctx.Sum(nil) //md5 final 75 | encryptedData := hex.EncodeToString(cipherStr) //hex_digest 76 | return encryptedData 77 | } 78 | 79 | func ReadKeyFromFile(filePath string) (string, error) { 80 | file, err := os.Open(filePath) 81 | if err != nil { 82 | return "", err 83 | } 84 | 85 | data, err := ioutil.ReadAll(file) 86 | if err != nil { 87 | return "", err 88 | } 89 | 90 | if err != nil { 91 | return "", err 92 | } 93 | 94 | return string(data), nil 95 | } 96 | -------------------------------------------------------------------------------- /dal/redis/init.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/go-redsync/redsync/v4" 7 | "github.com/go-redsync/redsync/v4/redis/goredis/v9" 8 | "sync" 9 | "time" 10 | 11 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/viper" 12 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/zap" 13 | "github.com/redis/go-redis/v9" 14 | ) 15 | 16 | const ExpireTime = 6 * time.Second 17 | 18 | var ( 19 | config = viper.Init("db") 20 | zapLogger = zap.InitLogger() 21 | redisOnce sync.Once 22 | redisHelper *RedisHelper 23 | FavoriteMutex *redsync.Mutex 24 | RelationMutex *redsync.Mutex 25 | ) 26 | 27 | type RedisHelper struct { 28 | *redis.Client 29 | } 30 | 31 | func GetRedisHelper() *RedisHelper { 32 | return redisHelper 33 | } 34 | 35 | // LockByMutex Obtain a lock for our given mutex. After this is successful, no one else can obtain the same lock (the same mutex name) until we unlock it. 36 | func LockByMutex(ctx context.Context, mutex *redsync.Mutex) error { 37 | if err := mutex.LockContext(ctx); err != nil { 38 | return err 39 | } 40 | return nil 41 | } 42 | 43 | // UnlockByMutex Release the lock so other processes or threads can obtain a lock. 44 | func UnlockByMutex(ctx context.Context, mutex *redsync.Mutex) error { 45 | if _, err := mutex.UnlockContext(ctx); err != nil { 46 | return err 47 | } 48 | return nil 49 | } 50 | 51 | func NewRedisHelper() *redis.Client { 52 | rdb := redis.NewClient(&redis.Options{ 53 | Addr: fmt.Sprintf("%s:%s", config.Viper.GetString("redis.addr"), config.Viper.GetString("redis.port")), 54 | Password: config.Viper.GetString("redis.password"), 55 | DB: config.Viper.GetInt("redis.db"), 56 | DialTimeout: 10 * time.Second, 57 | ReadTimeout: 30 * time.Second, 58 | WriteTimeout: 30 * time.Second, 59 | //MaxConnAge: 1 * time.Minute, go-redis v9 已删去 60 | PoolSize: 10, 61 | PoolTimeout: 30 * time.Second, 62 | }) 63 | 64 | redisOnce.Do(func() { 65 | rdh := new(RedisHelper) 66 | rdh.Client = rdb 67 | redisHelper = rdh 68 | }) 69 | return rdb 70 | } 71 | 72 | func init() { 73 | ctx := context.Background() 74 | rdb := NewRedisHelper() 75 | if _, err := rdb.Ping(ctx).Result(); err != nil { 76 | zapLogger.Fatalln(err.Error()) 77 | return 78 | } 79 | zapLogger.Info("Redis server connection successful!") 80 | 81 | // 开启定时同步至数据库 82 | GoCronFavorite() 83 | GoCronRelation() 84 | zapLogger.Info("MySQL synchronization is enabled.") 85 | 86 | // Redis锁 87 | // 创建Redis连接池 88 | pool := goredis.NewPool(rdb) 89 | // Create an instance of redisync to be used to obtain a mutual exclusion lock. 90 | rs := redsync.New(pool) 91 | // Obtain a new mutex by using the same name for all instances wanting the same lock. 92 | FavoriteMutex = rs.NewMutex("mutex-favorite") 93 | RelationMutex = rs.NewMutex("mutex-relation") 94 | } 95 | -------------------------------------------------------------------------------- /pkg/etcd/discovery.go: -------------------------------------------------------------------------------- 1 | package etcd 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | 8 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/zap" 9 | "github.com/cloudwego/kitex/pkg/discovery" 10 | "github.com/cloudwego/kitex/pkg/rpcinfo" 11 | clientv3 "go.etcd.io/etcd/client/v3" 12 | ) 13 | 14 | const ( 15 | defaultWeight = 10 16 | ) 17 | 18 | // etcdResolver is a resolver using etcd. 19 | type etcdResolver struct { 20 | etcdClient *clientv3.Client 21 | } 22 | 23 | // NewEtcdResolver creates a etcd based resolver. 24 | func NewEtcdResolver(endpoints []string) (discovery.Resolver, error) { 25 | return NewEtcdResolverWithAuth(endpoints, "", "") 26 | } 27 | 28 | // NewEtcdResolverWithAuth creates a etcd based resolver with given username and password. 29 | func NewEtcdResolverWithAuth(endpoints []string, username, password string) (discovery.Resolver, error) { 30 | etcdClient, err := clientv3.New(clientv3.Config{ 31 | Endpoints: endpoints, 32 | Username: username, 33 | Password: password, 34 | }) 35 | if err != nil { 36 | return nil, err 37 | } 38 | return &etcdResolver{ 39 | etcdClient: etcdClient, 40 | }, nil 41 | } 42 | 43 | // Target implements the Resolver interface. 44 | func (e *etcdResolver) Target(ctx context.Context, target rpcinfo.EndpointInfo) (description string) { 45 | return target.ServiceName() 46 | } 47 | 48 | // Resolve implements the Resolver interface. 49 | func (e *etcdResolver) Resolve(ctx context.Context, desc string) (discovery.Result, error) { 50 | logger := zap.InitLogger() 51 | prefix := serviceKeyPrefix(desc) 52 | resp, err := e.etcdClient.Get(ctx, prefix, clientv3.WithPrefix()) 53 | if err != nil { 54 | return discovery.Result{}, err 55 | } 56 | var ( 57 | info instanceInfo 58 | eps []discovery.Instance 59 | ) 60 | for _, kv := range resp.Kvs { 61 | err := json.Unmarshal(kv.Value, &info) 62 | if err != nil { 63 | //klog.Warnf("fail to unmarshal with err: %v, ignore key: %v", err, string(kv.Key)) 64 | logger.Warnf("fail to unmarshal with err: %v, ignore key: %v", err, string(kv.Key)) 65 | continue 66 | } 67 | weight := info.Weight 68 | if weight <= 0 { 69 | weight = defaultWeight 70 | } 71 | eps = append(eps, discovery.NewInstance(info.Network, info.Address, weight, info.Tags)) 72 | } 73 | if len(eps) == 0 { 74 | return discovery.Result{}, fmt.Errorf("no instance remains for %v", desc) 75 | } 76 | return discovery.Result{ 77 | Cacheable: true, 78 | CacheKey: desc, 79 | Instances: eps, 80 | }, nil 81 | } 82 | 83 | // Diff implements the Resolver interface. 84 | func (e *etcdResolver) Diff(cacheKey string, prev, next discovery.Result) (discovery.Change, bool) { 85 | return discovery.DefaultDiff(cacheKey, prev, next) 86 | } 87 | 88 | // Name implements the Resolver interface. 89 | func (e *etcdResolver) Name() string { 90 | return "etcd" 91 | } 92 | -------------------------------------------------------------------------------- /cmd/api/handler/message.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "context" 5 | "github.com/cloudwego/hertz/pkg/app" 6 | "net/http" 7 | "strconv" 8 | 9 | "github.com/bytedance-youthcamp-jbzx/tiktok/cmd/api/rpc" 10 | "github.com/bytedance-youthcamp-jbzx/tiktok/internal/response" 11 | kitex "github.com/bytedance-youthcamp-jbzx/tiktok/kitex/kitex_gen/message" 12 | ) 13 | 14 | func MessageChat(ctx context.Context, c *app.RequestContext) { 15 | token := c.Query("token") 16 | toUserID, err := strconv.ParseInt(c.Query("to_user_id"), 10, 64) 17 | if err != nil { 18 | c.JSON(http.StatusOK, response.MessageChat{ 19 | Base: response.Base{ 20 | StatusCode: -1, 21 | StatusMsg: "to_user_id 不合法", 22 | }, 23 | MessageList: nil, 24 | }) 25 | return 26 | } 27 | 28 | // 调用kitex/kitex_gen 29 | req := &kitex.MessageChatRequest{ 30 | Token: token, 31 | ToUserId: toUserID, 32 | } 33 | res, _ := rpc.MessageChat(ctx, req) 34 | if res.StatusCode == -1 { 35 | c.JSON(http.StatusOK, response.MessageChat{ 36 | Base: response.Base{ 37 | StatusCode: -1, 38 | StatusMsg: res.StatusMsg, 39 | }, 40 | MessageList: nil, 41 | }) 42 | return 43 | } 44 | c.JSON(http.StatusOK, response.MessageChat{ 45 | Base: response.Base{ 46 | StatusCode: 0, 47 | StatusMsg: res.StatusMsg, 48 | }, 49 | MessageList: res.MessageList, 50 | }) 51 | } 52 | 53 | func MessageAction(ctx context.Context, c *app.RequestContext) { 54 | token := c.Query("token") 55 | if token == "" { 56 | c.JSON(http.StatusOK, response.RelationAction{ 57 | Base: response.Base{ 58 | StatusCode: -1, 59 | StatusMsg: "Token has expired.", 60 | }, 61 | }) 62 | return 63 | } 64 | 65 | toUserID, err := strconv.ParseInt(c.Query("to_user_id"), 10, 64) 66 | if err != nil { 67 | c.JSON(http.StatusOK, response.MessageAction{ 68 | Base: response.Base{ 69 | StatusCode: -1, 70 | StatusMsg: "to_user_id 不合法", 71 | }, 72 | }) 73 | return 74 | } 75 | actionType, err := strconv.ParseInt(c.Query("action_type"), 10, 64) 76 | if err != nil || actionType != 1 { 77 | c.JSON(http.StatusOK, response.MessageAction{ 78 | Base: response.Base{ 79 | StatusCode: -1, 80 | StatusMsg: "action_type 不合法", 81 | }, 82 | }) 83 | return 84 | } 85 | 86 | if len(c.Query("content")) == 0 { 87 | c.JSON(http.StatusOK, response.MessageAction{ 88 | Base: response.Base{ 89 | StatusCode: -1, 90 | StatusMsg: "参数 content 不能为空", 91 | }, 92 | }) 93 | return 94 | } 95 | 96 | // 调用kitex/kitex_gen 97 | req := &kitex.MessageActionRequest{ 98 | Token: token, 99 | ToUserId: toUserID, 100 | ActionType: int32(actionType), 101 | Content: c.Query("content"), 102 | } 103 | res, _ := rpc.MessageAction(ctx, req) 104 | if res.StatusCode == -1 { 105 | c.JSON(http.StatusOK, response.MessageAction{ 106 | Base: response.Base{ 107 | StatusCode: -1, 108 | StatusMsg: res.StatusMsg, 109 | }, 110 | }) 111 | return 112 | } 113 | c.JSON(http.StatusOK, response.MessageAction{ 114 | Base: response.Base{ 115 | StatusCode: 0, 116 | StatusMsg: res.StatusMsg, 117 | }, 118 | }) 119 | } 120 | -------------------------------------------------------------------------------- /dal/db/feed.go: -------------------------------------------------------------------------------- 1 | // 2 | // Package db 3 | // @Description: 数据库数据库操作业务逻辑 4 | // @Author hehehhh 5 | // @Date 2023-01-21 14:33:47 6 | // @Update 7 | // 8 | 9 | package db 10 | 11 | import ( 12 | "context" 13 | "time" 14 | 15 | "gorm.io/gorm" 16 | "gorm.io/plugin/dbresolver" 17 | ) 18 | 19 | // Video 20 | // 21 | // @Description: 视频数据模型 22 | type Video struct { 23 | ID uint `gorm:"primarykey"` 24 | CreatedAt time.Time `gorm:"not null;index:idx_create" json:"created_at,omitempty"` 25 | UpdatedAt time.Time 26 | DeletedAt gorm.DeletedAt `gorm:"index"` 27 | Author User `gorm:"foreignkey:AuthorID" json:"author,omitempty"` 28 | AuthorID uint `gorm:"index:idx_authorid;not null" json:"author_id,omitempty"` 29 | PlayUrl string `gorm:"type:varchar(255);not null" json:"play_url,omitempty"` 30 | CoverUrl string `gorm:"type:varchar(255)" json:"cover_url,omitempty"` 31 | FavoriteCount uint `gorm:"default:0;not null" json:"favorite_count,omitempty"` 32 | CommentCount uint `gorm:"default:0;not null" json:"comment_count,omitempty"` 33 | Title string `gorm:"type:varchar(50);not null" json:"title,omitempty"` 34 | } 35 | 36 | func (Video) TableName() string { 37 | return "videos" 38 | } 39 | 40 | // MGetVideos 41 | // 42 | // @Description: 获取最近发布的视频 43 | // @Date 2023-01-21 16:39:00 44 | // @param ctx 45 | // @param limit 获取的视频条数 46 | // @param latestTime 最早的时间限制 47 | // @return []*Video 视频列表 48 | // @return error 49 | func MGetVideos(ctx context.Context, limit int, latestTime *int64) ([]*Video, error) { 50 | videos := make([]*Video, 0) 51 | 52 | if latestTime == nil || *latestTime == 0 { 53 | curTime := time.Now().UnixMilli() 54 | latestTime = &curTime 55 | } 56 | conn := GetDB().Clauses(dbresolver.Read).WithContext(ctx) 57 | if err := conn.Limit(limit).Order("created_at desc").Find(&videos, "created_at < ?", time.UnixMilli(*latestTime)).Error; err != nil { 58 | return nil, err 59 | } 60 | return videos, nil 61 | } 62 | 63 | // GetVideoById 64 | // 65 | // @Description: 根据视频id获取视频 66 | // @Date 2023-01-24 15:58:52 67 | // @param ctx 数据库操作上下文 68 | // @param videoID 视频id 69 | // @return *Video 视频数据 70 | // @return error 71 | func GetVideoById(ctx context.Context, videoID int64) (*Video, error) { 72 | res := new(Video) 73 | if err := GetDB().Clauses(dbresolver.Read).WithContext(ctx).First(&res, videoID).Error; err == nil { 74 | return res, nil 75 | } else if err == gorm.ErrRecordNotFound { 76 | return nil, nil 77 | } else { 78 | return nil, err 79 | } 80 | } 81 | 82 | // GetVideoListByIDs 83 | // 84 | // @Description: 根据视频id列表获取视频列表 85 | // @Date 2023-01-24 16:00:12 86 | // @param ctx 数据库操作上下文 87 | // @param videoIDs 视频id列表 88 | // @return []*Video 视频数据列表 89 | // @return error 90 | func GetVideoListByIDs(ctx context.Context, videoIDs []int64) ([]*Video, error) { 91 | res := make([]*Video, 0) 92 | if len(videoIDs) == 0 { 93 | return res, nil 94 | } 95 | 96 | if err := GetDB().Clauses(dbresolver.Read).WithContext(ctx).Where("video_id in ?", videoIDs).Find(&res).Error; err != nil { 97 | return nil, err 98 | } 99 | return res, nil 100 | } 101 | -------------------------------------------------------------------------------- /pkg/middleware/test_util.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "strconv" 9 | "strings" 10 | "testing" 11 | "time" 12 | 13 | sjwt "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/jwt" 14 | 15 | gjwt "github.com/golang-jwt/jwt" 16 | 17 | "github.com/gin-gonic/gin" 18 | ) 19 | 20 | type authResponse struct { 21 | StatusCode int `json:"status_code"` 22 | StatusMsg string `json:"status_msg"` 23 | } 24 | 25 | type tokenResponse struct { 26 | authResponse 27 | Token string `json:"token"` 28 | } 29 | 30 | func runTestServer(middleware gin.HandlerFunc) { 31 | // 创建一个服务器包含创建token和token验证的服务器 32 | r := gin.Default() 33 | signKey := []byte{0x12, 0x34, 0x56, 0x78, 0x9a} 34 | userJwt := sjwt.NewJWT(signKey) 35 | 36 | r.Use(middleware) 37 | r.POST("/login", func(c *gin.Context) { 38 | username, err := strconv.Atoi(c.PostForm("username")) 39 | if err != nil { 40 | c.JSON(200, gin.H{ 41 | "status_code": -1, 42 | "status_msg": "invalid argument", 43 | }) 44 | return 45 | } 46 | token, err := userJwt.CreateToken(sjwt.CustomClaims{ 47 | int64(username), 48 | gjwt.StandardClaims{ 49 | ExpiresAt: time.Now().Add(time.Second * 5).Unix(), // 5秒之后失效 50 | Issuer: "dousheng", 51 | }, 52 | }) 53 | 54 | if err != nil { 55 | c.JSON(200, gin.H{ 56 | "status_code": -1, 57 | "status_msg": err, 58 | }) 59 | 60 | } else { 61 | c.JSON(200, gin.H{ 62 | "status_code": 0, 63 | "status_msg": "", 64 | "token": token, 65 | }) 66 | } 67 | 68 | }) 69 | 70 | r.GET("/user", func(c *gin.Context) { 71 | c.JSON(200, gin.H{ 72 | "status_code": 0, 73 | "status_msg": "", 74 | }) 75 | 76 | }) 77 | 78 | go func() { 79 | r.Run(":4001") 80 | }() 81 | 82 | } 83 | 84 | func doAuth(token string, t *testing.T) (int, int) { 85 | resp, err := http.Get(fmt.Sprintf("http://localhost:4001/user?token=%s", token)) 86 | 87 | if err != nil { 88 | t.Fatalf("error %v", err) 89 | } 90 | 91 | if resp.Body != nil { 92 | defer resp.Body.Close() 93 | } 94 | 95 | body, err := ioutil.ReadAll(resp.Body) 96 | 97 | if err != nil { 98 | t.Fatalf("error %v", err) 99 | } 100 | 101 | authResponse := authResponse{} 102 | 103 | err = json.Unmarshal(body, &authResponse) 104 | 105 | if err != nil { 106 | t.Fatalf("json unmarshal error: %v", err) 107 | } 108 | 109 | return resp.StatusCode, authResponse.StatusCode 110 | } 111 | 112 | func getAuthToken(username string, t *testing.T) string { 113 | resp, err := http.Post("http://localhost:4001/login", 114 | "application/x-www-form-urlencoded", 115 | strings.NewReader("username="+username)) 116 | 117 | if err != nil { 118 | t.Fatalf("error %v", err) 119 | } 120 | 121 | if resp.Body != nil { 122 | defer resp.Body.Close() 123 | } 124 | 125 | body, err := ioutil.ReadAll(resp.Body) 126 | 127 | if err != nil { 128 | t.Fatalf("error %v", err) 129 | } 130 | 131 | tokenResponse := tokenResponse{} 132 | 133 | err = json.Unmarshal(body, &tokenResponse) 134 | 135 | if err != nil { 136 | t.Fatalf("json unmarshal error: %v", err) 137 | } 138 | 139 | return tokenResponse.Token 140 | } 141 | -------------------------------------------------------------------------------- /kitex/kitex_gen/relation/relationservice/client.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.4.4. DO NOT EDIT. 2 | 3 | package relationservice 4 | 5 | import ( 6 | "context" 7 | relation "github.com/bytedance-youthcamp-jbzx/tiktok/kitex/kitex_gen/relation" 8 | client "github.com/cloudwego/kitex/client" 9 | callopt "github.com/cloudwego/kitex/client/callopt" 10 | ) 11 | 12 | // Client is designed to provide IDL-compatible methods with call-option parameter for kitex framework. 13 | type Client interface { 14 | RelationAction(ctx context.Context, Req *relation.RelationActionRequest, callOptions ...callopt.Option) (r *relation.RelationActionResponse, err error) 15 | RelationFollowList(ctx context.Context, Req *relation.RelationFollowListRequest, callOptions ...callopt.Option) (r *relation.RelationFollowListResponse, err error) 16 | RelationFollowerList(ctx context.Context, Req *relation.RelationFollowerListRequest, callOptions ...callopt.Option) (r *relation.RelationFollowerListResponse, err error) 17 | RelationFriendList(ctx context.Context, Req *relation.RelationFriendListRequest, callOptions ...callopt.Option) (r *relation.RelationFriendListResponse, err error) 18 | } 19 | 20 | // NewClient creates a client for the service defined in IDL. 21 | func NewClient(destService string, opts ...client.Option) (Client, error) { 22 | var options []client.Option 23 | options = append(options, client.WithDestService(destService)) 24 | 25 | options = append(options, opts...) 26 | 27 | kc, err := client.NewClient(serviceInfo(), options...) 28 | if err != nil { 29 | return nil, err 30 | } 31 | return &kRelationServiceClient{ 32 | kClient: newServiceClient(kc), 33 | }, nil 34 | } 35 | 36 | // MustNewClient creates a client for the service defined in IDL. It panics if any error occurs. 37 | func MustNewClient(destService string, opts ...client.Option) Client { 38 | kc, err := NewClient(destService, opts...) 39 | if err != nil { 40 | panic(err) 41 | } 42 | return kc 43 | } 44 | 45 | type kRelationServiceClient struct { 46 | *kClient 47 | } 48 | 49 | func (p *kRelationServiceClient) RelationAction(ctx context.Context, Req *relation.RelationActionRequest, callOptions ...callopt.Option) (r *relation.RelationActionResponse, err error) { 50 | ctx = client.NewCtxWithCallOptions(ctx, callOptions) 51 | return p.kClient.RelationAction(ctx, Req) 52 | } 53 | 54 | func (p *kRelationServiceClient) RelationFollowList(ctx context.Context, Req *relation.RelationFollowListRequest, callOptions ...callopt.Option) (r *relation.RelationFollowListResponse, err error) { 55 | ctx = client.NewCtxWithCallOptions(ctx, callOptions) 56 | return p.kClient.RelationFollowList(ctx, Req) 57 | } 58 | 59 | func (p *kRelationServiceClient) RelationFollowerList(ctx context.Context, Req *relation.RelationFollowerListRequest, callOptions ...callopt.Option) (r *relation.RelationFollowerListResponse, err error) { 60 | ctx = client.NewCtxWithCallOptions(ctx, callOptions) 61 | return p.kClient.RelationFollowerList(ctx, Req) 62 | } 63 | 64 | func (p *kRelationServiceClient) RelationFriendList(ctx context.Context, Req *relation.RelationFriendListRequest, callOptions ...callopt.Option) (r *relation.RelationFriendListResponse, err error) { 65 | ctx = client.NewCtxWithCallOptions(ctx, callOptions) 66 | return p.kClient.RelationFriendList(ctx, Req) 67 | } 68 | -------------------------------------------------------------------------------- /pkg/errno/errno.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 a76yyyy && CloudWeGo Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /* 16 | * @Author: a76yyyy q981331502@163.com 17 | * @Date: 2022-06-08 16:22:35 18 | * @LastEditors: a76yyyy q981331502@163.com 19 | * @LastEditTime: 2022-06-19 00:49:57 20 | * @FilePath: /tiktok/pkg/errno/errno.go 21 | * @Description: 错误码报错业务逻辑 22 | */ 23 | 24 | package errno 25 | 26 | import ( 27 | "errors" 28 | "fmt" 29 | ) 30 | 31 | type ErrNo struct { 32 | ErrCode int 33 | ErrMsg string 34 | } 35 | 36 | // Err represents an error 37 | type Err struct { 38 | ErrCode int 39 | ErrMsg string 40 | Err error 41 | } 42 | 43 | type HttpErr struct { 44 | StatusCode int 45 | ErrNo ErrNo 46 | } 47 | 48 | func NewErrNo(code int, msg string) ErrNo { 49 | return ErrNo{code, msg} 50 | } 51 | 52 | func NewHttpErr(code int, httpcode int, msg string) HttpErr { 53 | return HttpErr{ 54 | StatusCode: httpcode, 55 | ErrNo: ErrNo{ErrCode: code, ErrMsg: msg}, 56 | } 57 | } 58 | 59 | func (e ErrNo) Error() string { 60 | return fmt.Sprintf("err_code=%d, err_msg=%s", e.ErrCode, e.ErrMsg) 61 | } 62 | 63 | func (e ErrNo) WithMessage(msg string) ErrNo { 64 | e.ErrMsg = msg 65 | return e 66 | } 67 | 68 | func NewErr(errno *ErrNo, err error) *Err { 69 | return &Err{ErrCode: errno.ErrCode, ErrMsg: errno.ErrMsg, Err: err} 70 | } 71 | 72 | func (err *Err) Add(message string) error { 73 | //err.ErrMsg = fmt.Sprintf("%s %s", err.ErrMsg, message) 74 | err.ErrMsg += " " + message 75 | return err 76 | } 77 | 78 | func (err *Err) Addf(format string, args ...interface{}) error { 79 | //return err.ErrMsg = fmt.Sprintf("%s %s", err.ErrMsg, fmt.Sprintf(format, args...)) 80 | err.ErrMsg += " " + fmt.Sprintf(format, args...) 81 | return err 82 | } 83 | 84 | func (err *Err) Error() string { 85 | return fmt.Sprintf("Err - code: %d, message: %s, error: %s", err.ErrCode, err.ErrMsg, err.Err) 86 | } 87 | 88 | func IsErrUserNotFound(err error) bool { 89 | code, _ := DecodeErr(err) 90 | return code == ErrUserNotFound.ErrCode 91 | } 92 | 93 | func DecodeErr(err error) (int, string) { 94 | if err == nil { 95 | return Success.ErrCode, Success.ErrMsg 96 | } 97 | 98 | switch typed := err.(type) { 99 | case *Err: 100 | return typed.ErrCode, typed.ErrMsg 101 | case *ErrNo: 102 | return typed.ErrCode, typed.ErrMsg 103 | default: 104 | } 105 | 106 | return ErrUnknown.ErrCode, err.Error() 107 | } 108 | 109 | // ConvertErr convert error to Errno 110 | func ConvertErr(err error) ErrNo { 111 | Err := ErrNo{} 112 | if errors.As(err, &Err) { 113 | return Err 114 | } 115 | 116 | s := ErrUnknown 117 | s.ErrMsg = err.Error() 118 | return s 119 | } 120 | -------------------------------------------------------------------------------- /cmd/api/handler/comment.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "context" 5 | "github.com/cloudwego/hertz/pkg/app" 6 | "net/http" 7 | "strconv" 8 | 9 | "github.com/bytedance-youthcamp-jbzx/tiktok/cmd/api/rpc" 10 | "github.com/bytedance-youthcamp-jbzx/tiktok/internal/response" 11 | kitex "github.com/bytedance-youthcamp-jbzx/tiktok/kitex/kitex_gen/comment" 12 | ) 13 | 14 | func CommentAction(ctx context.Context, c *app.RequestContext) { 15 | token := c.Query("token") 16 | vid, err := strconv.ParseInt(c.Query("video_id"), 10, 64) 17 | if err != nil { 18 | c.JSON(http.StatusOK, response.CommentAction{ 19 | Base: response.Base{ 20 | StatusCode: -1, 21 | StatusMsg: "video_id 不合法", 22 | }, 23 | Comment: nil, 24 | }) 25 | return 26 | } 27 | actionType, err := strconv.ParseInt(c.Query("action_type"), 10, 64) 28 | if err != nil || (actionType != 1 && actionType != 2) { 29 | c.JSON(http.StatusOK, response.CommentAction{ 30 | Base: response.Base{ 31 | StatusCode: -1, 32 | StatusMsg: "action_type 不合法", 33 | }, 34 | Comment: nil, 35 | }) 36 | return 37 | } 38 | req := new(kitex.CommentActionRequest) 39 | req.Token = token 40 | req.VideoId = vid 41 | req.ActionType = int32(actionType) 42 | 43 | if actionType == 1 { 44 | commentText := c.Query("comment_text") 45 | if commentText == "" { 46 | c.JSON(http.StatusOK, response.CommentAction{ 47 | Base: response.Base{ 48 | StatusCode: -1, 49 | StatusMsg: "comment_text 不能为空", 50 | }, 51 | Comment: nil, 52 | }) 53 | return 54 | } 55 | req.CommentText = commentText 56 | } else if actionType == 2 { 57 | commentID, err := strconv.ParseInt(c.Query("comment_id"), 10, 64) 58 | if err != nil { 59 | c.JSON(http.StatusOK, response.CommentAction{ 60 | Base: response.Base{ 61 | StatusCode: -1, 62 | StatusMsg: "comment_id 不合法", 63 | }, 64 | Comment: nil, 65 | }) 66 | return 67 | } 68 | req.CommentId = commentID 69 | } 70 | res, _ := rpc.CommentAction(ctx, req) 71 | if res.StatusCode == -1 { 72 | c.JSON(http.StatusOK, response.CommentAction{ 73 | Base: response.Base{ 74 | StatusCode: -1, 75 | StatusMsg: res.StatusMsg, 76 | }, 77 | Comment: nil, 78 | }) 79 | return 80 | } 81 | c.JSON(http.StatusOK, response.CommentAction{ 82 | Base: response.Base{ 83 | StatusCode: 0, 84 | StatusMsg: res.StatusMsg, 85 | }, 86 | Comment: res.Comment, 87 | }) 88 | } 89 | 90 | func CommentList(ctx context.Context, c *app.RequestContext) { 91 | token := c.Query("token") 92 | vid, err := strconv.ParseInt(c.Query("video_id"), 10, 64) 93 | if err != nil { 94 | c.JSON(http.StatusOK, response.CommentList{ 95 | Base: response.Base{ 96 | StatusCode: -1, 97 | StatusMsg: "video_id 不合法", 98 | }, 99 | CommentList: nil, 100 | }) 101 | return 102 | } 103 | req := &kitex.CommentListRequest{ 104 | Token: token, 105 | VideoId: vid, 106 | } 107 | res, _ := rpc.CommentList(ctx, req) 108 | if res.StatusCode == -1 { 109 | c.JSON(http.StatusOK, response.CommentList{ 110 | Base: response.Base{ 111 | StatusCode: -1, 112 | StatusMsg: res.StatusMsg, 113 | }, 114 | CommentList: nil, 115 | }) 116 | return 117 | } 118 | c.JSON(http.StatusOK, response.CommentList{ 119 | Base: response.Base{ 120 | StatusCode: 0, 121 | StatusMsg: res.StatusMsg, 122 | }, 123 | CommentList: res.CommentList, 124 | }) 125 | } 126 | -------------------------------------------------------------------------------- /cmd/api/handler/user.go: -------------------------------------------------------------------------------- 1 | // Package handler /* 2 | package handler 3 | 4 | import ( 5 | "context" 6 | "github.com/cloudwego/hertz/pkg/app" 7 | "net/http" 8 | "strconv" 9 | 10 | "github.com/bytedance-youthcamp-jbzx/tiktok/cmd/api/rpc" 11 | "github.com/bytedance-youthcamp-jbzx/tiktok/internal/response" 12 | "github.com/bytedance-youthcamp-jbzx/tiktok/kitex/kitex_gen/user" 13 | kitex "github.com/bytedance-youthcamp-jbzx/tiktok/kitex/kitex_gen/user" 14 | ) 15 | 16 | // Register 注册 17 | func Register(ctx context.Context, c *app.RequestContext) { 18 | username := c.Query("username") 19 | password := c.Query("password") 20 | //校验参数 21 | if len(username) == 0 || len(password) == 0 { 22 | c.JSON(http.StatusBadRequest, response.Register{ 23 | Base: response.Base{ 24 | StatusCode: -1, 25 | StatusMsg: "用户名或密码不能为空", 26 | }, 27 | }) 28 | return 29 | } 30 | if len(username) > 32 || len(password) > 32 { 31 | c.JSON(http.StatusOK, response.Register{ 32 | Base: response.Base{ 33 | StatusCode: -1, 34 | StatusMsg: "用户名或密码长度不能大于32个字符", 35 | }, 36 | }) 37 | return 38 | } 39 | //调用kitex/kitex_gen 40 | req := &kitex.UserRegisterRequest{ 41 | Username: username, 42 | Password: password, 43 | } 44 | res, _ := rpc.Register(ctx, req) 45 | if res.StatusCode == -1 { 46 | c.JSON(http.StatusOK, response.Register{ 47 | Base: response.Base{ 48 | StatusCode: -1, 49 | StatusMsg: res.StatusMsg, 50 | }, 51 | }) 52 | return 53 | } 54 | c.JSON(http.StatusOK, response.Register{ 55 | Base: response.Base{ 56 | StatusCode: 0, 57 | StatusMsg: res.StatusMsg, 58 | }, 59 | UserID: res.UserId, 60 | Token: res.Token, 61 | }) 62 | } 63 | 64 | // Login 登录 65 | func Login(ctx context.Context, c *app.RequestContext) { 66 | username := c.Query("username") 67 | password := c.Query("password") 68 | //校验参数 69 | if len(username) == 0 || len(password) == 0 { 70 | c.JSON(http.StatusBadRequest, response.Login{ 71 | Base: response.Base{ 72 | StatusCode: -1, 73 | StatusMsg: "用户名或密码不能为空", 74 | }, 75 | }) 76 | return 77 | } 78 | //调用kitex/kitex_gen 79 | req := &user.UserLoginRequest{ 80 | Username: username, 81 | Password: password, 82 | } 83 | res, _ := rpc.Login(ctx, req) 84 | if res.StatusCode == -1 { 85 | c.JSON(http.StatusOK, response.Login{ 86 | Base: response.Base{ 87 | StatusCode: -1, 88 | StatusMsg: res.StatusMsg, 89 | }, 90 | }) 91 | return 92 | } 93 | c.JSON(http.StatusOK, response.Login{ 94 | Base: response.Base{ 95 | StatusCode: 0, 96 | StatusMsg: res.StatusMsg, 97 | }, 98 | UserID: res.UserId, 99 | Token: res.Token, 100 | }) 101 | } 102 | 103 | // UserInfo 用户信息 104 | func UserInfo(ctx context.Context, c *app.RequestContext) { 105 | userId := c.Query("user_id") 106 | token := c.Query("token") 107 | if len(token) == 0 { 108 | c.JSON(http.StatusOK, response.UserInfo{ 109 | Base: response.Base{ 110 | StatusCode: -1, 111 | StatusMsg: "token 已过期", 112 | }, 113 | User: nil, 114 | }) 115 | return 116 | } 117 | id, err := strconv.ParseInt(userId, 10, 64) 118 | if err != nil { 119 | c.JSON(http.StatusOK, response.UserInfo{ 120 | Base: response.Base{ 121 | StatusCode: -1, 122 | StatusMsg: "user_id 不合法", 123 | }, 124 | User: nil, 125 | }) 126 | return 127 | } 128 | 129 | //调用kitex/kitex_genit 130 | req := &kitex.UserInfoRequest{ 131 | UserId: id, 132 | Token: token, 133 | } 134 | res, _ := rpc.UserInfo(ctx, req) 135 | if res.StatusCode == -1 { 136 | c.JSON(http.StatusOK, response.UserInfo{ 137 | Base: response.Base{ 138 | StatusCode: -1, 139 | StatusMsg: res.StatusMsg, 140 | }, 141 | User: nil, 142 | }) 143 | return 144 | } 145 | c.JSON(http.StatusOK, response.UserInfo{ 146 | Base: response.Base{ 147 | StatusCode: 0, 148 | StatusMsg: res.StatusMsg, 149 | }, 150 | User: res.User, 151 | }) 152 | } 153 | -------------------------------------------------------------------------------- /cmd/api/handler/video.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/zap" 7 | "github.com/cloudwego/hertz/pkg/app" 8 | "io" 9 | "net/http" 10 | "strconv" 11 | "time" 12 | 13 | "github.com/bytedance-youthcamp-jbzx/tiktok/cmd/api/rpc" 14 | "github.com/bytedance-youthcamp-jbzx/tiktok/internal/response" 15 | kitex "github.com/bytedance-youthcamp-jbzx/tiktok/kitex/kitex_gen/video" 16 | ) 17 | 18 | func Feed(ctx context.Context, c *app.RequestContext) { 19 | token := c.Query("token") 20 | latestTime := c.Query("latest_time") 21 | var timestamp int64 = 0 22 | if latestTime != "" { 23 | timestamp, _ = strconv.ParseInt(latestTime, 10, 64) 24 | } else { 25 | timestamp = time.Now().UnixMilli() 26 | } 27 | 28 | req := &kitex.FeedRequest{ 29 | LatestTime: timestamp, 30 | Token: token, 31 | } 32 | res, _ := rpc.Feed(ctx, req) 33 | if res.StatusCode == -1 { 34 | c.JSON(http.StatusOK, response.Feed{ 35 | Base: response.Base{ 36 | StatusCode: -1, 37 | StatusMsg: res.StatusMsg, 38 | }, 39 | }) 40 | return 41 | } 42 | c.JSON(http.StatusOK, response.Feed{ 43 | Base: response.Base{ 44 | StatusCode: 0, 45 | StatusMsg: res.StatusMsg, 46 | }, 47 | VideoList: res.VideoList, 48 | }) 49 | } 50 | 51 | func PublishList(ctx context.Context, c *app.RequestContext) { 52 | token := c.GetString("token") 53 | 54 | uid, err := strconv.ParseInt(c.Query("user_id"), 10, 64) 55 | if err != nil { 56 | c.JSON(http.StatusOK, response.PublishList{ 57 | Base: response.Base{ 58 | StatusCode: -1, 59 | StatusMsg: "user_id 不合法", 60 | }, 61 | }) 62 | return 63 | } 64 | req := &kitex.PublishListRequest{ 65 | Token: token, 66 | UserId: uid, 67 | } 68 | res, _ := rpc.PublishList(ctx, req) 69 | if res.StatusCode == -1 { 70 | c.JSON(http.StatusOK, response.PublishList{ 71 | Base: response.Base{ 72 | StatusCode: -1, 73 | StatusMsg: res.StatusMsg, 74 | }, 75 | }) 76 | return 77 | } 78 | c.JSON(http.StatusOK, response.PublishList{ 79 | Base: response.Base{ 80 | StatusCode: 0, 81 | StatusMsg: "success", 82 | }, 83 | VideoList: res.VideoList, 84 | }) 85 | } 86 | 87 | func PublishAction(ctx context.Context, c *app.RequestContext) { 88 | logger := zap.InitLogger() 89 | token := c.PostForm("token") 90 | if token == "" { 91 | c.JSON(http.StatusOK, response.PublishAction{ 92 | Base: response.Base{ 93 | StatusCode: -1, 94 | StatusMsg: "用户鉴权失败,token为空", 95 | }, 96 | }) 97 | return 98 | } 99 | title := c.PostForm("title") 100 | if title == "" { 101 | c.JSON(http.StatusOK, response.PublishAction{ 102 | Base: response.Base{ 103 | StatusCode: -1, 104 | StatusMsg: "标题不能为空", 105 | }, 106 | }) 107 | return 108 | } 109 | // 视频数据 110 | file, err := c.FormFile("data") 111 | if err != nil { 112 | logger.Errorln(err.Error()) 113 | c.JSON(http.StatusBadRequest, response.RelationAction{ 114 | Base: response.Base{ 115 | StatusCode: -1, 116 | StatusMsg: "上传视频加载失败", 117 | }, 118 | }) 119 | return 120 | } 121 | src, err := file.Open() 122 | buf := bytes.NewBuffer(nil) 123 | if _, err := io.Copy(buf, src); err != nil { 124 | logger.Errorln(err.Error()) 125 | c.JSON(http.StatusBadRequest, response.RelationAction{ 126 | Base: response.Base{ 127 | StatusCode: -1, 128 | StatusMsg: "视频上传失败", 129 | }, 130 | }) 131 | return 132 | } 133 | 134 | req := &kitex.PublishActionRequest{ 135 | Token: token, 136 | Title: title, 137 | Data: buf.Bytes(), 138 | } 139 | res, _ := rpc.PublishAction(ctx, req) 140 | if res.StatusCode == -1 { 141 | c.JSON(http.StatusOK, response.PublishAction{ 142 | Base: response.Base{ 143 | StatusCode: -1, 144 | StatusMsg: res.StatusMsg, 145 | }, 146 | }) 147 | return 148 | } 149 | c.JSON(http.StatusOK, response.PublishAction{ 150 | Base: response.Base{ 151 | StatusCode: 0, 152 | StatusMsg: res.StatusMsg, 153 | }, 154 | }) 155 | } 156 | -------------------------------------------------------------------------------- /dal/db/comment.go: -------------------------------------------------------------------------------- 1 | // 2 | // Package db 3 | // @Description: 数据库数据库操作业务逻辑 4 | // @Author hehehhh 5 | // @Date 2023-01-21 14:33:47 6 | // @Update 7 | // 8 | 9 | package db 10 | 11 | import ( 12 | "context" 13 | "time" 14 | 15 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/errno" 16 | "gorm.io/gorm" 17 | "gorm.io/plugin/dbresolver" 18 | ) 19 | 20 | // Comment 21 | // 22 | // @Description: 用户评论数据模型 23 | type Comment struct { 24 | ID uint `gorm:"primarykey"` 25 | CreatedAt time.Time `gorm:"index;not null" json:"create_date"` 26 | UpdatedAt time.Time 27 | DeletedAt gorm.DeletedAt `gorm:"index"` 28 | Video Video `gorm:"foreignkey:VideoID" json:"video,omitempty"` 29 | VideoID uint `gorm:"index:idx_videoid;not null" json:"video_id"` 30 | User User `gorm:"foreignkey:UserID" json:"user,omitempty"` 31 | UserID uint `gorm:"index:idx_userid;not null" json:"user_id"` 32 | Content string `gorm:"type:varchar(255);not null" json:"content"` 33 | LikeCount uint `gorm:"column:like_count;default:0;not null" json:"like_count,omitempty"` 34 | TeaseCount uint `gorm:"column:tease_count;default:0;not null" json:"tease_count,omitempty"` 35 | } 36 | 37 | func (Comment) TableName() string { 38 | return "comments" 39 | } 40 | 41 | // CreateComment 42 | // 43 | // @Description: 新增一条评论数据,并对所属视频的评论数+1 44 | // @Date 2023-01-21 14:42:49 45 | // @param ctx 数据库操作上下文 46 | // @param comment 评论数据 47 | // @return error 48 | func CreateComment(ctx context.Context, comment *Comment) error { 49 | err := GetDB().Clauses(dbresolver.Write).WithContext(ctx).Transaction(func(tx *gorm.DB) error { 50 | // 在事务中执行一些 db 操作(从这里开始,您应该使用 'tx' 而不是 'db') 51 | // 1. 新增评论数据 52 | err := tx.Create(comment).Error 53 | if err != nil { 54 | return err 55 | } 56 | 57 | // 2.对 Video 表中的评论数+1 58 | res := tx.Model(&Video{}).Where("id = ?", comment.VideoID).Update("comment_count", gorm.Expr("comment_count + ?", 1)) 59 | if res.Error != nil { 60 | return res.Error 61 | } 62 | 63 | if res.RowsAffected != 1 { 64 | // 影响的数据条数不是1 65 | return errno.ErrDatabase 66 | } 67 | 68 | return nil 69 | }) 70 | return err 71 | } 72 | 73 | // DelCommentByID 74 | // 75 | // @Description: 删除一条评论数据,并对所属视频的评论数-1 76 | // @Date 2023-01-21 14:49:43 77 | // @param ctx 数据库操作上下文 78 | // @param commentID 需要删除的评论的id 79 | // @param vid 评论所属视频的id 80 | // @return error 81 | func DelCommentByID(ctx context.Context, commentID int64, vid int64) error { 82 | err := GetDB().Clauses(dbresolver.Write).WithContext(ctx).Transaction(func(tx *gorm.DB) error { 83 | // 在事务中执行一些 db 操作(从这里开始,您应该使用 'tx' 而不是 'db') 84 | comment := new(Comment) 85 | if err := tx.First(&comment, commentID).Error; err != nil { 86 | return err 87 | } else if err == gorm.ErrRecordNotFound { 88 | return nil 89 | } 90 | 91 | // 1. 删除评论数据 92 | // 这里使用的实际上是软删除 93 | err := tx.Where("id = ?", commentID).Delete(&Comment{}).Error 94 | if err != nil { 95 | return err 96 | } 97 | 98 | // 2.改变 video 表中的 comment count 99 | res := tx.Model(&Video{}).Where("id = ?", vid).Update("comment_count", gorm.Expr("comment_count - ?", 1)) 100 | if res.Error != nil { 101 | return res.Error 102 | } 103 | 104 | if res.RowsAffected != 1 { 105 | // 影响的数据条数不是1 106 | return errno.ErrDatabase 107 | } 108 | 109 | return nil 110 | }) 111 | return err 112 | } 113 | 114 | // GetVideoCommentListByVideoID 115 | // 116 | // @Description: 根据视频id获取指定视频的全部评论内容 117 | // @Date 2023-01-21 15:13:33 118 | // @param ctx 数据库操作上下文 119 | // @param videoID 视频id 120 | // @return []*Comment 评论内容 121 | // @return error 122 | func GetVideoCommentListByVideoID(ctx context.Context, videoID int64) ([]*Comment, error) { 123 | var comments []*Comment 124 | err := GetDB().Clauses(dbresolver.Read).WithContext(ctx).Model(&Comment{}).Where(&Comment{VideoID: uint(videoID)}).Order("created_at DESC").Find(&comments).Error 125 | if err != nil { 126 | return nil, err 127 | } 128 | return comments, nil 129 | } 130 | 131 | // GetCommentByCommentID 132 | // 133 | // @Description: 根据评论ID获取评论 134 | // @Date 2023-02-23 10:03:01 135 | // @param ctx 数据库操作上下文 136 | // @param id 评论id 137 | // @return *Comment 评论 138 | // @return error 139 | func GetCommentByCommentID(ctx context.Context, commentID int64) (*Comment, error) { 140 | comment := new(Comment) 141 | if err := GetDB().Clauses(dbresolver.Read).WithContext(ctx).Where("id = ?", commentID).First(&comment).Error; err == nil { 142 | return comment, nil 143 | } else if err == gorm.ErrRecordNotFound { 144 | return nil, nil 145 | } else { 146 | return nil, err 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /cmd/api/handler/relation.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "context" 5 | "github.com/cloudwego/hertz/pkg/app" 6 | "net/http" 7 | "strconv" 8 | 9 | "github.com/bytedance-youthcamp-jbzx/tiktok/cmd/api/rpc" 10 | "github.com/bytedance-youthcamp-jbzx/tiktok/internal/response" 11 | kitex "github.com/bytedance-youthcamp-jbzx/tiktok/kitex/kitex_gen/relation" 12 | ) 13 | 14 | func FriendList(ctx context.Context, c *app.RequestContext) { 15 | token := c.Query("token") 16 | uid, err := strconv.ParseInt(c.Query("user_id"), 10, 64) 17 | if err != nil { 18 | c.JSON(http.StatusOK, response.FriendList{ 19 | Base: response.Base{ 20 | StatusCode: -1, 21 | StatusMsg: "user_id 不合法", 22 | }, 23 | }) 24 | return 25 | } 26 | // 调用kitex/kitex_gen 27 | req := &kitex.RelationFriendListRequest{ 28 | UserId: uid, 29 | Token: token, 30 | } 31 | res, _ := rpc.RelationFriendList(ctx, req) 32 | if res.StatusCode == -1 { 33 | c.JSON(http.StatusOK, response.FriendList{ 34 | Base: response.Base{ 35 | StatusCode: -1, 36 | StatusMsg: res.StatusMsg, 37 | }, 38 | }) 39 | return 40 | } 41 | c.JSON(http.StatusOK, response.FriendList{ 42 | Base: response.Base{ 43 | StatusCode: 0, 44 | StatusMsg: "success", 45 | }, 46 | UserList: res.UserList, 47 | }) 48 | } 49 | 50 | func FollowerList(ctx context.Context, c *app.RequestContext) { 51 | uid, err := strconv.ParseInt(c.Query("user_id"), 10, 64) 52 | if err != nil { 53 | c.JSON(http.StatusOK, response.FollowerList{ 54 | Base: response.Base{ 55 | StatusCode: -1, 56 | StatusMsg: "user_id 不合法", 57 | }, 58 | }) 59 | return 60 | } 61 | 62 | token := c.Query("token") 63 | req := &kitex.RelationFollowerListRequest{ 64 | UserId: uid, 65 | Token: token, 66 | } 67 | res, _ := rpc.RelationFollowerList(ctx, req) 68 | if res.StatusCode == -1 { 69 | c.JSON(http.StatusOK, response.FollowerList{ 70 | Base: response.Base{ 71 | StatusCode: -1, 72 | StatusMsg: res.StatusMsg, 73 | }, 74 | }) 75 | return 76 | } 77 | c.JSON(http.StatusOK, response.FollowerList{ 78 | Base: response.Base{ 79 | StatusCode: 0, 80 | StatusMsg: res.StatusMsg, 81 | }, 82 | UserList: res.UserList, 83 | }) 84 | } 85 | 86 | func FollowList(ctx context.Context, c *app.RequestContext) { 87 | uid, err := strconv.ParseInt(c.Query("user_id"), 10, 64) 88 | if err != nil { 89 | c.JSON(http.StatusOK, response.FollowList{ 90 | Base: response.Base{ 91 | StatusCode: -1, 92 | StatusMsg: "user_id 不合法", 93 | }, 94 | }) 95 | return 96 | } 97 | token := c.Query("token") 98 | req := &kitex.RelationFollowListRequest{ 99 | UserId: uid, 100 | Token: token, 101 | } 102 | res, _ := rpc.RelationFollowList(ctx, req) 103 | if res.StatusCode == -1 { 104 | c.JSON(http.StatusOK, response.FollowList{ 105 | Base: response.Base{ 106 | StatusCode: -1, 107 | StatusMsg: res.StatusMsg, 108 | }, 109 | }) 110 | return 111 | } 112 | c.JSON(http.StatusOK, response.FollowList{ 113 | Base: response.Base{ 114 | StatusCode: 0, 115 | StatusMsg: res.StatusMsg, 116 | }, 117 | UserList: res.UserList, 118 | }) 119 | } 120 | 121 | func RelationAction(ctx context.Context, c *app.RequestContext) { 122 | tid, err := strconv.ParseInt(c.Query("to_user_id"), 10, 64) 123 | if err != nil { 124 | c.JSON(http.StatusOK, response.RelationAction{ 125 | Base: response.Base{ 126 | StatusCode: -1, 127 | StatusMsg: "to_user_id 不合法", 128 | }, 129 | }) 130 | return 131 | } 132 | actionType, err := strconv.ParseInt(c.Query("action_type"), 10, 64) 133 | if err != nil || (actionType != 1 && actionType != 2) { 134 | c.JSON(http.StatusOK, response.RelationAction{ 135 | Base: response.Base{ 136 | StatusCode: -1, 137 | StatusMsg: "action_type 不合法", 138 | }, 139 | }) 140 | return 141 | } 142 | token := c.Query("token") 143 | if token == "" { 144 | c.JSON(http.StatusOK, response.RelationAction{ 145 | Base: response.Base{ 146 | StatusCode: -1, 147 | StatusMsg: "Token has expired.", 148 | }, 149 | }) 150 | return 151 | } 152 | req := &kitex.RelationActionRequest{ 153 | Token: token, 154 | ToUserId: tid, 155 | ActionType: int32(actionType), 156 | } 157 | res, _ := rpc.RelationAction(ctx, req) 158 | if res.StatusCode == -1 { 159 | c.JSON(http.StatusOK, response.FollowList{ 160 | Base: response.Base{ 161 | StatusCode: -1, 162 | StatusMsg: res.StatusMsg, 163 | }, 164 | }) 165 | return 166 | } 167 | c.JSON(http.StatusOK, response.RelationAction{ 168 | Base: response.Base{ 169 | StatusCode: 0, 170 | StatusMsg: res.StatusMsg, 171 | }, 172 | }) 173 | } 174 | -------------------------------------------------------------------------------- /dal/db/message.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "gorm.io/gorm" 8 | "gorm.io/plugin/dbresolver" 9 | ) 10 | 11 | // Message 12 | // 13 | // @Description: 聊天消息数据模型 14 | type Message struct { 15 | ID uint `gorm:"primarykey"` 16 | CreatedAt time.Time `gorm:"index;not null" json:"create_time"` 17 | UpdatedAt time.Time 18 | DeletedAt gorm.DeletedAt `gorm:"index"` 19 | FromUser User `gorm:"foreignkey:FromUserID;" json:"from_user,omitempty"` 20 | FromUserID uint `gorm:"index:idx_userid_from;not null" json:"from_user_id"` 21 | ToUser User `gorm:"foreignkey:ToUserID;" json:"to_user,omitempty"` 22 | ToUserID uint `gorm:"index:idx_userid_from;index:idx_userid_to;not null" json:"to_user_id"` 23 | Content string `gorm:"type:varchar(255);not null" json:"content"` 24 | } 25 | 26 | func (Message) TableName() string { 27 | return "messages" 28 | } 29 | 30 | // GetMessagesByUserIDs 31 | // 32 | // @Description: 根据两个用户的用户id获取聊天信息记录 33 | // @Date 2023-01-25 11:37:08 34 | // @param ctx 数据库操作上下文 35 | // @param userID 主用户id 36 | // @param toUserID 对象用户id 37 | // @param lastTimestamp 要查询消息时间的下限 38 | // @return []*Message 聊天信息数据列表 39 | // @return error 40 | func GetMessagesByUserIDs(ctx context.Context, userID int64, toUserID int64, lastTimestamp int64) ([]*Message, error) { 41 | res := make([]*Message, 0) 42 | if err := GetDB().Clauses(dbresolver.Read).WithContext(ctx).Where("((from_user_id = ? AND to_user_id = ?) OR (from_user_id = ? AND to_user_id = ?)) AND created_at > ?", 43 | userID, toUserID, toUserID, userID, time.UnixMilli(lastTimestamp).Format("2006-01-02 15:04:05.000"), 44 | ).Order("created_at ASC").Find(&res).Error; err != nil { 45 | return nil, err 46 | } 47 | return res, nil 48 | } 49 | 50 | // GetMessagesByUserToUser 51 | // 52 | // @Description: 根据两个用户的用户id获取单向数据 53 | // @Date 2023-01-25 11:37:08 54 | // @param ctx 数据库操作上下文 55 | // @param userID 主用户id 56 | // @param toUserID 对象用户id 57 | // @param lastTimestamp 要查询消息时间的下限 58 | // @return []*Message 聊天信息数据列表 59 | // @return error 60 | 61 | func GetMessagesByUserToUser(ctx context.Context, userID int64, toUserID int64, lastTimestamp int64) ([]*Message, error) { 62 | res := make([]*Message, 0) 63 | if err := GetDB().Clauses(dbresolver.Read).WithContext(ctx).Where("from_user_id = ? AND to_user_id = ? AND created_at > ?", 64 | userID, toUserID, time.UnixMilli(lastTimestamp).Format("2006-01-02 15:04:05.000"), 65 | ).Order("created_at ASC").Find(&res).Error; err != nil { 66 | return nil, err 67 | } 68 | return res, nil 69 | } 70 | 71 | // CreateMessagesByList 72 | // 73 | // @Description: 新增多条聊天信息 74 | // @Date 2023-01-21 17:13:26 75 | // @param ctx 数据库操作上下文 76 | // @param users 用户数据列表 77 | // @return error 78 | func CreateMessagesByList(ctx context.Context, messages []*Message) error { 79 | err := GetDB().Clauses(dbresolver.Write).WithContext(ctx).Transaction(func(tx *gorm.DB) error { 80 | if err := tx.Create(messages).Error; err != nil { 81 | return err 82 | } 83 | return nil 84 | }) 85 | return err 86 | } 87 | 88 | // GetMessageIDsByUserIDs 89 | // 90 | // @Description: 查询消息ID 91 | // @Date 2023-01-21 17:13:26 92 | // @param ctx 数据库操作上下文 93 | // @param userID 发送用户 94 | // @param toUserID 接收用户 95 | // @return error 96 | func GetMessageIDsByUserIDs(ctx context.Context, userID int64, toUserID int64) ([]*Message, error) { 97 | res := make([]*Message, 0) 98 | if err := GetDB().Clauses(dbresolver.Read).WithContext(ctx).Select("id").Where("(from_user_id = ? AND to_user_id = ?) OR (from_user_id = ? AND to_user_id = ?)", userID, toUserID, toUserID, userID).Find(&res).Error; err != nil { 99 | return nil, err 100 | } 101 | return res, nil 102 | } 103 | 104 | // GetMessageByID 105 | // 106 | // @Description: 根据消息ID查询消息 107 | // @Date 2023-01-21 17:13:26 108 | // @param ctx 数据库操作上下文 109 | // @param messageID 消息ID列表 110 | // @return error 111 | func GetMessageByID(ctx context.Context, messageID int64) (*Message, error) { 112 | res := new(Message) 113 | if err := GetDB().Clauses(dbresolver.Read).WithContext(ctx).Select("id, from_user_id, to_user_id, content, created_at").Where("id = ?", messageID).First(&res).Error; err != nil { 114 | return nil, err 115 | } 116 | return res, nil 117 | } 118 | 119 | func GetFriendLatestMessage(ctx context.Context, userID int64, toUserID int64) (*Message, error) { 120 | var res *Message 121 | if err := GetDB().Clauses(dbresolver.Read).WithContext(ctx).Select("id, from_user_id, to_user_id, content, created_at").Where("(from_user_id = ? AND to_user_id = ?) OR (from_user_id = ? AND to_user_id = ?)", userID, toUserID, toUserID, userID).Order("created_at DESC").Limit(1).Find(&res).Error; err != nil { 122 | return nil, err 123 | } 124 | return res, nil 125 | } 126 | -------------------------------------------------------------------------------- /pkg/rabbitmq/rabbitmq.go: -------------------------------------------------------------------------------- 1 | package rabbitmq 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "log" 8 | "time" 9 | 10 | amqp "github.com/rabbitmq/amqp091-go" 11 | ) 12 | 13 | // rabbitMQ结构体 14 | type RabbitMQ struct { 15 | conn *amqp.Connection 16 | channel *amqp.Channel 17 | //队列名称 18 | QueueName string 19 | //交换机名称 20 | Exchange string 21 | //bind Key 名称 22 | Key string 23 | //连接信息 24 | Mqurl string 25 | Queue amqp.Queue 26 | // 通知 27 | notifyClose chan *amqp.Error // 如果异常关闭,会接收数据 28 | notifyConfirm chan amqp.Confirmation // 消息发送成功确认,会接收到数据 29 | prefetchCount int 30 | } 31 | 32 | // 创建结构体实例 33 | func NewRabbitMQ(queueName string, exchange string, key string, prefetchCount int) *RabbitMQ { 34 | return &RabbitMQ{QueueName: queueName, Exchange: exchange, Key: key, Mqurl: MqUrl, prefetchCount: prefetchCount} 35 | } 36 | 37 | // 断开channel 和 connection 38 | func (r *RabbitMQ) Destroy() { 39 | r.channel.Close() 40 | r.conn.Close() 41 | } 42 | 43 | // 错误处理函数 44 | func (r *RabbitMQ) failOnErr(err error, message string) { 45 | if err != nil { 46 | log.Fatalf("%s:%s", message, err) 47 | panic(fmt.Sprintf("%s:%s", message, err)) 48 | } 49 | } 50 | 51 | // 创建简单模式下RabbitMQ实例 52 | func NewRabbitMQSimple(queueName string, autoAck bool) *RabbitMQ { 53 | // 创建RabbitMQ实例 54 | rabbitmq := NewRabbitMQ(queueName, "", "", config.Viper.GetInt("server.prefetchCount")) 55 | var err error 56 | // 获取connection 57 | rabbitmq.conn, err = amqp.Dial(rabbitmq.Mqurl) 58 | rabbitmq.failOnErr(err, "failed to connect rabbitmq!") 59 | // 获取channel 60 | rabbitmq.channel, err = rabbitmq.conn.Channel() 61 | rabbitmq.failOnErr(err, "failed to open a channel") 62 | if !autoAck { 63 | //创建一个qos控制 64 | err = rabbitmq.channel.Qos(rabbitmq.prefetchCount, 0, false) 65 | rabbitmq.failOnErr(err, "failed to create a qos") 66 | } 67 | // 注册监听 68 | rabbitmq.channel.NotifyClose(rabbitmq.notifyClose) 69 | rabbitmq.channel.NotifyPublish(rabbitmq.notifyConfirm) 70 | return rabbitmq 71 | } 72 | 73 | // PublishSimple 简单模式队列生产 74 | func (r *RabbitMQ) PublishSimple(ctx context.Context, message []byte) error { 75 | //1.申请队列,如果队列不存在会自动创建,存在则跳过创建 76 | _, err := r.channel.QueueDeclare( 77 | r.QueueName, 78 | // 是否持久化 79 | false, 80 | // 是否自动删除 81 | false, 82 | // 是否具有排他性 83 | false, 84 | // 是否阻塞处理 85 | false, 86 | // 额外的属性 87 | nil, 88 | ) 89 | if err != nil { 90 | fmt.Println(err) 91 | logger.Errorf("MQ 生产者错误:%v", err.Error()) 92 | if r.conn.IsClosed() || r.channel.IsClosed() { 93 | logger.Errorln("RabbitMQ 连接断开,需要重连") 94 | return errors.New("RabbitMQ 连接断开,需要重连:" + err.Error()) 95 | } 96 | return err 97 | } 98 | // 调用channel 发送消息到队列中 99 | err = r.channel.PublishWithContext( 100 | ctx, 101 | r.Exchange, 102 | r.QueueName, 103 | // 如果为true,根据自身exchange类型和routekey规则无法找到符合条件的队列会把消息返还给发送者 104 | false, 105 | // 如果为true,当exchange发送消息到队列后发现队列上没有消费者,则会把消息返还给发送者 106 | false, 107 | amqp.Publishing{ 108 | ContentType: "application/json", //设置消息请求头为json 109 | Body: message, 110 | Timestamp: time.Now(), 111 | }) 112 | if err != nil { 113 | logger.Errorf("MQ 生产者错误:%v", err.Error()) 114 | if r.conn.IsClosed() || r.channel.IsClosed() { 115 | logger.Infoln("RabbitMQ 连接断开,需要重连") 116 | return errors.New("RabbitMQ 连接断开,需要重连:" + err.Error()) 117 | } 118 | return err 119 | } 120 | return nil 121 | } 122 | 123 | // ConsumeSimple simple 模式下消费者 124 | func (r *RabbitMQ) ConsumeSimple() (<-chan amqp.Delivery, error) { 125 | //1.申请队列,如果队列不存在会自动创建,存在则跳过创建 126 | q, err := r.channel.QueueDeclare( 127 | r.QueueName, 128 | // 是否持久化 129 | false, 130 | // 是否自动删除 131 | false, 132 | // 是否具有排他性 133 | false, 134 | // 是否阻塞处理 135 | false, 136 | // 额外的属性 137 | nil, 138 | ) 139 | if err != nil { 140 | logger.Errorf("MQ 消费者错误:%v", err.Error()) 141 | return nil, err 142 | } 143 | 144 | //接收消息 145 | msgs, err := r.channel.Consume( 146 | q.Name, // queue 147 | // 用来区分多个消费者 148 | "", // consumer 149 | // 是否自动应答 150 | config.Viper.GetBool("consumer.favorite.autoAck"), // auto-ack 151 | // 是否独有 152 | false, // exclusive 153 | // 设置为true,表示 不能将同一个Connection中生产者发送的消息传递给这个Connection中的消费者 154 | false, // no-local 155 | // 列是否阻塞 156 | false, // no-wait 157 | nil, // args 158 | ) 159 | if err != nil { 160 | logger.Errorf("MQ 消费者错误:%v", err.Error()) 161 | return nil, err 162 | } 163 | return msgs, nil 164 | } 165 | 166 | func (r *RabbitMQ) DeclareQueue() error { 167 | q, err := r.channel.QueueDeclare( 168 | r.QueueName, 169 | //是否持久化 170 | false, 171 | //是否自动删除 172 | false, 173 | //是否具有排他性 174 | false, 175 | //是否阻塞处理 176 | false, 177 | //额外的属性 178 | nil, 179 | ) 180 | if err != nil { 181 | logger.Errorln(err.Error()) 182 | } 183 | r.Queue = q 184 | return err 185 | } 186 | -------------------------------------------------------------------------------- /pkg/etcd/registry.go: -------------------------------------------------------------------------------- 1 | package etcd 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "os" 8 | "strconv" 9 | "time" 10 | 11 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/zap" 12 | 13 | "github.com/cloudwego/kitex/pkg/registry" 14 | clientv3 "go.etcd.io/etcd/client/v3" 15 | ) 16 | 17 | const ( 18 | ttlKey = "KITEX_ETCD_REGISTRY_LEASE_TTL" 19 | defaultTTL = 60 20 | ) 21 | 22 | type etcdRegistry struct { 23 | etcdClient *clientv3.Client 24 | leaseTTL int64 25 | meta *registerMeta 26 | } 27 | 28 | type registerMeta struct { 29 | leaseID clientv3.LeaseID 30 | ctx context.Context 31 | cancel context.CancelFunc 32 | } 33 | 34 | // NewEtcdRegistry creates a etcd based registry. 35 | func NewEtcdRegistry(endpoints []string) (registry.Registry, error) { 36 | return NewEtcdRegistryWithAuth(endpoints, "", "") 37 | } 38 | 39 | // NewEtcdRegistryWithAuth creates a etcd based registry with given username and password. 40 | func NewEtcdRegistryWithAuth(endpoints []string, username, password string) (registry.Registry, error) { 41 | etcdClient, err := clientv3.New(clientv3.Config{ 42 | Endpoints: endpoints, 43 | Username: username, 44 | Password: password, 45 | }) 46 | if err != nil { 47 | return nil, err 48 | } 49 | return &etcdRegistry{ 50 | etcdClient: etcdClient, 51 | leaseTTL: getTTL(), 52 | }, nil 53 | } 54 | 55 | // Register registers a server with given registry info. 56 | func (e *etcdRegistry) Register(info *registry.Info) error { 57 | if err := validateRegistryInfo(info); err != nil { 58 | return err 59 | } 60 | leaseID, err := e.grantLease() 61 | if err != nil { 62 | return err 63 | } 64 | 65 | if err := e.register(info, leaseID); err != nil { 66 | return err 67 | } 68 | meta := registerMeta{ 69 | leaseID: leaseID, 70 | } 71 | meta.ctx, meta.cancel = context.WithCancel(context.Background()) 72 | if err := e.keepalive(&meta); err != nil { 73 | return err 74 | } 75 | e.meta = &meta 76 | return nil 77 | } 78 | 79 | // Deregister deregisters a server with given registry info. 80 | func (e *etcdRegistry) Deregister(info *registry.Info) error { 81 | if info.ServiceName == "" { 82 | return fmt.Errorf("missing service name in Deregister") 83 | } 84 | if err := e.deregister(info); err != nil { 85 | return err 86 | } 87 | e.meta.cancel() 88 | return nil 89 | } 90 | 91 | func (e *etcdRegistry) register(info *registry.Info, leaseID clientv3.LeaseID) error { 92 | val, err := json.Marshal(&instanceInfo{ 93 | Network: info.Addr.Network(), 94 | Address: info.Addr.String(), 95 | Weight: info.Weight, 96 | Tags: info.Tags, 97 | }) 98 | if err != nil { 99 | return err 100 | } 101 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) 102 | defer cancel() 103 | _, err = e.etcdClient.Put(ctx, serviceKey(info.ServiceName, info.Addr.String()), string(val), clientv3.WithLease(leaseID)) 104 | return err 105 | } 106 | 107 | func (e *etcdRegistry) deregister(info *registry.Info) error { 108 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) 109 | defer cancel() 110 | _, err := e.etcdClient.Delete(ctx, serviceKey(info.ServiceName, info.Addr.String())) 111 | return err 112 | } 113 | 114 | func (e *etcdRegistry) grantLease() (clientv3.LeaseID, error) { 115 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) 116 | defer cancel() 117 | resp, err := e.etcdClient.Grant(ctx, e.leaseTTL) 118 | if err != nil { 119 | return clientv3.NoLease, err 120 | } 121 | return resp.ID, nil 122 | } 123 | 124 | func (e *etcdRegistry) keepalive(meta *registerMeta) error { 125 | logger := zap.InitLogger() 126 | keepAlive, err := e.etcdClient.KeepAlive(meta.ctx, meta.leaseID) 127 | if err != nil { 128 | return err 129 | } 130 | go func() { 131 | // eat keepAlive channel to keep related lease alive. 132 | //klog.Infof("start keepalive lease %x for etcd registry", meta.leaseID) 133 | logger.Infof("start keepalive lease %x for etcd registry", meta.leaseID) 134 | for range keepAlive { 135 | select { 136 | case <-meta.ctx.Done(): 137 | break 138 | default: 139 | } 140 | } 141 | //klog.Infof("stop keepalive lease %x for etcd registry", meta.leaseID) 142 | logger.Infof("stop keepalive lease %x for etcd registry", meta.leaseID) 143 | }() 144 | return nil 145 | } 146 | 147 | func validateRegistryInfo(info *registry.Info) error { 148 | if info.ServiceName == "" { 149 | return fmt.Errorf("missing service name in Register") 150 | } 151 | if info.Addr == nil { 152 | return fmt.Errorf("missing addr in Register") 153 | } 154 | return nil 155 | } 156 | 157 | func getTTL() int64 { 158 | var ttl int64 = defaultTTL 159 | if str, ok := os.LookupEnv(ttlKey); ok { 160 | if t, err := strconv.Atoi(str); err == nil { 161 | ttl = int64(t) 162 | } 163 | } 164 | return ttl 165 | } 166 | -------------------------------------------------------------------------------- /cmd/api/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "github.com/bytedance-youthcamp-jbzx/tiktok/cmd/api/handler" 7 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/jwt" 8 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/middleware" 9 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/viper" 10 | z "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/zap" 11 | "github.com/cloudwego/hertz/pkg/app/server" 12 | "github.com/cloudwego/hertz/pkg/common/config" 13 | "github.com/cloudwego/hertz/pkg/network/standard" 14 | "github.com/hertz-contrib/gzip" 15 | ) 16 | 17 | var ( 18 | apiConfig = viper.Init("api") 19 | apiServerName = apiConfig.Viper.GetString("server.name") 20 | apiServerAddr = fmt.Sprintf("%s:%d", apiConfig.Viper.GetString("server.host"), apiConfig.Viper.GetInt("server.port")) 21 | etcdAddress = fmt.Sprintf("%s:%d", apiConfig.Viper.GetString("Etcd.host"), apiConfig.Viper.GetInt("Etcd.port")) 22 | signingKey = apiConfig.Viper.GetString("JWT.signingKey") 23 | serverTLSKey = apiConfig.Viper.GetString("Hertz.tls.keyFile") 24 | serverTLSCert = apiConfig.Viper.GetString("Hertz.tls.certFile") 25 | ) 26 | 27 | func registerGroup(hz *server.Hertz) { 28 | douyin := hz.Group("/douyin") 29 | { 30 | user := douyin.Group("/user") 31 | { 32 | user.GET("/", handler.UserInfo) 33 | user.POST("/register/", handler.Register) 34 | user.POST("/login/", handler.Login) 35 | } 36 | message := douyin.Group("/message") 37 | { 38 | message.GET("/chat/", handler.MessageChat) 39 | message.POST("/action/", handler.MessageAction) 40 | } 41 | relation := douyin.Group("/relation") 42 | { 43 | // 粉丝列表 44 | relation.GET("/follower/list/", handler.FollowerList) 45 | // 关注列表 46 | relation.GET("/follow/list/", handler.FollowList) 47 | // 朋友列表 48 | relation.GET("/friend/list/", handler.FriendList) 49 | relation.POST("/action/", handler.RelationAction) 50 | } 51 | publish := douyin.Group("/publish") 52 | { 53 | publish.GET("/list/", handler.PublishList) 54 | publish.POST("/action/", handler.PublishAction) 55 | } 56 | douyin.GET("/feed", handler.Feed) 57 | favorite := douyin.Group("/favorite") 58 | { 59 | favorite.POST("/action/", handler.FavoriteAction) 60 | favorite.GET("/list/", handler.FavoriteList) 61 | } 62 | comment := douyin.Group("/comment") 63 | { 64 | comment.POST("/action/", handler.CommentAction) 65 | comment.GET("/list/", handler.CommentList) 66 | } 67 | } 68 | } 69 | 70 | func InitHertz() *server.Hertz { 71 | logger := z.InitLogger() 72 | 73 | opts := []config.Option{server.WithHostPorts(apiServerAddr)} 74 | 75 | // 网络库 76 | hertzNet := standard.NewTransporter 77 | //if apiConfig.Viper.GetBool("Hertz.useNetPoll") { 78 | // hertzNet = netpoll.NewTransporter 79 | //} 80 | opts = append(opts, server.WithTransport(hertzNet)) 81 | 82 | // TLS & Http2 83 | // https://github.com/cloudwego/hertz-examples/blob/main/protocol/tls/main.go 84 | tlsConfig := tls.Config{ 85 | MinVersion: tls.VersionTLS12, 86 | CurvePreferences: []tls.CurveID{tls.X25519, tls.CurveP256}, 87 | CipherSuites: []uint16{ 88 | tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, 89 | tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, 90 | tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, 91 | }, 92 | } 93 | if apiConfig.Viper.GetBool("Hertz.tls.enable") { 94 | if len(serverTLSKey) == 0 { 95 | panic("not found tiktok_tls_key in configuration") 96 | } 97 | if len(serverTLSCert) == 0 { 98 | panic("not found tiktok_tls_cert in configuration") 99 | } 100 | 101 | cert, err := tls.LoadX509KeyPair(serverTLSCert, serverTLSKey) 102 | if err != nil { 103 | logger.Errorln(err) 104 | } 105 | tlsConfig.Certificates = append(tlsConfig.Certificates, cert) 106 | opts = append(opts, server.WithTLS(&tlsConfig)) 107 | 108 | if alpn := apiConfig.Viper.GetBool("Hertz.tls.ALPN"); alpn { 109 | opts = append(opts, server.WithALPN(alpn)) 110 | } 111 | } else if apiConfig.Viper.GetBool("Hertz.http2.enable") { 112 | opts = append(opts, server.WithH2C(apiConfig.Viper.GetBool("Hertz.http2.enable"))) 113 | } 114 | 115 | hz := server.Default(opts...) 116 | 117 | hz.Use( 118 | // secure.New( 119 | // secure.WithSSLHost(apiServerAddr), 120 | // secure.WithSSLRedirect(true), 121 | // ), // TLS 122 | middleware.TokenAuthMiddleware(*jwt.NewJWT([]byte(signingKey)), 123 | "/douyin/user/register/", 124 | "/douyin/user/login/", 125 | "/douyin/feed", 126 | "/douyin/favorite/list/", 127 | "/douyin/publish/list/", 128 | "/douyin/comment/list/", 129 | "/douyin/relation/follower/list/", 130 | "/douyin/relation/follow/list/", 131 | ), // 用户鉴权中间件 132 | middleware.TokenLimitMiddleware(), //限流中间件 133 | middleware.AccessLog(), 134 | gzip.Gzip(gzip.DefaultCompression), 135 | ) 136 | return hz 137 | } 138 | 139 | func main() { 140 | hz := InitHertz() 141 | 142 | // add handler 143 | registerGroup(hz) 144 | 145 | hz.Spin() 146 | } 147 | -------------------------------------------------------------------------------- /dal/db/user.go: -------------------------------------------------------------------------------- 1 | // 2 | // Package db 3 | // @Description: 数据库数据库操作业务逻辑 4 | // @Author hehehhh 5 | // @Date 2023-01-21 14:33:47 6 | // @Update 7 | // 8 | 9 | package db 10 | 11 | import ( 12 | "context" 13 | 14 | "gorm.io/gorm" 15 | "gorm.io/plugin/dbresolver" 16 | ) 17 | 18 | // User 19 | // 20 | // @Description: 用户数据模型 21 | type User struct { 22 | gorm.Model 23 | UserName string `gorm:"index:idx_username,unique;type:varchar(40);not null" json:"name,omitempty"` 24 | Password string `gorm:"type:varchar(256);not null" json:"password,omitempty"` 25 | FavoriteVideos []Video `gorm:"many2many:user_favorite_videos" json:"favorite_videos,omitempty"` 26 | FollowingCount uint `gorm:"default:0;not null" json:"follow_count,omitempty"` // 关注总数 27 | FollowerCount uint `gorm:"default:0;not null" json:"follower_count,omitempty"` // 粉丝总数 28 | Avatar string `gorm:"type:varchar(256)" json:"avatar,omitempty"` // 用户头像 29 | BackgroundImage string `gorm:"column:background_image;type:varchar(256);default:default_background.jpg" json:"background_image,omitempty"` // 用户个人页顶部大图 30 | WorkCount uint `gorm:"default:0;not null" json:"work_count,omitempty"` // 作品数 31 | FavoriteCount uint `gorm:"default:0;not null" json:"favorite_count,omitempty"` // 喜欢数 32 | TotalFavorited uint `gorm:"default:0;not null" json:"total_favorited,omitempty"` // 获赞总量 33 | Signature string `gorm:"type:varchar(256)" json:"signature,omitempty"` // 个人简介 34 | } 35 | 36 | func (User) TableName() string { 37 | return "users" 38 | } 39 | 40 | // GetUsersByIDs 41 | // 42 | // @Description: 根据用户id列表获取用户列表 43 | // @Date 2023-01-21 17:11:13 44 | // @param ctx 数据库操作上下文 45 | // @param userIDs 用户id列表 46 | // @return []*User 用户列表 47 | // @return error 48 | func GetUsersByIDs(ctx context.Context, userIDs []int64) ([]*User, error) { 49 | res := make([]*User, 0) 50 | if len(userIDs) == 0 { 51 | return res, nil 52 | } 53 | 54 | if err := GetDB().WithContext(ctx).Where("id in ?", userIDs).Find(&res).Error; err != nil { 55 | return nil, err 56 | } 57 | return res, nil 58 | } 59 | 60 | // GetUserByID 61 | // 62 | // @Description: 根据用户id获取用户数据 63 | // @Date 2023-01-21 17:12:54 64 | // @param ctx 数据库操作上下文 65 | // @param userID 用户id 66 | // @return *User 用户数据 67 | // @return error 68 | func GetUserByID(ctx context.Context, userID int64) (*User, error) { 69 | res := new(User) 70 | if err := GetDB().Clauses(dbresolver.Read).WithContext(ctx).First(&res, userID).Error; err == nil { 71 | return res, err 72 | } else if err == gorm.ErrRecordNotFound { 73 | return nil, nil 74 | } else { 75 | return nil, err 76 | } 77 | } 78 | 79 | // CreateUsers 80 | // 81 | // @Description: 新增多条用户数据 82 | // @Date 2023-01-21 17:13:26 83 | // @param ctx 数据库操作上下文 84 | // @param users 用户数据列表 85 | // @return error 86 | func CreateUsers(ctx context.Context, users []*User) error { 87 | err := GetDB().Clauses(dbresolver.Write).WithContext(ctx).Transaction(func(tx *gorm.DB) error { 88 | if err := tx.Create(users).Error; err != nil { 89 | return err 90 | } 91 | return nil 92 | }) 93 | return err 94 | } 95 | 96 | // CreateUser 97 | // 98 | // @Description: 新增一条用户数据 99 | // @Date 2023-02-22 11:46:43 100 | // @param ctx 数据库操作上下文 101 | // @param user 用户数据 102 | // @return error 103 | func CreateUser(ctx context.Context, user *User) error { 104 | err := GetDB().Clauses(dbresolver.Write).WithContext(ctx).Transaction(func(tx *gorm.DB) error { 105 | if err := tx.Create(user).Error; err != nil { 106 | return err 107 | } 108 | return nil 109 | }) 110 | return err 111 | } 112 | 113 | // GetUserByName 114 | // 115 | // @Description: 根据用户名获取用户数据列表 116 | // @Date 2023-01-21 17:15:17 117 | // @param ctx 数据库操作上下文 118 | // @param userName 用户名 119 | // @return []*User 用户数据列表 120 | // @return error 121 | func GetUserByName(ctx context.Context, userName string) (*User, error) { 122 | res := new(User) 123 | if err := GetDB().Clauses(dbresolver.Read).WithContext(ctx).Select("id, user_name, password").Where("user_name = ?", userName).First(&res).Error; err == nil { 124 | return res, nil 125 | } else if err == gorm.ErrRecordNotFound { 126 | return nil, nil 127 | } else { 128 | return nil, err 129 | } 130 | } 131 | 132 | // 根据用户名获取密码 133 | func GetPasswordByUsername(ctx context.Context, userName string) (*User, error) { 134 | user := new(User) 135 | if err := GetDB().Clauses(dbresolver.Read).WithContext(ctx). 136 | Select("password").Where("user_name = ?", userName). 137 | First(&user).Error; err == nil { 138 | return user, nil 139 | } else if err == gorm.ErrRecordNotFound { 140 | return nil, nil 141 | } else { 142 | return nil, err 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /cmd/message/service/handler.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/bytedance-youthcamp-jbzx/tiktok/dal/db" 7 | "github.com/bytedance-youthcamp-jbzx/tiktok/dal/redis" 8 | "github.com/bytedance-youthcamp-jbzx/tiktok/internal/tool" 9 | message "github.com/bytedance-youthcamp-jbzx/tiktok/kitex/kitex_gen/message" 10 | "github.com/bytedance-youthcamp-jbzx/tiktok/pkg/zap" 11 | ) 12 | 13 | // MessageServiceImpl implements the last service interface defined in the IDL. 14 | type MessageServiceImpl struct{} 15 | 16 | // MessageChat implements the MessageServiceImpl interface. 17 | func (s *MessageServiceImpl) MessageChat(ctx context.Context, req *message.MessageChatRequest) (resp *message.MessageChatResponse, err error) { 18 | logger := zap.InitLogger() 19 | // 解析token,获取用户id 20 | claims, err := Jwt.ParseToken(req.Token) 21 | if err != nil { 22 | logger.Errorln(err.Error()) 23 | res := &message.MessageChatResponse{ 24 | StatusCode: -1, 25 | StatusMsg: "token 解析错误", 26 | } 27 | return res, nil 28 | } 29 | userID := claims.Id 30 | 31 | // 从redis中获取message时间戳 32 | lastTimestamp, err := redis.GetMessageTimestamp(ctx, req.Token, req.ToUserId) 33 | if err != nil { 34 | logger.Errorln(err.Error()) 35 | res := &message.MessageChatResponse{ 36 | StatusCode: -1, 37 | StatusMsg: "聊天记录获取失败:服务器内部错误", 38 | } 39 | return res, nil 40 | } 41 | 42 | var results []*db.Message 43 | if lastTimestamp == -1 { 44 | results, err = db.GetMessagesByUserIDs(ctx, userID, req.ToUserId, int64(lastTimestamp)) 45 | lastTimestamp = 0 46 | } else { 47 | results, err = db.GetMessagesByUserToUser(ctx, req.ToUserId, userID, int64(lastTimestamp)) 48 | } 49 | 50 | if err != nil { 51 | logger.Errorln(err.Error()) 52 | res := &message.MessageChatResponse{ 53 | StatusCode: -1, 54 | StatusMsg: "聊天记录获取失败:服务器内部错误", 55 | } 56 | return res, nil 57 | } 58 | messages := make([]*message.Message, 0) 59 | for _, r := range results { 60 | decContent, err := tool.Base64Decode([]byte(r.Content)) 61 | if err != nil { 62 | logger.Errorf("Base64Decode error: %v\n", err.Error()) 63 | res := &message.MessageChatResponse{ 64 | StatusCode: -1, 65 | StatusMsg: "聊天记录获取失败:服务器内部错误", 66 | } 67 | return res, nil 68 | } 69 | decContent, err = tool.RsaDecrypt(decContent, privateKey) 70 | if err != nil { 71 | logger.Errorf("rsa decrypt error: %v\n", err.Error()) 72 | res := &message.MessageChatResponse{ 73 | StatusCode: -1, 74 | StatusMsg: "聊天记录获取失败:服务器内部错误", 75 | } 76 | return res, nil 77 | } 78 | messages = append(messages, &message.Message{ 79 | Id: int64(r.ID), 80 | FromUserId: int64(r.FromUserID), 81 | ToUserId: int64(r.ToUserID), 82 | Content: string(decContent), 83 | CreateTime: r.CreatedAt.UnixMilli(), 84 | }) 85 | } 86 | 87 | res := &message.MessageChatResponse{ 88 | StatusCode: 0, 89 | StatusMsg: "success", 90 | MessageList: messages, 91 | } 92 | 93 | // 更新时间redis里的时间戳 94 | if len(messages) > 0 { 95 | message := messages[len(messages)-1] 96 | lastTimestamp = int(message.CreateTime) 97 | } 98 | 99 | if err = redis.SetMessageTimestamp(ctx, req.Token, req.ToUserId, lastTimestamp); err != nil { 100 | logger.Errorln(err.Error()) 101 | res := &message.MessageChatResponse{ 102 | StatusCode: -1, 103 | StatusMsg: "聊天记录获取失败:服务器内部错误", 104 | } 105 | return res, nil 106 | } 107 | 108 | return res, nil 109 | } 110 | 111 | // MessageAction implements the MessageServiceImpl interface. 112 | func (s *MessageServiceImpl) MessageAction(ctx context.Context, req *message.MessageActionRequest) (resp *message.MessageActionResponse, err error) { 113 | logger := zap.InitLogger() 114 | // 解析token,获取用户id 115 | claims, err := Jwt.ParseToken(req.Token) 116 | if err != nil { 117 | logger.Errorln(err.Error()) 118 | res := &message.MessageActionResponse{ 119 | StatusCode: -1, 120 | StatusMsg: "token 解析错误", 121 | } 122 | return res, nil 123 | } 124 | userID := claims.Id 125 | 126 | toUserID, actionType := req.ToUserId, req.ActionType 127 | 128 | if userID == toUserID { 129 | logger.Errorln("不能给自己发送消息") 130 | res := &message.MessageActionResponse{ 131 | StatusCode: -1, 132 | StatusMsg: "消息发送失败:不能给自己发送消息", 133 | } 134 | return res, nil 135 | } 136 | 137 | relation, err := db.GetRelationByUserIDs(ctx, userID, toUserID) 138 | if relation == nil { 139 | logger.Errorf("消息发送失败:非朋友关系,无法发送") 140 | res := &message.MessageActionResponse{ 141 | StatusCode: -1, 142 | StatusMsg: "消息发送失败:非朋友关系,无法发送", 143 | } 144 | return res, nil 145 | } 146 | 147 | rsaContent, err := tool.RsaEncrypt([]byte(req.Content), publicKey) 148 | if err != nil { 149 | logger.Errorf("rsa encrypt error: %v\n", err.Error()) 150 | res := &message.MessageActionResponse{ 151 | StatusCode: -1, 152 | StatusMsg: "消息发送失败:服务器内部错误", 153 | } 154 | return res, nil 155 | } 156 | 157 | messages := make([]*db.Message, 0) 158 | messages = append(messages, &db.Message{ 159 | FromUserID: uint(userID), 160 | ToUserID: uint(toUserID), 161 | Content: string(tool.Base64Encode(rsaContent)), 162 | }) 163 | if actionType == 1 { 164 | err := db.CreateMessagesByList(ctx, messages) 165 | if err != nil { 166 | logger.Errorln(err.Error()) 167 | res := &message.MessageActionResponse{ 168 | StatusCode: -1, 169 | StatusMsg: "消息发送失败:服务器内部错误", 170 | } 171 | return res, nil 172 | } 173 | } else { 174 | logger.Errorf("action_type 非法:%v", actionType) 175 | res := &message.MessageActionResponse{ 176 | StatusCode: -1, 177 | StatusMsg: "消息发送失败:非法的 action_type", 178 | } 179 | return res, nil 180 | } 181 | res := &message.MessageActionResponse{ 182 | StatusCode: 0, 183 | StatusMsg: "success", 184 | } 185 | return res, nil 186 | } 187 | --------------------------------------------------------------------------------