diff --git a/.gitignore b/.gitignore index 82c4ca1c..bba5f3c4 100644 --- a/.gitignore +++ b/.gitignore @@ -20,4 +20,6 @@ # Go workspace file go.work -.idea \ No newline at end of file +.idea + +.vscode \ No newline at end of file diff --git a/README.md b/README.md index 73b96f53..5be6976b 100644 --- a/README.md +++ b/README.md @@ -19,4 +19,9 @@ go build && ./simple-demo ### 测试数据 -测试数据写在 demo_data.go 中,用于列表接口的 mock 测试 \ No newline at end of file +测试数据写在 demo_data.go 中,用于列表接口的 mock 测试 + +### 目录结构说明 + +- document: 项目相关的一些文档记录 +- model: gorm模型 diff --git a/controller/comment.go b/controller/comment.go index 6875a1e8..15028855 100644 --- a/controller/comment.go +++ b/controller/comment.go @@ -1,38 +1,49 @@ package controller import ( - "github.com/gin-gonic/gin" + "context" + "fmt" "net/http" -) - -type CommentListResponse struct { - Response - CommentList []Comment `json:"comment_list,omitempty"` -} + "strconv" -type CommentActionResponse struct { - Response - Comment Comment `json:"comment,omitempty"` -} + "github.com/BaiZe1998/douyin-simple-demo/db/model" + "github.com/BaiZe1998/douyin-simple-demo/dto" + "github.com/BaiZe1998/douyin-simple-demo/pkg/util" + "github.com/BaiZe1998/douyin-simple-demo/service" + "github.com/gin-gonic/gin" +) // CommentAction no practical effect, just check if token is valid func CommentAction(c *gin.Context) { token := c.Query("token") actionType := c.Query("action_type") + videoId, _ := strconv.ParseInt(c.Query("video_id"), 10, 64) + userClaims, _ := util.ParseToken(token) + userModel, _ := model.QueryUserById(context.Background(), userClaims.ID) + users := dto.User{ + Id: userModel.ID, + Name: userModel.Name, + FollowCount: userModel.FollowCount, + FollowerCount: userModel.FollowerCount, + IsFollow: false, + } - if user, exist := usersLoginInfo[token]; exist { + if userModel.ID > 0 { if actionType == "1" { text := c.Query("comment_text") - c.JSON(http.StatusOK, CommentActionResponse{Response: Response{StatusCode: 0}, - Comment: Comment{ - Id: 1, - User: user, - Content: text, - CreateDate: "05-01", - }}) - return + //comment addcomment + responseComment := service.AddComment(text, users, videoId) + c.JSON(http.StatusOK, + dto.CommentActionResponse{ + Response: dto.Response{StatusCode: 0}, + Comment: responseComment, + }) + } else { + commentId, _ := strconv.ParseInt(c.Query("comment_id"), 10, 64) + //comment delete + model.DeleteCommnet(context.Background(), commentId) + c.JSON(http.StatusOK, Response{StatusCode: 0}) } - c.JSON(http.StatusOK, Response{StatusCode: 0}) } else { c.JSON(http.StatusOK, Response{StatusCode: 1, StatusMsg: "User doesn't exist"}) } @@ -40,8 +51,11 @@ func CommentAction(c *gin.Context) { // CommentList all videos have same demo comment list func CommentList(c *gin.Context) { - c.JSON(http.StatusOK, CommentListResponse{ - Response: Response{StatusCode: 0}, - CommentList: DemoComments, + videoId, _ := strconv.ParseInt(c.Query("video_id"), 10, 64) + res, total, _ := model.QueryComment(context.Background(), videoId, 10, 0) + c.JSON(http.StatusOK, dto.CommentListResponse{ + Response: dto.Response{StatusCode: 0, StatusMsg: ""}, + CommentList: res, }) + fmt.Println(total) } diff --git a/controller/common.go b/controller/common.go index a8213a16..e6b75569 100644 --- a/controller/common.go +++ b/controller/common.go @@ -1,29 +1,32 @@ package controller +import "github.com/BaiZe1998/douyin-simple-demo/dto" + type Response struct { StatusCode int32 `json:"status_code"` StatusMsg string `json:"status_msg,omitempty"` + NextTime int64 `json:"next_time,omitempty"` } type Video struct { - Id int64 `json:"id,omitempty"` - Author User `json:"author"` - PlayUrl string `json:"play_url" json:"play_url,omitempty"` - CoverUrl string `json:"cover_url,omitempty"` - FavoriteCount int64 `json:"favorite_count,omitempty"` - CommentCount int64 `json:"comment_count,omitempty"` - IsFavorite bool `json:"is_favorite,omitempty"` + Id int64 `json:"id,omitempty"` + Author dto.User `json:"author"` + PlayUrl string `json:"play_url" json:"play_url,omitempty"` + CoverUrl string `json:"cover_url,omitempty"` + FavoriteCount int64 `json:"favorite_count,omitempty"` + CommentCount int64 `json:"comment_count,omitempty"` + IsFavorite bool `json:"is_favorite,omitempty"` } type Comment struct { - Id int64 `json:"id,omitempty"` - User User `json:"user"` - Content string `json:"content,omitempty"` - CreateDate string `json:"create_date,omitempty"` + Id int64 `json:"id,omitempty"` + User dto.User `json:"dto.user"` + Content string `json:"content,omitempty"` + CreateDate string `json:"create_date,omitempty"` } type User struct { - Id int64 `json:"id,omitempty"` + Id string `json:"id,omitempty"` Name string `json:"name,omitempty"` FollowCount int64 `json:"follow_count,omitempty"` FollowerCount int64 `json:"follower_count,omitempty"` diff --git a/controller/demo_data.go b/controller/demo_data.go index cc10ff36..324530f1 100644 --- a/controller/demo_data.go +++ b/controller/demo_data.go @@ -1,30 +1,45 @@ package controller +import "github.com/BaiZe1998/douyin-simple-demo/dto" + var DemoVideos = []Video{ { Id: 1, - Author: DemoUser, - PlayUrl: "https://www.w3schools.com/html/movie.mp4", + Author: DemoUser1, + PlayUrl: "http://niuyefan.oss-cn-beijing.aliyuncs.com/douyin/bear.mp4", CoverUrl: "https://cdn.pixabay.com/photo/2016/03/27/18/10/bear-1283347_1280.jpg", - FavoriteCount: 0, - CommentCount: 0, + FavoriteCount: 2, + CommentCount: 2, IsFavorite: false, }, } -var DemoComments = []Comment{ - { - Id: 1, - User: DemoUser, - Content: "Test Comment", - CreateDate: "05-01", - }, -} +// var DemoComments = []Comment{ +// { +// Id: 1, +// User: DemoUser1, +// Content: "Test Comment", +// CreateDate: "05-01", +// }, +// { +// Id: 2, +// User: DemoUser1, +// Content: "Test Commen22", +// CreateDate: "05-012", +// }, +// } -var DemoUser = User{ +var DemoUser1 = dto.User{ Id: 1, Name: "TestUser", FollowCount: 0, FollowerCount: 0, IsFollow: false, } +var DemoUser = User{ + Id: "1", + Name: "TestUser", + FollowCount: 0, + FollowerCount: 0, + IsFollow: false, +} diff --git a/controller/favorite.go b/controller/favorite.go index 5f19213f..719702b2 100644 --- a/controller/favorite.go +++ b/controller/favorite.go @@ -7,13 +7,13 @@ import ( // FavoriteAction no practical effect, just check if token is valid func FavoriteAction(c *gin.Context) { - token := c.Query("token") + //token := c.Query("token") - if _, exist := usersLoginInfo[token]; exist { - c.JSON(http.StatusOK, Response{StatusCode: 0}) - } else { - c.JSON(http.StatusOK, Response{StatusCode: 1, StatusMsg: "User doesn't exist"}) - } + //if _, exist := usersLoginInfo[token]; exist { + // c.JSON(http.StatusOK, Response{StatusCode: 0}) + //} else { + // c.JSON(http.StatusOK, Response{StatusCode: 1, StatusMsg: "User doesn't exist"}) + //} } // FavoriteList all users have same favorite video list diff --git a/controller/feed.go b/controller/feed.go index b4ac439a..22559ec5 100644 --- a/controller/feed.go +++ b/controller/feed.go @@ -1,22 +1,25 @@ package controller import ( - "github.com/gin-gonic/gin" "net/http" "time" + + "github.com/gin-gonic/gin" ) type FeedResponse struct { Response - VideoList []Video `json:"video_list,omitempty"` - NextTime int64 `json:"next_time,omitempty"` + VideoList []Video `json:"video_list,omitempty"` + NextTime int64 `json:"next_time,omitempty"` + CommentCount int64 `json:"comment_count,omitempty"` } // Feed same demo video list for every request func Feed(c *gin.Context) { c.JSON(http.StatusOK, FeedResponse{ - Response: Response{StatusCode: 0}, + Response: Response{StatusCode: 0, + StatusMsg: "", + NextTime: time.Now().Unix()}, VideoList: DemoVideos, - NextTime: time.Now().Unix(), }) } diff --git a/controller/publish.go b/controller/publish.go index 6990f85a..9ca8b8b8 100644 --- a/controller/publish.go +++ b/controller/publish.go @@ -1,10 +1,8 @@ package controller import ( - "fmt" "github.com/gin-gonic/gin" "net/http" - "path/filepath" ) type VideoListResponse struct { @@ -14,38 +12,38 @@ type VideoListResponse struct { // Publish check token then save upload file to public directory func Publish(c *gin.Context) { - token := c.PostForm("token") - - if _, exist := usersLoginInfo[token]; !exist { - c.JSON(http.StatusOK, Response{StatusCode: 1, StatusMsg: "User doesn't exist"}) - return - } - - data, err := c.FormFile("data") - if err != nil { - c.JSON(http.StatusOK, Response{ - StatusCode: 1, - StatusMsg: err.Error(), - }) - return - } - - filename := filepath.Base(data.Filename) - user := usersLoginInfo[token] - finalName := fmt.Sprintf("%d_%s", user.Id, filename) - saveFile := filepath.Join("./public/", finalName) - if err := c.SaveUploadedFile(data, saveFile); err != nil { - c.JSON(http.StatusOK, Response{ - StatusCode: 1, - StatusMsg: err.Error(), - }) - return - } - - c.JSON(http.StatusOK, Response{ - StatusCode: 0, - StatusMsg: finalName + " uploaded successfully", - }) + //token := c.PostForm("token") + // + //if _, exist := usersLoginInfo[token]; !exist { + // c.JSON(http.StatusOK, Response{StatusCode: 1, StatusMsg: "User doesn't exist"}) + // return + //} + // + //data, err := c.FormFile("data") + //if err != nil { + // c.JSON(http.StatusOK, Response{ + // StatusCode: 1, + // StatusMsg: err.Error(), + // }) + // return + //} + // + ////filename := filepath.Base(data.Filename) + ////user := usersLoginInfo[token] + //finalName := fmt.Sprintf("%d_%s", user.Id, filename) + //saveFile := filepath.Join("./public/", finalName) + //if err := c.SaveUploadedFile(data, saveFile); err != nil { + // c.JSON(http.StatusOK, Response{ + // StatusCode: 1, + // StatusMsg: err.Error(), + // }) + // return + //} + // + //c.JSON(http.StatusOK, Response{ + // StatusCode: 0, + // StatusMsg: finalName + " uploaded successfully", + //}) } // PublishList all users have same publish video list diff --git a/controller/relation.go b/controller/relation.go index 8dc8e30b..75a904d7 100644 --- a/controller/relation.go +++ b/controller/relation.go @@ -1,42 +1,84 @@ package controller import ( - "github.com/gin-gonic/gin" + "github.com/BaiZe1998/douyin-simple-demo/dto" "net/http" -) + "strconv" -type UserListResponse struct { - Response - UserList []User `json:"user_list"` -} + "github.com/BaiZe1998/douyin-simple-demo/service" + "github.com/gin-gonic/gin" +) // RelationAction no practical effect, just check if token is valid func RelationAction(c *gin.Context) { - token := c.Query("token") + // token := c.Query("token") + + user_id_from_c, _ := c.Get("user_id") + user_id, _ := user_id_from_c.(int64) + to_user_id, _ := strconv.ParseInt(c.Query("to_user_id"), 10, 64) + action_type, _ := strconv.ParseInt(c.Query("action_type"), 10, 64) - if _, exist := usersLoginInfo[token]; exist { - c.JSON(http.StatusOK, Response{StatusCode: 0}) + err := service.FollowAction(c, user_id, to_user_id, int(action_type)) + + if err != nil { + c.JSON(http.StatusBadRequest, dto.Response{ + StatusCode: 1, + StatusMsg: err.Error(), + }) } else { - c.JSON(http.StatusOK, Response{StatusCode: 1, StatusMsg: "User doesn't exist"}) + c.JSON(http.StatusAccepted, dto.Response{ + StatusCode: 1, + StatusMsg: "操作成功", + }) } } // FollowList all users have same follow list func FollowList(c *gin.Context) { - c.JSON(http.StatusOK, UserListResponse{ - Response: Response{ - StatusCode: 0, - }, - UserList: []User{DemoUser}, - }) + userId, _ := strconv.ParseInt(c.Query("user_id"), 10, 64) + + followList, err := service.GetFollowList(c, userId, 1) + + if err == nil { + c.JSON(http.StatusAccepted, dto.UserListResponse{ + Response: dto.Response{ + StatusCode: 0, + StatusMsg: "查找成功", + }, + UserList: followList, + }) + } else { + c.JSON(http.StatusBadRequest, dto.UserListResponse{ + Response: dto.Response{ + StatusCode: 1, + StatusMsg: "查找失败", + }, + UserList: nil, + }) + } } // FollowerList all users have same follower list func FollowerList(c *gin.Context) { - c.JSON(http.StatusOK, UserListResponse{ - Response: Response{ - StatusCode: 0, - }, - UserList: []User{DemoUser}, - }) + toUserId, _ := strconv.ParseInt(c.Query("user_id"), 10, 64) + + followerList, err := service.GetFollowList(c, toUserId, 2) + + if err == nil { + c.JSON(http.StatusAccepted, dto.UserListResponse{ + Response: dto.Response{ + StatusCode: 0, + StatusMsg: "查找成功", + }, + UserList: followerList, + }) + } else { + c.JSON(http.StatusBadRequest, dto.UserListResponse{ + Response: dto.Response{ + StatusCode: 1, + StatusMsg: "查找失败", + }, + UserList: nil, + }) + } } diff --git a/controller/user.go b/controller/user.go index 72fc57ec..ee57d2a0 100644 --- a/controller/user.go +++ b/controller/user.go @@ -1,18 +1,23 @@ package controller import ( + "context" + "github.com/BaiZe1998/douyin-simple-demo/db/model" + "github.com/BaiZe1998/douyin-simple-demo/dto" + "github.com/BaiZe1998/douyin-simple-demo/pkg/util" + "github.com/BaiZe1998/douyin-simple-demo/service" "github.com/gin-gonic/gin" + "log" "net/http" - "sync/atomic" ) // usersLoginInfo use map to store user info, and key is username+password for demo // user data will be cleared every time the server starts // test data: username=zhanglei, password=douyin var usersLoginInfo = map[string]User{ - "zhangleidouyin": { - Id: 1, - Name: "zhanglei", + "tasdgfasdg123456": { + Id: "08dc2b99ef974d47a2554ed3dea73ea0", + Name: "tasdgfasdg", FollowCount: 10, FollowerCount: 5, IsFollow: true, @@ -21,38 +26,38 @@ var usersLoginInfo = map[string]User{ var userIdSequence = int64(1) -type UserLoginResponse struct { - Response - UserId int64 `json:"user_id,omitempty"` - Token string `json:"token"` -} - -type UserResponse struct { - Response - User User `json:"user"` -} - func Register(c *gin.Context) { + username := c.Query("username") password := c.Query("password") - token := username + password + //Password encrypted with salt + password, _ = service.Encryption(password) + + //QueryUser QueryUser By Name for judged user is exit or not + user, _ := model.QueryUserByName(context.Background(), username) - if _, exist := usersLoginInfo[token]; exist { - c.JSON(http.StatusOK, UserLoginResponse{ - Response: Response{StatusCode: 1, StatusMsg: "User already exist"}, + //judege user exit or not + if user.Name != "" { + log.Printf(user.Name, user.ID) + c.JSON(http.StatusOK, dto.UserLoginResponse{ + Response: dto.Response{StatusCode: 1, StatusMsg: "User already exist"}, }) } else { - atomic.AddInt64(&userIdSequence, 1) - newUser := User{ - Id: userIdSequence, - Name: username, + newUser := &model.User{ + Name: username, + Password: password, } - usersLoginInfo[token] = newUser - c.JSON(http.StatusOK, UserLoginResponse{ - Response: Response{StatusCode: 0}, - UserId: userIdSequence, - Token: username + password, + //userinfo register + model.CreateUser(context.Background(), newUser) + //Query Userinfo for get id + userInfo, _ := model.QueryUserByName(context.Background(), username) + //token + token, _ := util.GenerateToken(&util.UserClaims{ID: userInfo.ID, Name: username, PassWord: password}) + c.JSON(http.StatusOK, dto.UserLoginResponse{ + Response: dto.Response{StatusCode: 0}, + UserId: userInfo.ID, + Token: token, }) } } @@ -60,18 +65,28 @@ func Register(c *gin.Context) { func Login(c *gin.Context) { username := c.Query("username") password := c.Query("password") + //Password encrypted with salt + encryptionPassWord, _ := service.Encryption(password) - token := username + password + user, _ := model.QueryUserByName(context.Background(), username) + token, _ := util.GenerateToken(&util.UserClaims{ID: user.ID, Name: username, PassWord: encryptionPassWord}) - if user, exist := usersLoginInfo[token]; exist { - c.JSON(http.StatusOK, UserLoginResponse{ - Response: Response{StatusCode: 0}, - UserId: user.Id, - Token: token, - }) + if user != nil { + //judge password + if service.ComparePasswords(user.Password, password) { + c.JSON(http.StatusOK, dto.UserLoginResponse{ + Response: dto.Response{StatusCode: 0}, + UserId: user.ID, + Token: token, + }) + } else { + c.JSON(http.StatusOK, dto.UserLoginResponse{ + Response: dto.Response{StatusCode: 1, StatusMsg: "password wrong"}, + }) + } } else { - c.JSON(http.StatusOK, UserLoginResponse{ - Response: Response{StatusCode: 1, StatusMsg: "User doesn't exist"}, + c.JSON(http.StatusOK, dto.UserLoginResponse{ + Response: dto.Response{StatusCode: 1, StatusMsg: "User doesn't exist"}, }) } } @@ -79,14 +94,21 @@ func Login(c *gin.Context) { func UserInfo(c *gin.Context) { token := c.Query("token") - if user, exist := usersLoginInfo[token]; exist { - c.JSON(http.StatusOK, UserResponse{ - Response: Response{StatusCode: 0}, - User: user, - }) - } else { - c.JSON(http.StatusOK, UserResponse{ - Response: Response{StatusCode: 1, StatusMsg: "User doesn't exist"}, - }) + userClaims, _ := util.ParseToken(token) + userModel, _ := model.QueryUserById(context.Background(), userClaims.ID) + + user := dto.User{ + Id: userModel.ID, + Name: userModel.Name, + FollowCount: userModel.FollowCount, + FollowerCount: userModel.FollowerCount, + IsFollow: false, } + c.JSON(http.StatusOK, dto.UserResponse{ + Response: dto.Response{StatusCode: 0}, + User: user, + }) + + //log.Printf(token) + //log.Printf(userinfo.ID) } diff --git a/db/cache.go b/db/cache.go new file mode 100644 index 00000000..4aa8fb91 --- /dev/null +++ b/db/cache.go @@ -0,0 +1,43 @@ +package db + +import ( + "context" + "time" + + "github.com/BaiZe1998/douyin-simple-demo/pkg/constants" + "github.com/go-redis/redis/v8" +) + +var RedisCaches map[string]*redis.Client + +func InitRedisPools() error { + RedisCaches := make(map[string]*redis.Client) + for k, v := range constants.RedisDBList { + RedisCaches[k] = redis.NewClient(&redis.Options{ + Addr: constants.RedisDefaultDSN, + Password: constants.RedisDefaultPWD, + DB: v, + PoolSize: 0, + }) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + _, err := RedisCaches[k].Ping(ctx).Result() + + if err != nil { + return err + } + } + + return nil +} + +func CacheSet(ctx context.Context, dbName string, key string, value string, expire uint) error { + err := RedisCaches[dbName].Set(ctx, key, value, time.Duration(expire)).Err() + return err +} + +func CacheGet(ctx context.Context, dbName string, key string) (string, error) { + result, err := RedisCaches[dbName].Get(ctx, key).Result() + return result, err +} diff --git a/db/douyin.sql b/db/douyin.sql new file mode 100644 index 00000000..843c0878 --- /dev/null +++ b/db/douyin.sql @@ -0,0 +1,98 @@ +/* + Navicat Premium Data Transfer + + Source Server : 127.0.0.1 + Source Server Type : MySQL + Source Server Version : 80029 + Source Host : 127.0.0.1:3306 + Source Schema : douyin + + Target Server Type : MySQL + Target Server Version : 80029 + File Encoding : 65001 + + Date: 09/06/2022 23:45:29 +*/ + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for comment +-- ---------------------------- +DROP TABLE IF EXISTS `comment`; +CREATE TABLE `comment` ( + `id` int NOT NULL AUTO_INCREMENT, + `user_id` int DEFAULT NULL COMMENT '用户id', + `video_id` int DEFAULT NULL COMMENT '视频id', + `status` tinyint DEFAULT NULL COMMENT '评论状态', + `content` text CHARACTER SET utf8mb3 COLLATE utf8_general_ci COMMENT '内容', + `created_at` datetime DEFAULT NULL COMMENT '创建时间', + `updated_at` datetime DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_video_id_status` (`video_id`,`status`) USING BTREE COMMENT '获取某个视频的所有评论' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3; + +-- ---------------------------- +-- Table structure for favorite +-- ---------------------------- +DROP TABLE IF EXISTS `favorite`; +CREATE TABLE `favorite` ( + `id` int NOT NULL AUTO_INCREMENT, + `user_id` int DEFAULT NULL COMMENT '用户id', + `video_id` int DEFAULT NULL COMMENT '视频id', + `status` tinyint DEFAULT NULL COMMENT '点赞状态', + `created_at` datetime DEFAULT NULL COMMENT '创建时间', + `updated_at` datetime DEFAULT NULL COMMENT '修改时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_user_id_status_video_id` (`user_id`,`status`,`video_id`) USING BTREE COMMENT '获取某个用户所有点赞视频id' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3; + +-- ---------------------------- +-- Table structure for follow +-- ---------------------------- +DROP TABLE IF EXISTS `follow`; +CREATE TABLE `follow` ( + `id` int NOT NULL AUTO_INCREMENT, + `user_id` int DEFAULT NULL COMMENT '用户id', + `followed_user` int DEFAULT NULL COMMENT '被关注者id', + `status` tinyint DEFAULT '1' COMMENT '关注状态', + `created_at` datetime DEFAULT NULL COMMENT '创建时间', + `updated_at` datetime DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_user_id_status_followed_user` (`user_id`,`status`,`followed_user`) USING BTREE COMMENT '查询关注列表', + KEY `idx_followed_user_status_user_id` (`followed_user`,`status`,`user_id`) USING BTREE COMMENT '查询粉丝列表' +) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb3; + +-- ---------------------------- +-- Table structure for user +-- ---------------------------- +DROP TABLE IF EXISTS `user`; +CREATE TABLE `user` ( + `id` int NOT NULL AUTO_INCREMENT, + `name` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8_general_ci DEFAULT NULL COMMENT '姓名', + `password` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8_general_ci DEFAULT NULL COMMENT '密码', + `follow_count` int DEFAULT NULL COMMENT '关注数', + `follower_count` int DEFAULT NULL COMMENT '粉丝数', + `created_at` datetime DEFAULT NULL COMMENT '创建时间', + PRIMARY KEY (`id`), + KEY `idx_name` (`name`) USING BTREE COMMENT '根据用户名查询信息(如注册)' +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb3; + +-- ---------------------------- +-- Table structure for video +-- ---------------------------- +DROP TABLE IF EXISTS `video`; +CREATE TABLE `video` ( + `id` int NOT NULL AUTO_INCREMENT, + `author_id` int DEFAULT NULL COMMENT '作者id', + `play_url` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8_general_ci DEFAULT NULL COMMENT '播放路径', + `cover_url` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8_general_ci DEFAULT NULL COMMENT '封面路径', + `favorite_count` int DEFAULT NULL COMMENT '喜欢数', + `comment_count` int DEFAULT NULL COMMENT '评论数', + `created_at` datetime DEFAULT NULL COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_author_id` (`author_id`) USING BTREE COMMENT '根据用户id查询所有发布的视频' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3; + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/db/init.go b/db/init.go new file mode 100644 index 00000000..f5c1aadb --- /dev/null +++ b/db/init.go @@ -0,0 +1,9 @@ +package db + +import "github.com/BaiZe1998/douyin-simple-demo/db/model" + +// Init init db +func Init() { + model.Init() // mysql + InitRedisPools() +} diff --git a/db/model/comment.go b/db/model/comment.go new file mode 100644 index 00000000..158c2649 --- /dev/null +++ b/db/model/comment.go @@ -0,0 +1,69 @@ +package model + +import ( + "context" + "fmt" + "time" + + "github.com/BaiZe1998/douyin-simple-demo/dto" + "gorm.io/gorm" +) + +type Comment struct { + ID int64 `gorm:"primarykey"` + UserId int64 + VideoId int64 + Content string + Status int + CreatedAt time.Time + UpdatedAt time.Time +} + +// CreateComment Comment info +func CreateComment(ctx context.Context, comment *Comment) error { + if err := DB.Table("comment").WithContext(ctx).Create(comment).Error; err != nil { + return err + } + return nil +} + +// QueryComment query list of Comment info +func QueryComment(ctx context.Context, videoId int64, limit, offset int) ([]*dto.ResponeComment, int64, error) { + var total int64 + var res []*Comment + var conn *gorm.DB + var responeComment []*dto.ResponeComment + i := 0 + conn = DB.Table("comment").WithContext(ctx).Model(&Comment{}).Where("video_id = ? and status = 1 ", videoId) + if err := conn.Count(&total).Error; err != nil { + return responeComment, total, err + } + if err := conn.Limit(limit).Offset(offset).Find(&res).Error; err != nil { + return responeComment, total, err + } + responeComment = make([]*dto.ResponeComment, len(res)) + for _, v := range res { + userInfo, _ := QueryUserById(context.Background(), v.UserId) + fmt.Println("user:", userInfo.Name) + users := dto.User{ + Id: userInfo.ID, + Name: userInfo.Name, + FollowCount: userInfo.FollowCount, + FollowerCount: userInfo.FollowerCount, + IsFollow: false, + } + responeComment[i] = &dto.ResponeComment{ + ID: v.ID, + User: users, + Content: v.Content, + CreatedAt: v.CreatedAt, + } + i++ + } + return responeComment, total, nil +} + +// DeleteComment delete comment info +func DeleteCommnet(ctx context.Context, commentId int64) error { + return DB.Table("comment").WithContext(ctx).Where("id = ? ", commentId).Update("status", 2).Error +} diff --git a/db/model/favorite.go b/db/model/favorite.go new file mode 100644 index 00000000..7cbfe5b1 --- /dev/null +++ b/db/model/favorite.go @@ -0,0 +1,12 @@ +package model + +import "time" + +type Favorite struct { + ID int64 `gorm:"primarykey"` + UserId int64 + VideoId int64 + Status int + CreatedAt time.Time + UpdatedAt time.Time +} diff --git a/db/model/follow.go b/db/model/follow.go new file mode 100644 index 00000000..5f5275f4 --- /dev/null +++ b/db/model/follow.go @@ -0,0 +1,60 @@ +package model + +import ( + "context" + "time" + + "gorm.io/gorm" +) + +type Follow struct { + ID int64 `gorm:"primarykey"` + UserId int64 + FollowedUser int64 + Status int + CreatedAt time.Time + UpdatedAt time.Time +} + +// CreateFollow create follow info +func CreateFollow(ctx context.Context, follow *Follow) error { + if err := DB.Table("follow").WithContext(ctx).Create(follow).Error; err != nil { + return err + } + return nil +} + +// UpdateFollow update follow info +func UpdateFollow(ctx context.Context, userID, followedUser int64, status *int) error { + params := map[string]interface{}{} + if status != nil { + params["status"] = *status + } + return DB.Table("follow").WithContext(ctx).Model(&Follow{}).Where("user_id = ? and followed_user = ?", userID, followedUser). + Updates(params).Error +} + +// DeleteFollow delete follow info +func DeleteFollow(ctx context.Context, userID int64, followedUser int64) error { + return DB.Table("follow").WithContext(ctx).Where("user_id = ? and followed_user = ? ", userID, followedUser).Delete(&Follow{}).Error +} + +//First +// QueryFollow query list of note info +func QueryFollow(ctx context.Context, userID int64, status, limit, offset int) ([]*Follow, int64, error) { + var total int64 + var res []*Follow + var conn *gorm.DB + if status == 1 { + conn = DB.Table("follow").WithContext(ctx).Model(&Follow{}).Where("user_id = ? and status = 1", userID) + } else { // query for followers + conn = DB.Table("follow").WithContext(ctx).Model(&Follow{}).Where("followed_user = ? and status = 1", userID) + } + if err := conn.Count(&total).Error; err != nil { + return res, total, err + } + if err := conn.Limit(limit).Offset(offset).Find(&res).Error; err != nil { + return res, total, err + } + return res, total, nil +} diff --git a/db/model/init.go b/db/model/init.go new file mode 100644 index 00000000..eda50de4 --- /dev/null +++ b/db/model/init.go @@ -0,0 +1,23 @@ +package model + +import ( + "github.com/BaiZe1998/douyin-simple-demo/pkg/constants" + "gorm.io/driver/mysql" + "gorm.io/gorm" +) + +var DB *gorm.DB + +// Init init DB +func Init() { + var err error + DB, err = gorm.Open(mysql.Open(constants.MySQLLocalDSN), + &gorm.Config{ + PrepareStmt: true, // executes the given query in cached statement + SkipDefaultTransaction: true, // disable default transaction + }, + ) + if err != nil { + panic(err) + } +} diff --git a/db/model/user.go b/db/model/user.go new file mode 100644 index 00000000..214c464a --- /dev/null +++ b/db/model/user.go @@ -0,0 +1,40 @@ +package model + +import ( + "context" + "time" +) + +type User struct { + ID int64 `gorm:"primarykey"` + Name string + Password string + FollowCount int64 + FollowerCount int64 + CreatedAt time.Time +} + +//CteateUser create user info +func CreateUser(ctx context.Context, user *User) error { + if err := DB.Table("user").WithContext(ctx).Create(user).Error; err != nil { + return err + } + return nil +} + +//QueryUser Quert User By Name +func QueryUserByName(ctx context.Context, username string) (*User, error) { + var userInfo *User + if err := DB.Table("user").WithContext(ctx).Where("name=?", username).Find(&userInfo).Error; err != nil { + return userInfo, err + } + return userInfo, nil +} + +func QueryUserById(ctx context.Context, id int64) (*User, error) { + var userInfo *User + if err := DB.Table("user").WithContext(ctx).Where("id=?", id).Find(&userInfo).Error; err != nil { + return userInfo, err + } + return userInfo, nil +} diff --git a/db/model/video.go b/db/model/video.go new file mode 100644 index 00000000..7e15861d --- /dev/null +++ b/db/model/video.go @@ -0,0 +1,24 @@ +package model + +import ( + "context" + "time" +) + +type Video struct { + ID int64 `gorm:"primarykey"` + AuthorID int64 + PlayUrl string + CoverUrl string + FavoriteCount int64 + CommentCount int64 + CreatedAt time.Time +} + +// CreateVideo create video info +func CreateVideo(ctx context.Context, video *Video) error { + if err := DB.Table("follow").WithContext(ctx).Create(video).Error; err != nil { + return err + } + return nil +} diff --git a/documents/database.md b/documents/database.md new file mode 100644 index 00000000..a2ba8030 --- /dev/null +++ b/documents/database.md @@ -0,0 +1,165 @@ +# 数据库设计与技术选型 + +[TOC] + +## MySQL数据库字段 + +> MySQL包含5个表,分别是User、Video、Comment、Favorite、Follow,分别记录用户信息、视频信息、评论信息、点赞信息、关注信息 + +## 数据库表 + +### User + +| 字段 | 类型 | 备注 | +| -------------- | -------- | ---- | +| id | int | | +| name | varchar | 姓名 | +| follow_count | Int | 关注数 | +| follower_count | Int | 粉丝数 | +| create_time | datetime | 创建时间 | + +### Video + +| 字段 | 类型 | 备注 | +| -------------- | -------- | ---- | +| id | int | | +| author_id | Int | 姓名 | +| play_url | varcher | 播放地址 | +| cover_url | varcher | 封面地址 | +| favorite_count | Int | 点赞数 | +| comment_count | int | 评论数 | +| created_at | datetime | 创建时间 | + +### Comment + +| 字段 | 类型 | 备注 | +| ---------- | -------- |-----------| +| id | int | | +| user_id | Int | 用户id | +| video_id | int | 视频id | +| content | text | 评论内容 | +| status | tinyint | 评论状态(1/0) | +| created_at | datetime | 创建时间 | +| updated_at | datetime | 修改时间 | + +### Favorite + +| 字段 | 类型 | 备注 | +| ---------- | -------- | --------- | +| id | int | | +| user_id | Int | 用户id | +| video_id | int | 视频id | +| status | tinyint | 是否点赞(1/0) | +| created_at | datetime | 创建时间 | +| updated_at | datetime | 修改时间 | + +### Follow + +| 字段 | 类型 | 备注 | +| ------------- | -------- | ------ | +| id | int | | +| user_id | Int | 用户id | +| followed_user | int | 被关注者id | +| status | tinyint | 关注状态 | +| created_at | datetime | 创建时间 | +| updated_at | datetime | 修改时间| + +## MongoDB数据库字段 + +> MongoDB数据库主要是用于记录用户之间的关系,即关注和被关注,考虑到在大量用户的场景下,使用MySQL记录和查询用户关系的开销比较大且不够方便,所以考虑使用MongoDB来存放用户之间的关系 +> +> 查询时,直接取出一个用户的关注和被关注列表 + +### 关注列表 + +```json +{ + "user_id": "1", + "follow_list": [ + { + "user_id": "2", + "name": "2号用户" + }, + { + "user_id": "3", + "name": "3号用户" + } + ] +} +``` + +### 被关注列表 + +```json +{ + "user_id": "1", + "follower_list": [ + { + "user_id": "2", + "name": "2号用户" + }, + { + "user_id": "3", + "name": "3号用户" + } + ] +} +``` + +## Redis数据库字段(MongoDB数据库的另一个备选方案) + +> 上述使用MongoDB所完成的用户关系记录也可以考虑使用Redis来完成;Redis和方案相比Mongo方案,查询速度应该是更快的,但是相比之下,查看关注列表和被关注列表可能不是特别常用的操作,这些数据长期占用Redis的内存可能有些浪费。 +> +> 由此也可能有一个更扩展的做法,先从MongoDB读取列表,然后存放在Redis中,分批次加载给用户(一般场景可能是这样的:用户通常不会查看关注列表,若要查看关注列表,则可能会查看多个关注列表) + +### follow库 + +```json +{ + "[user_id]": [ + { + "user_id": "2", + "name": "2号用户" + }, + { + "user_id": "3", + "name": "3号用户" + } + ] +} +``` + +### follower库 + +```json +{ + "[user_id]": [ + { + "user_id": "2", + "name": "2号用户" + }, + { + "user_id": "3", + "name": "3号用户" + } + ] +} +``` + +## 接口字段 + +|接口|依赖的表| +|:---:|:---:| +|douyin/feed|user, video| +|douyin/user/register|user| +|douyin/user/login|user| +|douyin/user|user| +|douyin/publish/action|user, video| +|douyin/publish/list|user, video| +|douyin/favorite/action|video| +|douyin/favorite/list|video, user| +|douyin/comment/action|comment, user| +|douyin/comment/list|comment, user| +|douyin/relation/action|| +|douyin/relation/follow/list|| +|douyin/relation/follower/list|| diff --git a/douyin-simple-demo b/douyin-simple-demo new file mode 100755 index 00000000..e44e3dab Binary files /dev/null and b/douyin-simple-demo differ diff --git a/dto/comment.go b/dto/comment.go new file mode 100644 index 00000000..705498aa --- /dev/null +++ b/dto/comment.go @@ -0,0 +1,29 @@ +package dto + +import ( + "time" +) + +type Comment struct { + Id int64 `json:"id,omitempty"` + User User `json:"user"` + Content string `json:"content,omitempty"` + CreateDate string `json:"create_date,omitempty"` +} + +type CommentListResponse struct { + Response + CommentList []*ResponeComment `json:"comment_list,omitempty"` +} + +type CommentActionResponse struct { + Response + Comment *ResponeComment `json:"comment,omitempty"` +} + +type ResponeComment struct { + ID int64 `json:"id,omitempty"` + User User `json:"user,omitempty"` + Content string `json:"content,omitempty"` + CreatedAt time.Time `json:"create_date,omitempty"` +} diff --git a/dto/common.go b/dto/common.go new file mode 100644 index 00000000..62e89997 --- /dev/null +++ b/dto/common.go @@ -0,0 +1,7 @@ +package dto + +type Response struct { + StatusCode int32 `json:"status_code"` + StatusMsg string `json:"status_msg,omitempty"` + NextTime int64 `json:"next_time,omitempty"` +} diff --git a/dto/user.go b/dto/user.go new file mode 100644 index 00000000..97575878 --- /dev/null +++ b/dto/user.go @@ -0,0 +1,25 @@ +package dto + +type User struct { + Id int64 `json:"id,omitempty"` + Name string `json:"name,omitempty"` + FollowCount int64 `json:"follow_count,omitempty"` + FollowerCount int64 `json:"follower_count,omitempty"` + IsFollow bool `json:"is_follow,omitempty"` +} + +type UserListResponse struct { + Response + UserList []User `json:"user_list"` +} + +type UserLoginResponse struct { + Response + UserId int64 `json:"user_id,omitempty"` + Token string `json:"token"` +} + +type UserResponse struct { + Response + User User `json:"user"` +} diff --git a/dto/video.go b/dto/video.go new file mode 100644 index 00000000..9b452218 --- /dev/null +++ b/dto/video.go @@ -0,0 +1,11 @@ +package dto + +type Video struct { + Id int64 `json:"id,omitempty"` + Author User `json:"author"` + PlayUrl string `json:"play_url" json:"play_url,omitempty"` + CoverUrl string `json:"cover_url,omitempty"` + FavoriteCount int64 `json:"favorite_count,omitempty"` + CommentCount int64 `json:"comment_count,omitempty"` + IsFavorite bool `json:"is_favorite,omitempty"` +} diff --git a/go.mod b/go.mod index 2a0d336e..59c9ba1a 100644 --- a/go.mod +++ b/go.mod @@ -1,15 +1,28 @@ -module github.com/RaymondCode/simple-demo +module github.com/BaiZe1998/douyin-simple-demo go 1.17 -require github.com/gin-gonic/gin v1.7.7 +require ( + github.com/dgrijalva/jwt-go v3.2.0+incompatible + github.com/gin-gonic/gin v1.7.7 + github.com/google/uuid v1.3.0 + gorm.io/driver/mysql v1.3.4 + gorm.io/gorm v1.23.5 +) require ( + github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/garyburd/redigo v1.6.3 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-playground/locales v0.14.0 // indirect github.com/go-playground/universal-translator v0.18.0 // indirect github.com/go-playground/validator/v10 v10.11.0 // indirect + github.com/go-redis/redis/v8 v8.11.5 // indirect + github.com/go-sql-driver/mysql v1.6.0 // indirect github.com/golang/protobuf v1.5.2 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/leodido/go-urn v1.2.1 // indirect github.com/mattn/go-isatty v0.0.14 // indirect diff --git a/go.sum b/go.sum index e398fec5..af578d8a 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,15 @@ +github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/garyburd/redigo v1.6.3 h1:HCeeRluvAgMusMomi1+6Y5dmFOdYV/JzoRrrbFlkGIc= +github.com/garyburd/redigo v1.6.3/go.mod h1:rTb6epsqigu3kYKBnaF028A7Tf/Aw5s0cqA47doKKqw= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs= @@ -17,6 +25,10 @@ github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-playground/validator/v10 v10.11.0 h1:0W+xRM511GY47Yy3bZUbJVitCNg2BOGlCyvTqsp/xIw= github.com/go-playground/validator/v10 v10.11.0/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= +github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= +github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= @@ -24,6 +36,13 @@ github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiu github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= @@ -107,3 +126,8 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/mysql v1.3.4 h1:/KoBMgsUHC3bExsekDcmNYaBnfH2WNeFuXqqrqMc98Q= +gorm.io/driver/mysql v1.3.4/go.mod h1:s4Tq0KmD0yhPGHbZEwg1VPlH0vT/GBHJZorPzhcxBUE= +gorm.io/gorm v1.23.4/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= +gorm.io/gorm v1.23.5 h1:TnlF26wScKSvknUC/Rn8t0NLLM22fypYBlvj1+aH6dM= +gorm.io/gorm v1.23.5/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= diff --git a/main.go b/main.go index 1ad0aa33..3782d4ee 100644 --- a/main.go +++ b/main.go @@ -1,10 +1,17 @@ package main import ( + "github.com/BaiZe1998/douyin-simple-demo/db" "github.com/gin-gonic/gin" ) +func Init() { + db.Init() +} + func main() { + Init() + r := gin.Default() initRouter(r) diff --git a/pkg/constants/constant.go b/pkg/constants/constant.go new file mode 100644 index 00000000..dc95fd18 --- /dev/null +++ b/pkg/constants/constant.go @@ -0,0 +1,15 @@ +package constants + +const ( + MySQLDefaultDSN = "dyweadmin:JXU2MTNGJXU5NzUyJXU5Rjk5JXU2MzA3JXU1RjE1JXU0RjYw@tcp(xinzf0520.top:20607)/douyin?charset=utf8&parseTime=True&loc=Local" + MySQLLocalDSN = "root:root@tcp(localhost:3306)/douyin?charset=utf8&parseTime=True&loc=Local" + + RedisLocalDSN = "localhost:6379" + RedisLocalPWD = "" + RedisDefaultDSN = "localhost:6379" + RedisDefaultPWD = "" +) + +var RedisDBList = map[string]int{ + "Default": 0, +} diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go new file mode 100644 index 00000000..f45f9520 --- /dev/null +++ b/pkg/middleware/middleware.go @@ -0,0 +1,63 @@ +package middleware + +import ( + "fmt" + "net/url" + "strings" + + "github.com/BaiZe1998/douyin-simple-demo/pkg/util" + "github.com/gin-gonic/gin" +) + +func whiteList() map[string]string { + return map[string]string{ + "/douyin/feed/": "GET", + "/douyin/user/register/": "POST", + "/douyin/user/login/": "POST", + } +} + +func withinWhiteList(url *url.URL, method string) bool { + target := whiteList() + queryUrl := strings.Split(fmt.Sprint(url), "?")[0] + if _, ok := target[queryUrl]; ok { + if target[queryUrl] == method { + return true + } + return false + } + return false +} + +func Authorize() gin.HandlerFunc { + return func(c *gin.Context) { + + type QueryToken struct { + Token string `binding:"required" form:"token"` + } + + // 当路由不在白名单内时进行token检测 + if !withinWhiteList(c.Request.URL, c.Request.Method) { + var queryToken QueryToken + if c.ShouldBindQuery(&queryToken) != nil { + c.AbortWithStatusJSON(200, gin.H{ + "status_code": 40001, + "status_msg": "请先登录", + }) + return + } + // 验证token有效性 + userClaims, err := util.ParseToken(queryToken.Token) + if err != nil { + c.AbortWithStatusJSON(200, gin.H{ + "status_code": 40001, + "status_msg": "登陆过期", + }) + return + } + c.Set("token", userClaims) + c.Set("user_id", userClaims.ID) + } + c.Next() + } +} diff --git a/pkg/util/oss_connect.go b/pkg/util/oss_connect.go new file mode 100644 index 00000000..c7d86821 --- /dev/null +++ b/pkg/util/oss_connect.go @@ -0,0 +1 @@ +package util diff --git a/pkg/util/token.go b/pkg/util/token.go new file mode 100644 index 00000000..6cafffcb --- /dev/null +++ b/pkg/util/token.go @@ -0,0 +1,49 @@ +package util + +import ( + "errors" + + "github.com/dgrijalva/jwt-go" +) + +//用户信息类,作为生成token的参数 +type UserClaims struct { + ID int64 `json:"user_id"` + Name string `json:"name"` + PassWord string `json:"password"` + //jwt-go提供的标准claim + jwt.StandardClaims +} + +var ( + //自定义的token秘钥 + secret = []byte("16849841325189456f487") +) + +// 生成token +func GenerateToken(claims *UserClaims) (string, error) { + //生成token + sign, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(secret) + if err != nil { + //这里因为项目接入了统一异常处理,所以使用panic并不会使程序终止,如不接入,可使用原始方式处理错误 + //接入统一异常可参考 https://blog.csdn.net/u014155085/article/details/106733391 + panic(err) + } + return sign, nil +} + +// 解析Token +func ParseToken(tokenString string) (*UserClaims, error) { + //解析token + token, err := jwt.ParseWithClaims(tokenString, &UserClaims{}, func(token *jwt.Token) (interface{}, error) { + return secret, nil + }) + if err != nil { + return nil, err + } + claims, ok := token.Claims.(*UserClaims) + if !ok { + return nil, errors.New("token is invalid") + } + return claims, nil +} diff --git a/router.go b/router.go index 087b1dde..ee238621 100644 --- a/router.go +++ b/router.go @@ -1,7 +1,8 @@ package main import ( - "github.com/RaymondCode/simple-demo/controller" + "github.com/BaiZe1998/douyin-simple-demo/controller" + "github.com/BaiZe1998/douyin-simple-demo/pkg/middleware" "github.com/gin-gonic/gin" ) @@ -10,7 +11,7 @@ func initRouter(r *gin.Engine) { r.Static("/static", "./public") apiRouter := r.Group("/douyin") - + apiRouter.Use(middleware.Authorize()) // basic apis apiRouter.GET("/feed/", controller.Feed) apiRouter.GET("/user/", controller.UserInfo) diff --git a/service/comment.go b/service/comment.go new file mode 100644 index 00000000..3240d921 --- /dev/null +++ b/service/comment.go @@ -0,0 +1,27 @@ +package service + +import ( + "context" + + "github.com/BaiZe1998/douyin-simple-demo/db/model" + "github.com/BaiZe1998/douyin-simple-demo/dto" +) + +func AddComment(text string, users dto.User, videoId int64) *dto.ResponeComment { + + newComment := &model.Comment{ + VideoId: videoId, + UserId: users.Id, + Content: text, + Status: 1, + } + //comment commit + model.CreateComment(context.Background(), newComment) + responseComment := &dto.ResponeComment{ + ID: newComment.ID, + User: users, + Content: text, + CreatedAt: newComment.CreatedAt, + } + return responseComment +} diff --git a/service/comment_test.go b/service/comment_test.go new file mode 100644 index 00000000..8eef8a8b --- /dev/null +++ b/service/comment_test.go @@ -0,0 +1,22 @@ +package service + +import ( + "context" + "fmt" + "testing" + + "github.com/BaiZe1998/douyin-simple-demo/db" + "github.com/BaiZe1998/douyin-simple-demo/db/model" +) + +func TestComment(t *testing.T) { + db.Init() + commentModel := &model.Comment{ + UserId: 1, + VideoId: 1, + Content: "nihao", + } + model.CreateComment(context.Background(), commentModel) + res, total, _ := model.QueryComment(context.Background(), 1, 10, 0) + fmt.Println(res[0].Content, total) +} diff --git a/service/follow.go b/service/follow.go new file mode 100644 index 00000000..d6a361be --- /dev/null +++ b/service/follow.go @@ -0,0 +1,109 @@ +package service + +import ( + "context" + "errors" + "fmt" + "github.com/BaiZe1998/douyin-simple-demo/dto" + + "github.com/BaiZe1998/douyin-simple-demo/db/model" + "gorm.io/gorm" +) + +func IsFollow(ctx context.Context, userID int64, followedUser int64) (bool, bool) { + var relation model.Follow + + if err := model.DB.Table("follow").WithContext(ctx).Where("user_id=? and followed_user=? ", userID, followedUser).First(&relation).Error; err == nil { + if relation.Status == 1 { + return true, true + } else { + return true, false + } + } else { + return false, false + } +} + +func FollowCountAction(ctx context.Context, userID int64, followedUser int64, action_type int) error { + if action_type == 1 { + // 关注操作 + // user_id 关注数+1, to_user_id 被关注数+1 + model.DB.Table("user").WithContext(ctx).Where("id = ?", userID).Update("follow_count", gorm.Expr("follow_count+?", 1)) + model.DB.Table("user").WithContext(ctx).Where("id = ?", followedUser).Update("follower_count", gorm.Expr("follower_count+?", 1)) + } else { + // 取关操作 + model.DB.Table("user").WithContext(ctx).Where("id = ?", userID).Update("follow_count", gorm.Expr("follow_count-?", 1)) + model.DB.Table("user").WithContext(ctx).Where("id = ?", followedUser).Update("follower_count", gorm.Expr("follower_count-?", 1)) + } + + return nil +} + +func FollowAction(ctx context.Context, user_id int64, to_user_id int64, action_type int) error { + is_exist, is_follow := IsFollow(ctx, user_id, to_user_id) + + if !is_exist { + // 不存在的关系直接创建 + new_follow_relation := model.Follow{ + UserId: int64(user_id), + FollowedUser: int64(to_user_id), + Status: int(action_type), + } + + err := model.CreateFollow(ctx, &new_follow_relation) + + if err == nil { + FollowCountAction(ctx, user_id, to_user_id, action_type) + } else { + fmt.Println(err) + } + } else { + // 存在的关系进行更新 + if (action_type == 1 && !is_follow) || (action_type == 2 && is_follow) { + err := model.UpdateFollow(ctx, user_id, to_user_id, &action_type) + + if err == nil { + FollowCountAction(ctx, user_id, to_user_id, action_type) + } else { + fmt.Println(err) + } + } + } + + return nil +} + +func GetFollowList(ctx context.Context, userId int64, actionType uint) ([]dto.User, error) { + var followList []dto.User + + if actionType == 1 { + // 操作类型为获取关注列表 + if err := model.DB.Table("user").WithContext(ctx).Joins("left join follow on user.id = follow.followed_user"). + Select("user.id", "user.name", "user.follow_count", "user.follower_count"). + Where("follow.user_id = ?", userId).Scan(&followList).Error; err != nil { + return followList, nil + } else { + fmt.Println(err) + } + } else if actionType == 2 { + // 操作类型为获取粉丝列表 + if err := model.DB.Table("user").WithContext(ctx).Joins("left join follow on user.id = follow.user_id"). + Select("user.id", "user.name", "user.follow_count", "user.follower_count"). + Where("follow.followed_user = ?", userId).Scan(&followList).Error; err != nil { + return followList, nil + } + } else { + return followList, errors.New("ambiguous actionType") + } + + for i, n := range followList { + isExist, isFollow := IsFollow(ctx, userId, n.Id) + if isExist && isFollow { + followList[i].IsFollow = true + } else { + followList[i].IsFollow = false + } + } + + return followList, nil +} diff --git a/service/follow_test.go b/service/follow_test.go new file mode 100644 index 00000000..07aeac77 --- /dev/null +++ b/service/follow_test.go @@ -0,0 +1,23 @@ +package service + +import ( + "context" + "fmt" + "testing" + + "github.com/BaiZe1998/douyin-simple-demo/db" + "github.com/BaiZe1998/douyin-simple-demo/db/model" +) + +func TestFollow(t *testing.T) { + db.Init() + + followModel := &model.Follow{ + UserId: 1, + FollowedUser: 7, + Status: 1, + } + model.CreateFollow(context.Background(), followModel) + res, total, _ := model.QueryFollow(context.Background(), 1, 1, 10, 0) + fmt.Println(len(res), total) +} diff --git a/service/user.go b/service/user.go new file mode 100644 index 00000000..71690529 --- /dev/null +++ b/service/user.go @@ -0,0 +1,24 @@ +package service + +import "golang.org/x/crypto/bcrypt" + +func Encryption(password string) (pwdHash string, err error) { + pwd := []byte(password) + hash, err := bcrypt.GenerateFromPassword(pwd, bcrypt.MinCost) + if err != nil { + return "", err + } + pwdHash = string(hash) + return pwdHash, nil +} + +// 验证密码 +func ComparePasswords(hashedPwd string, plainPwd string) bool { + byteHash := []byte(hashedPwd) + bytePwd := []byte(plainPwd) + err := bcrypt.CompareHashAndPassword(byteHash, bytePwd) + if err != nil { + return false + } + return true +} diff --git a/service/user_test.go b/service/user_test.go new file mode 100644 index 00000000..196688fb --- /dev/null +++ b/service/user_test.go @@ -0,0 +1,34 @@ +package service + +import ( + "context" + "fmt" + "github.com/BaiZe1998/douyin-simple-demo/db" + "github.com/BaiZe1998/douyin-simple-demo/db/model" + "testing" +) + +func TestUser(t *testing.T) { + db.Init() + //followModel := &model.User{ + // ID: "223", + // Name: "nyf123456", + // PassWord: "12345678", + //} + //model.CreateUser(context.Background(), followModel) + //res, total, _ := model.QueryFollow(context.Background(), "223", 1, 1, 10) + //fmt.Println(res, total) + //re, _ := model.QueryUserById(context.Background(), "08dc2b99ef974d47a2554ed3dea73ea0") + //for index, value := range re { + // fmt.Println("index=", index, "value=", value) + //} + //fmt.Println(*re[0]) + //userModel := &model.User{ + // Name: "nyf123456", + // PassWord: "12345678", + //} + //model.CreateUser(context.Background(), userModel) + res, m := model.QueryUserByName(context.Background(), "nyf") + fmt.Println(res) + fmt.Println(m) +}