├── public ├── data └── bear.mp4 ├── .gitattributes ├── models ├── Response.go ├── MessagePushEvent.go ├── Message.go ├── Follow.go ├── MessageSendEvent.go ├── Video.go ├── Like.go ├── User.go └── Comment.go ├── test ├── Snowflake_test.go ├── sensitive_filter_test.go ├── gorm_test.go ├── common.go ├── rabbitmq_test.go ├── base_api_test.go ├── social_api_test.go └── interact_api_test.go ├── service ├── MessageService.go ├── CommentService.go ├── FavoriteService.go ├── RelationService.go ├── UserService.go ├── VideoService.go └── impl │ ├── MessageServiceImpl.go │ ├── VideoServiceImpl.go │ ├── CommentServiceImpl.go │ ├── UserServiceImpl.go │ ├── FavoriteServiceImpl.go │ └── RelationServiceImpl.go ├── utils ├── CommonEntity.go ├── sensitiveFilter.go ├── resultutil │ └── result.go ├── bloomFilter │ └── bloomFilter.go ├── Snowflake.go ├── init.go ├── RedisTemaplte.go ├── JwtWorker.go ├── AuthAdminCheck.go └── UploadToFTPServer.go ├── .gitignore ├── controller ├── demo_data.go ├── common.go ├── publish.go ├── feed.go ├── message.go ├── relation.go ├── favorite.go ├── comment.go └── user.go ├── config ├── 配置文件的使用.md └── config.go ├── mq ├── rabbitMQ.go ├── commentMQ.go ├── followMQ.go └── likeMQ.go ├── 关注模块优化.md ├── router └── router.go ├── 项目优化提升策略.md ├── main.go ├── 点赞与评论模块性能优化.md ├── go.mod ├── README.md └── sql ├── 20230728v1douyin.sql ├── 20230728v2douyin.sql ├── 20230801v1douyin.sql ├── 20230731v3douyin.sql └── douyin-with-test-data.sql /public/data: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/bear.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guanjunyou/douyin/HEAD/public/bear.mp4 -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /models/Response.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "github.com/RaymondCode/simple-demo/utils" 4 | 5 | type Response struct { 6 | utils.CommonEntity 7 | StatusCode int32 `json:"status_code"` 8 | StatusMsg string `json:"status_msg,omitempty"` 9 | } 10 | -------------------------------------------------------------------------------- /test/Snowflake_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "fmt" 5 | "github.com/RaymondCode/simple-demo/utils" 6 | "testing" 7 | ) 8 | 9 | func TestSnowflake(t *testing.T) { 10 | for i := 0; i < 1000; i++ { 11 | sf := utils.NewSnowflake() 12 | fmt.Println(sf.NextID()) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/sensitive_filter_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "fmt" 5 | "github.com/RaymondCode/simple-demo/utils" 6 | "testing" 7 | ) 8 | 9 | func TestSensitiveFilter(t *testing.T) { 10 | utils.InitFilter() 11 | content := "氟abcdfuck" 12 | contentfiltered := utils.Filter.Replace(content, '*') 13 | fmt.Println(contentfiltered) 14 | } 15 | -------------------------------------------------------------------------------- /service/MessageService.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import "github.com/RaymondCode/simple-demo/models" 4 | 5 | type MessageService interface { 6 | 7 | // SendMessage SendMsg 发送消息 8 | SendMessage(userId int64, toUserId int64, content string) error 9 | 10 | // GetHistoryOfChat 查看消息记录 11 | GetHistoryOfChat(userId int64, toUserId int64) ([]models.MessageDVO, error) 12 | } 13 | -------------------------------------------------------------------------------- /service/CommentService.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import "github.com/RaymondCode/simple-demo/models" 4 | 5 | type CommentService interface { 6 | 7 | // PostComments 登录用户对视频进行评论 8 | //actionType=1-发表评论 ,2-删除评论 9 | PostComments(comment models.Comment, video_id int64) error 10 | 11 | DeleteComments(commentId int64) error 12 | 13 | // CommentList 查看视频的所有评论,按发布时间倒序 14 | CommentList(videoId int64) []models.Comment 15 | } 16 | -------------------------------------------------------------------------------- /service/FavoriteService.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import "github.com/RaymondCode/simple-demo/models" 4 | 5 | type FavoriteService interface { 6 | 7 | // LikeVideo 点赞视频 8 | //actionType=1-点赞,2-取消点赞 9 | LikeVideo(userId int64, vedioId int64, actionType int) error 10 | // QueryVideosOfLike 查询用户的所有点赞视频 11 | QueryVideosOfLike(userId int64) ([]models.LikeVedioListDVO, error) 12 | 13 | FindIsFavouriteByUserIdAndVideoId(userId int64, videoId int64) bool 14 | } 15 | -------------------------------------------------------------------------------- /utils/CommonEntity.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type CommonEntity struct { 8 | Id int64 `json:"id,omitempty"` 9 | CreateDate time.Time `json:"create_date,omitempty"` 10 | IsDeleted int64 `json:"is_deleted"` 11 | } 12 | 13 | func NewCommonEntity() CommonEntity { 14 | sf := NewSnowflake() 15 | return CommonEntity{ 16 | Id: sf.NextID(), 17 | CreateDate: time.Now(), 18 | IsDeleted: 0, 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /utils/sensitiveFilter.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "github.com/RaymondCode/simple-demo/config" 5 | "github.com/importcjj/sensitive" 6 | "log" 7 | ) 8 | 9 | /* 10 | * 11 | 使用方法 content = util.Filter.Replace(content, '#') 12 | */ 13 | var Filter *sensitive.Filter 14 | 15 | func InitFilter() { 16 | Filter = sensitive.New() 17 | err := Filter.LoadWordDict(config.WordDictPath) 18 | if err != nil { 19 | log.Println("InitFilter Fail,Err=" + err.Error()) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /service/RelationService.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import "github.com/RaymondCode/simple-demo/models" 4 | 5 | type RelationService interface { 6 | // FollowUser 关注用户 7 | FollowUser(userId int64, toUserId int64, actionType int) error 8 | 9 | // GetFollows 查询关注列表 10 | GetFollows(userId int64) ([]models.User, error) 11 | 12 | // GetFollowers 查询粉丝列表 13 | GetFollowers(userId int64) ([]models.User, error) 14 | 15 | // GetFriends 查询好友列表 16 | GetFriends(usrId int64) ([]models.User, error) 17 | } 18 | -------------------------------------------------------------------------------- /models/MessagePushEvent.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "github.com/RaymondCode/simple-demo/utils" 4 | 5 | type MessagePushEvent struct { 6 | utils.CommonEntity 7 | FromUserId int64 `json:"user_id,omitempty"` 8 | MsgContent string `json:"msg_content,omitempty"` 9 | } 10 | 11 | func (messagePushEvent *MessagePushEvent) TableName() string { 12 | return "message_push_event" 13 | } 14 | 15 | func SaveMessagePushEvent(messagePushEvent *MessagePushEvent) error { 16 | return utils.GetMysqlDB().Create(messagePushEvent).Error 17 | } 18 | -------------------------------------------------------------------------------- /service/UserService.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/RaymondCode/simple-demo/models" 5 | "github.com/gin-gonic/gin" 6 | ) 7 | 8 | type UserService interface { 9 | GetUserById(Id int64) (models.User, error) 10 | 11 | GetUserByName(name string) (models.User, error) 12 | 13 | Save(user models.User) error 14 | 15 | Register(username string, password string, c *gin.Context) error 16 | 17 | Login(username string, password string, c *gin.Context) error 18 | 19 | UserInfo(userId int64, token string) (*models.User, error) 20 | } 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | config/configuration.yaml 11 | 12 | # Test binary, built with `go test -c` 13 | *.test 14 | 15 | # Output of the go coverage tool, specifically when used with LiteIDE 16 | *.out 17 | 18 | # Dependency directories (remove the comment below to include it) 19 | # vendor/ 20 | 21 | # Go workspace file 22 | go.work 23 | 24 | .idea -------------------------------------------------------------------------------- /test/gorm_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "fmt" 5 | "github.com/RaymondCode/simple-demo/models" 6 | "gorm.io/driver/mysql" 7 | "gorm.io/gorm" 8 | "testing" 9 | ) 10 | 11 | func TestGormTest(t *testing.T) { 12 | dsn := "root@tcp(127.0.0.1:3306)/douyin?charset=utf8mb4&parseTime=True&loc=Local" 13 | db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) 14 | if err != nil { 15 | t.Fatal(err) 16 | } 17 | data := make([]*models.User, 0) 18 | err = db.Find(&data).Error 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | for _, v := range data { 23 | fmt.Printf("Problem ==> %v \n", v) 24 | } 25 | //select {} 26 | } 27 | -------------------------------------------------------------------------------- /utils/resultutil/result.go: -------------------------------------------------------------------------------- 1 | package resultutil 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/RaymondCode/simple-demo/models" 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | func gen(c *gin.Context, code int32, msg string) { 11 | c.JSON(http.StatusOK, models.Response{StatusCode: code, StatusMsg: msg}) 12 | } 13 | 14 | // GenSuccessWithMsg 返回带消息的成功 15 | func GenSuccessWithMsg(c *gin.Context, msg string) { 16 | gen(c, 0, msg) 17 | } 18 | 19 | // GenSuccessWithOutMsg 返回不带消息的成功 20 | func GenSuccessWithOutMsg(c *gin.Context) { 21 | gen(c, 0, "") 22 | } 23 | 24 | // GenFail 返回失败 25 | func GenFail(c *gin.Context, msg string) { 26 | gen(c, 400, msg) 27 | } 28 | -------------------------------------------------------------------------------- /utils/bloomFilter/bloomFilter.go: -------------------------------------------------------------------------------- 1 | package bloomFilter 2 | 3 | import ( 4 | "github.com/RaymondCode/simple-demo/models" 5 | "github.com/bits-and-blooms/bloom/v3" 6 | "strconv" 7 | ) 8 | 9 | var BloomFilter *bloom.BloomFilter 10 | 11 | func InitBloomFilter() { 12 | BloomFilter = bloom.NewWithEstimates(10000000, 0.01) 13 | 14 | //加入全部评论的id 15 | comments := models.GetAllCommentDBs() 16 | for _, comment := range comments { 17 | BloomFilter.Add([]byte(strconv.Itoa(int(comment.Id)))) 18 | } 19 | //加入全部视频的id 20 | videos, _ := models.GetAllExistVideo() 21 | for _, video := range videos { 22 | BloomFilter.Add([]byte(strconv.Itoa(int(video.Id)))) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /models/Message.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "github.com/RaymondCode/simple-demo/utils" 4 | 5 | type Message struct { 6 | utils.CommonEntity 7 | //Id int64 `json:"id,omitempty"` 8 | Content string `json:"content,omitempty"` 9 | } 10 | 11 | type MessageDVO struct { 12 | Id int64 `json:"id,omitempty"` 13 | ToUserId int64 `json:"to_user_id,omitempty"` 14 | UserId int64 `json:"from_user_id,omitempty"` 15 | Content string `json:"content,omitempty"` 16 | CreateTime int64 `json:"create_time,omitempty"` 17 | } 18 | 19 | func (message *Message) TableName() string { 20 | return "message" 21 | } 22 | 23 | func SaveMessage(message *Message) error { 24 | return utils.GetMysqlDB().Create(message).Error 25 | } 26 | -------------------------------------------------------------------------------- /controller/demo_data.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/RaymondCode/simple-demo/models" 5 | "github.com/RaymondCode/simple-demo/utils" 6 | ) 7 | 8 | var DemoVideos = []models.Video{ 9 | { 10 | CommonEntity: utils.NewCommonEntity(), 11 | //Id: 1, 12 | //Author: DemoUser, 13 | PlayUrl: "https://www.w3schools.com/html/movie.mp4", 14 | CoverUrl: "https://cdn.pixabay.com/photo/2016/03/27/18/10/bear-1283347_1280.jpg", 15 | FavoriteCount: 0, 16 | CommentCount: 0, 17 | IsFavorite: false, 18 | }, 19 | } 20 | 21 | var DemoComments = []models.Comment{ 22 | { 23 | CommonEntity: utils.NewCommonEntity(), 24 | //Id: 1, 25 | User: DemoUser, 26 | Content: "Test Comment", 27 | }, 28 | } 29 | 30 | var DemoUser = models.User{ 31 | CommonEntity: utils.NewCommonEntity(), 32 | //Id: 1, 33 | FollowCount: 0, 34 | FollowerCount: 0, 35 | } 36 | -------------------------------------------------------------------------------- /config/配置文件的使用.md: -------------------------------------------------------------------------------- 1 | ## 配置文件的基本使用 2 | 3 | ### 配置的定义 4 | 5 | * 第一步: 在yaml(目前配置文件的位置是在`config/configuration.yaml`)添加配置: 6 | ```yaml 7 | Redis: 8 | Addr: 127.0.0.1:6379 9 | Password: 10 | DB: 0 11 | PoolSize: 100 12 | ``` 13 | 14 | * 第二步: 在config中声明配置结构体(配置项的类型等等) 15 | 16 | ```go 17 | type Configuration struct { 18 | MySQL string `yaml:"MySQL"` 19 | VideoServer VideoServerConfig `yaml:"VideoServerAddr"` 20 | Redis RedisConfig `yaml:"Redis"` 21 | } 22 | 23 | type VideoServerConfig struct { 24 | Addr string `yaml:"Addr"` 25 | } 26 | 27 | type RedisConfig struct { 28 | Addr string `yaml:"Addr"` 29 | Password string `yaml:"Password"` 30 | DB int `yaml:"DB"` 31 | PoolSize int `yaml:"PoolSize"` 32 | } 33 | ``` 34 | 35 | * 第三步: 使用配置文件 36 | 37 | 因为配置文件的读取已经在main.go中通过 38 | `config.ReadConfig()`初始化了 39 | 所以直接调用config中的全局变量: Config调用即可 40 | 41 | 下面是一个例子: 42 | ```go 43 | config.Config.Redis.Addr 44 | ``` -------------------------------------------------------------------------------- /service/VideoService.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/RaymondCode/simple-demo/models" 5 | "github.com/gin-gonic/gin" 6 | "mime/multipart" 7 | "time" 8 | ) 9 | 10 | type VideoService interface { 11 | // Feed 12 | // 通过传入时间戳,当前用户的id,返回对应的视频切片数组,以及视频数组中最早的发布时间 13 | Feed(lastTime time.Time, userId int64) ([]models.Video, time.Time, error) 14 | 15 | // GetVideo 16 | // 传入视频id获得对应的视频对象 17 | GetVideo(videoId int64, userId int64) (models.Video, error) 18 | 19 | // Publish 20 | // 将传入的视频流保存在文件服务器中,并存储在mysql表中 21 | // 5.23 加入title 22 | Publish(data *multipart.FileHeader, userId int64, title string, c *gin.Context) error 23 | 24 | // PublishList 25 | // 通过userId来查询对应用户发布的视频,并返回对应的视频切片数组 26 | PublishList(userId int64) ([]models.VideoDVO, error) 27 | 28 | // GetVideoIdList 29 | // 通过一个作者id,返回该用户发布的视频id切片数组 30 | GetVideoIdList(userId int64) ([]int64, error) 31 | 32 | // GetVideoList 33 | GetVideoListByLastTime(latestTime time.Time) ([]models.VideoDVO, time.Time, error) 34 | } 35 | -------------------------------------------------------------------------------- /models/Follow.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/RaymondCode/simple-demo/utils" 5 | "gorm.io/gorm" 6 | ) 7 | 8 | // Follow 关注关系的item 9 | type Follow struct { 10 | utils.CommonEntity 11 | UserId int64 `json:"UserId"` 12 | FollowUserId int64 `json:"FollowUserId"` 13 | } 14 | 15 | type FollowMQToUser struct { 16 | UserId int64 `json:"user_id"` 17 | FollowUserId int64 `json:"follow_user_id"` 18 | ActionType int `json:"action_type"` 19 | } 20 | 21 | // 表名 22 | func (table *Follow) TableName() string { 23 | return "follow" 24 | } 25 | 26 | // Update 更新 27 | func (f *Follow) Update(tx *gorm.DB) (err error) { 28 | err = tx.Where("id = ?", f.Id).Updates(f).Error 29 | return 30 | } 31 | 32 | // Insert 插入记录 33 | func (f *Follow) Insert(tx *gorm.DB) (err error) { 34 | f.CommonEntity = utils.NewCommonEntity() 35 | err = tx.Create(f).Error 36 | return 37 | } 38 | 39 | // Delete 删除 40 | func (f *Follow) Delete(tx *gorm.DB) (err error) { 41 | err = tx.Where("id = ?", f.Id).Delete(f).Error 42 | return 43 | } 44 | -------------------------------------------------------------------------------- /utils/Snowflake.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | // Snowflake 结构体 9 | type Snowflake struct { 10 | mu sync.Mutex 11 | timestamp int64 // 时间戳部分 12 | machineID int64 // 机器 ID 部分 13 | sequenceID int64 // 序列号部分 14 | } 15 | 16 | // 饿汉式生成唯一的Snowflake实例 17 | var snowflake = &Snowflake{ 18 | timestamp: 0, 19 | machineID: 1, 20 | sequenceID: 0, 21 | } 22 | 23 | // NewSnowflake 函数,返回一个Snowflake 实例 24 | func NewSnowflake() *Snowflake { 25 | return snowflake 26 | } 27 | 28 | // NextID 方法,使用雪花算法生成下一个唯一的 ID 29 | func (sf *Snowflake) NextID() int64 { 30 | sf.mu.Lock() 31 | defer sf.mu.Unlock() 32 | 33 | // 获取当前时间戳,单位为毫秒 34 | now := time.Now().UnixNano() / int64(time.Millisecond) 35 | 36 | // 如果当前时间戳与上次生成的时间戳相同,则序列号递增 37 | if sf.timestamp == now { 38 | sf.sequenceID++ 39 | } else { 40 | // 否则,重置序列号为 0 41 | sf.sequenceID = 0 42 | } 43 | 44 | // 更新时间戳为当前时间戳 45 | sf.timestamp = now 46 | 47 | // 生成 ID,包括时间戳、机器 ID 和序列号部分 48 | ID := (now << 22) | (sf.machineID << 10) | sf.sequenceID 49 | return ID 50 | } 51 | -------------------------------------------------------------------------------- /utils/init.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "github.com/RaymondCode/simple-demo/config" 5 | "github.com/go-redis/redis/v8" 6 | "gorm.io/driver/mysql" 7 | "gorm.io/gorm" 8 | "log" 9 | "time" 10 | ) 11 | 12 | var GORM *gorm.DB 13 | 14 | func CreateGORMDB() { 15 | db, err := gorm.Open(mysql.Open(config.Config.MySQL), &gorm.Config{}) 16 | if err != nil { 17 | log.Println("gorm Init Error : ", err) 18 | } 19 | sqlDb, _ := db.DB() 20 | sqlDb.SetMaxOpenConns(100) 21 | sqlDb.SetMaxIdleConns(25) 22 | sqlDb.SetConnMaxLifetime(1 * time.Minute) 23 | 24 | GORM = db 25 | } 26 | 27 | // GetMysqlDB 需要使用数据库的时候直接创建一个连接 调用此方法即可/** 28 | func GetMysqlDB() *gorm.DB { 29 | return GORM 30 | } 31 | 32 | // GetRedisDB 需要使用数据库的时候直接创建一个连接 调用此方法即可/** 33 | func GetRedisDB() *redis.Client { 34 | return redis.NewClient(&redis.Options{ 35 | Addr: config.Config.Redis.Addr, 36 | Password: config.Config.Redis.Password, // no password set 37 | DB: config.Config.Redis.DB, // use default DB 38 | PoolSize: config.Config.Redis.PoolSize, // 连接池大小 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /mq/rabbitMQ.go: -------------------------------------------------------------------------------- 1 | package mq 2 | 3 | import ( 4 | "fmt" 5 | "github.com/RaymondCode/simple-demo/config" 6 | "github.com/streadway/amqp" 7 | "log" 8 | ) 9 | 10 | var RabbitMqUrl string = "amqp://" + config.Config.RabbitMQ.User + ":" + config.Config.RabbitMQ.Password + "@" + config.Config.RabbitMQ.Addr + 11 | ":" + config.Config.RabbitMQ.Port + "/" 12 | 13 | type RabbitMQ struct { 14 | conn *amqp.Connection 15 | mqurl string 16 | } 17 | 18 | var Rmq *RabbitMQ 19 | 20 | // InitRabbitMQ 初始化RabbitMQ的连接和通道。 21 | func InitRabbitMQ() { 22 | 23 | Rmq = &RabbitMQ{ 24 | mqurl: "amqp://rabbitMqUser:SyjwljgR&d133@114.132.217.209:5672/", 25 | } 26 | dial, err := amqp.Dial(Rmq.mqurl) 27 | Rmq.conn = dial 28 | if err != nil { 29 | log.Println("连接失败") 30 | } 31 | } 32 | 33 | // 关闭mq通道和mq的连接。 34 | func (r *RabbitMQ) destroy() { 35 | r.conn.Close() 36 | } 37 | 38 | // 连接出错时,输出错误信息。 39 | func (r *RabbitMQ) failOnErr(err error, message string) { 40 | if err != nil { 41 | log.Fatalf("%s:%s\n", err, message) 42 | panic(fmt.Sprintf("%s:%s\n", err, message)) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /utils/RedisTemaplte.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/RaymondCode/simple-demo/config" 7 | "time" 8 | ) 9 | 10 | func SaveTokenToRedis(username string, token string, expiration time.Duration) error { 11 | client := GetRedisDB() 12 | ctx := context.Background() 13 | key := fmt.Sprintf("%v%v", config.TokenKey, username) 14 | err := client.Set(ctx, key, token, expiration).Err() 15 | if err != nil { 16 | return err 17 | } 18 | return nil 19 | } 20 | 21 | func GetTokenFromRedis(username string) (string, error) { 22 | client := GetRedisDB() 23 | ctx := context.Background() 24 | key := fmt.Sprintf("%v%v", config.TokenKey, username) 25 | token, err := client.Get(ctx, key).Result() 26 | if err != nil { 27 | return "", err 28 | } 29 | return token, nil 30 | } 31 | 32 | // RefreshToken 刷新token有效期 33 | func RefreshToken(username string, expiration time.Duration) error { 34 | client := GetRedisDB() 35 | ctx := context.Background() 36 | key := fmt.Sprintf("%v%v", config.TokenKey, username) 37 | err := client.Expire(ctx, key, expiration).Err() 38 | if err != nil { 39 | return err 40 | } 41 | return nil 42 | } 43 | -------------------------------------------------------------------------------- /utils/JwtWorker.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "github.com/dgrijalva/jwt-go" 6 | ) 7 | 8 | type UserClaims struct { 9 | CommonEntity 10 | Name string `json:"name"` 11 | jwt.StandardClaims 12 | } 13 | 14 | var myKey = []byte("douyin") 15 | 16 | // GenerateToken 17 | // 生成 token 18 | func GenerateToken(name string, commonEntity CommonEntity) (string, error) { 19 | UserClaim := &UserClaims{ 20 | CommonEntity: commonEntity, 21 | Name: name, 22 | //IsAdmin: isAdmin, 23 | StandardClaims: jwt.StandardClaims{}, 24 | } 25 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, UserClaim) 26 | tokenString, err := token.SignedString(myKey) 27 | if err != nil { 28 | return "", err 29 | } 30 | return tokenString, nil 31 | } 32 | 33 | // AnalyseToken 34 | // 解析 token 35 | func AnalyseToken(tokenString string) (*UserClaims, error) { 36 | userClaim := new(UserClaims) 37 | claims, err := jwt.ParseWithClaims(tokenString, userClaim, func(token *jwt.Token) (interface{}, error) { 38 | return myKey, nil 39 | }) 40 | if err != nil { 41 | return nil, err 42 | } 43 | if !claims.Valid { 44 | return nil, fmt.Errorf("analyse Token Error:%v", err) 45 | } 46 | return userClaim, nil 47 | } 48 | -------------------------------------------------------------------------------- /models/MessageSendEvent.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "github.com/RaymondCode/simple-demo/utils" 4 | 5 | type MessageSendEvent struct { 6 | utils.CommonEntity 7 | UserId int64 `json:"user_id,omitempty"` 8 | ToUserId int64 `json:"to_user_id,omitempty"` 9 | MsgContent string `json:"msg_content,omitempty"` 10 | } 11 | 12 | type ByCreateTime []MessageSendEvent 13 | 14 | func (a ByCreateTime) Len() int { 15 | return len(a) 16 | } 17 | 18 | func (a ByCreateTime) Swap(i, j int) { 19 | a[i], a[j] = a[j], a[i] 20 | } 21 | 22 | func (a ByCreateTime) Less(i, j int) bool { 23 | return a[i].CreateDate.Before(a[j].CreateDate) 24 | } 25 | 26 | func (messageSendEvent *MessageSendEvent) TableName() string { 27 | return "message_send_event" 28 | } 29 | 30 | func SaveMessageSendEvent(messageSendEvent *MessageSendEvent) error { 31 | return utils.GetMysqlDB().Create(messageSendEvent).Error 32 | } 33 | 34 | func FindMessageSendEventByUserIdAndToUserId(userId int64, toUserId int64) ([]MessageSendEvent, error) { 35 | var messageSendEvents []MessageSendEvent 36 | err := utils.GetMysqlDB().Where("user_id = ? AND to_user_id = ?", userId, toUserId).Find(&messageSendEvents).Error 37 | if err != nil { 38 | return nil, err 39 | } 40 | return messageSendEvents, nil 41 | } 42 | -------------------------------------------------------------------------------- /关注模块优化.md: -------------------------------------------------------------------------------- 1 | ### 关注模块优化策略 2 | 3 | #### 数据结构 4 | 5 | FollowSet : key (用户ID) , value (关注对方 ID 集合) 6 | 7 | FollowerSet : key (用户ID),value (关注者的ID 集合) 8 | 9 | 因为后面服务拆分的时候,video和favorite和comment 设置为一个服务模块,所以点赞操作和user之间的消息队列采用 rabbitmq 而不是 channel 10 | 11 | FollowRabbitMQ 12 | 13 | #### 关注 用户: 14 | 15 | (第一步要像点赞那样加分布式锁) 16 | 17 | 1. 先看缓存自己的 FollowSet 中有没有这个ID 18 | 2. 使用 channel 队列异步关注 (生产者消费者模型),往 FollowRabbitMQ 中添加数据,可以用一个值标记是关注(如1),然后返回给用户关注成功 19 | 3. 每个 goroutine 消费完关注数据后,往数据库 follow 表中写入 20 | 4. 往 FollowSet 中增加一个ID,往对方的 FollowerSet 中增加一个ID 21 | 5. User 监听到FollowRabbitMQ 后消费消息,更新 User 表中的关注和被关注数。若更新失败,要同时将 2 - 4 步回滚 22 | 23 | #### 取关用户 24 | 25 | (第一步要像点赞那样加分布式锁) 26 | 27 | 1. 先查一下自己的 FollowSet 中有没有这个ID ,若有则把这个ID去掉,并把对方的 Follower 中的 自己的ID 删掉 28 | 2. 使用channel 队列异步取关 (生产者消费者模型),往 FollowRabbitMQ 中添加数据,可以用一个值标记是关注(如0),然后返回给用户取关成功 29 | 3. 每个 goroutine 消费完关注数据后,往数据库 follow 表中写入删除操作 30 | 4. User 监听到FollowRabbitMQ 后消费消息,更新 User 表中的关注和被关注数。若更新失败,要同时将 2 - 4 步回滚 31 | 5. 若更新成功,先查一下自己的 FollowSet 中有没有这个ID ,若有则把这个ID去掉,并把对方的 Follower 中的 自己的ID 删掉 (缓存延迟双删避免脏数据) 32 | 33 | #### 查询关注列表 34 | 35 | 1. 查一下有没有自己的 FollowSet , 若有则使用协程按照 ID 并发查询组装返回 36 | 2. 若没有则原计划 37 | 38 | #### 查询粉丝列表 39 | 40 | 1. 查一下有没有自己的 FollowerSet , 若有则使用协程按照 ID 并发查询组装返回 41 | 2. 若没有则原计划 42 | 43 | #### 查询好友列表 44 | 45 | 1. 查一下 自己的 FollowSet 和 FollowerSet 是否都存在,若都存在,则取交集 46 | 2. 若其中一个不存在则使用 sql 更新数据后取交集 47 | 48 | -------------------------------------------------------------------------------- /router/router.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "github.com/RaymondCode/simple-demo/controller" 5 | "github.com/gin-gonic/gin" 6 | ) 7 | 8 | func InitRouter1(r *gin.Engine) { 9 | // public directory is used to serve static resources 10 | r.Static("/static", "./public") 11 | 12 | apiRouter := r.Group("/douyin") 13 | 14 | // basic apis 15 | apiRouter.GET("/feed/", controller.Feed) 16 | apiRouter.GET("/user/", controller.UserInfo) 17 | apiRouter.POST("/user/register/", controller.Register) 18 | apiRouter.POST("/user/login/", controller.Login) 19 | apiRouter.POST("/publish/action/", controller.Publish) 20 | apiRouter.GET("/publish/list/", controller.PublishList) 21 | 22 | // extra apis - I 23 | apiRouter.POST("/favorite/action/", controller.FavoriteAction) 24 | apiRouter.GET("/favorite/list/", controller.FavoriteList) 25 | apiRouter.POST("/comment/action/", controller.CommentAction) 26 | apiRouter.GET("/comment/list/", controller.CommentList) 27 | 28 | // extra apis - II 29 | apiRouter.POST("/relation/action/", controller.RelationAction) 30 | apiRouter.GET("/relation/follow/list/", controller.FollowList) 31 | apiRouter.GET("/relation/follower/list/", controller.FollowerList) 32 | apiRouter.GET("/relation/friend/list/", controller.FriendList) 33 | apiRouter.GET("/message/chat/", controller.MessageChat) 34 | apiRouter.POST("/message/action/", controller.MessageAction) 35 | 36 | } 37 | -------------------------------------------------------------------------------- /test/common.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "github.com/gavv/httpexpect/v2" 5 | "net/http" 6 | "testing" 7 | ) 8 | 9 | var serverAddr = "http://localhost:8080" 10 | var testUserA = "douyinTestUserA" 11 | var testUserB = "douyinTestUserB" 12 | 13 | func newExpect(t *testing.T) *httpexpect.Expect { 14 | return httpexpect.WithConfig(httpexpect.Config{ 15 | Client: http.DefaultClient, 16 | BaseURL: serverAddr, 17 | Reporter: httpexpect.NewAssertReporter(t), 18 | Printers: []httpexpect.Printer{ 19 | httpexpect.NewDebugPrinter(t, true), 20 | }, 21 | }) 22 | } 23 | 24 | func getTestUserToken(user string, e *httpexpect.Expect) (int, string) { 25 | registerResp := e.POST("/douyin/user/register/"). 26 | WithQuery("username", user).WithQuery("password", user). 27 | WithFormField("username", user).WithFormField("password", user). 28 | Expect(). 29 | Status(http.StatusOK). 30 | JSON().Object() 31 | 32 | userId := 0 33 | token := registerResp.Value("token").String().Raw() 34 | if len(token) == 0 { 35 | loginResp := e.POST("/douyin/user/login/"). 36 | WithQuery("username", user).WithQuery("password", user). 37 | WithFormField("username", user).WithFormField("password", user). 38 | Expect(). 39 | Status(http.StatusOK). 40 | JSON().Object() 41 | loginToken := loginResp.Value("token").String() 42 | loginToken.Length().Gt(0) 43 | token = loginToken.Raw() 44 | userId = int(loginResp.Value("user_id").Number().Raw()) 45 | } else { 46 | userId = int(registerResp.Value("user_id").Number().Raw()) 47 | } 48 | return userId, token 49 | } 50 | -------------------------------------------------------------------------------- /项目优化提升策略.md: -------------------------------------------------------------------------------- 1 | ## 极简抖音后端项目优化想法 2 | 3 | ### 总体架构 4 | 5 | 1. 添加布隆过滤或者其它策略防止缓存穿透,使用随机 TTL 防止缓存雪崩 ,高并发下保证数据库与缓存一致性 6 | 2. 加入消息队列解耦合,削峰填谷 7 | 3. 整合 pprof 进行性能瓶颈测试 8 | 4. 优化为微服务框架,整合 RPC 9 | 10 | 11 | 12 | ### 关注模块优化策略 13 | 14 | #### 数据结构 15 | 16 | redis :FollowSet : key (用户ID) , value (关注对方 ID 集合) 17 | 18 | redis : FollowerSet : key (用户ID),value (关注者的ID 集合) 19 | 20 | 21 | 22 | FollowRabbitMQ 23 | 24 | #### 关注 用户: 25 | 26 | 1. 先看缓存自己的 FollowSet 中有没有这个ID 27 | 28 | 2. 使用 channel 队列异步关注 (生产者消费者模型),往 FollowRabbitMQ 中添加数据,可以用一个值标记是关注(如1),然后返回给用户关注成功 29 | 30 | 3. 每个 goroutine 消费完关注数据后,往数据库 follow 表中写入 31 | 32 | 4. 往 FollowSet 中增加一个ID,往对方的 FollowerSet 中增加一个ID (用分布式锁保障原子性) 33 | 34 | 5. User 监听到FollowRabbitMQ 后消费消息,更新 User 表中的关注和被关注数。 35 | 36 | 注:若 步骤 3 或 5 中其中一个更新失败则触发重置操作:删除该用户的 FollowSet 和 FollowerSet , 对 3 执行反操作或对 5 进行反操作 , 把对方 的 Follower 删掉自己的ID 37 | 38 | #### 取关用户 39 | 40 | 1. 先查一下自己的 FollowSet 中有没有这个ID ,若有则把这个ID去掉,并把对方的 Follower 中的 自己的ID 删掉 41 | 2. 使用channel 队列异步取关 (生产者消费者模型),往 FollowRabbitMQ 中添加数据,可以用一个值标记是关注(如0),然后返回给用户取关成功 42 | 3. 每个 goroutine 消费完关注数据后,往数据库 follow 表中写入删除操作 43 | 4. User 监听到FollowRabbitMQ 后消费消息,更新 User 表中的关注和被关注数。 44 | 5. 若更新成功,先查一下自己的 FollowSet 中有没有这个ID ,若有则把这个ID去掉,并把对方的 Follower 中的 自己的ID 删掉 (缓存延迟双删避免脏数据) 45 | 46 | #### 查询关注列表 47 | 48 | 1. 查一下有没有自己的 FollowSet , 若有则使用协程按照 ID 并发查询组装返回 49 | 2. 若没有则原计划 50 | 51 | #### 查询粉丝列表 52 | 53 | 1. 查一下有没有自己的 FollowerSet , 若有则使用协程按照 ID 并发查询组装返回 54 | 2. 若没有则原计划 55 | 56 | #### 查询好友列表 57 | 58 | 1. 查一下 自己的 FollowSet 和 FollowerSet 是否都存在,若都存在,则取交集 59 | 2. 若其中一个不存在则使用 sql 更新数据后取交集 60 | 61 | 62 | 63 | #### 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/RaymondCode/simple-demo/config" 5 | "github.com/RaymondCode/simple-demo/controller" 6 | "github.com/RaymondCode/simple-demo/mq" 7 | "github.com/RaymondCode/simple-demo/router" 8 | "github.com/RaymondCode/simple-demo/service/impl" 9 | "github.com/RaymondCode/simple-demo/utils" 10 | "github.com/RaymondCode/simple-demo/utils/bloomFilter" 11 | "github.com/gin-contrib/pprof" 12 | "github.com/gin-gonic/gin" 13 | "github.com/sirupsen/logrus" 14 | "log" 15 | "net/http" 16 | ) 17 | 18 | var SF *utils.Snowflake 19 | 20 | func main() { 21 | initDeps() 22 | config.ReadConfig() 23 | logrus.SetLevel(logrus.DebugLevel) 24 | go impl.RunMessageServer() 25 | r := gin.Default() 26 | r.Use(utils.RefreshHandler()) 27 | r.Use(utils.AuthAdminCheck()) 28 | // 创建一个 Snowflake 实例,并指定机器 ID 29 | SF = utils.NewSnowflake() 30 | router.InitRouter1(r) 31 | pprof.Register(r) 32 | utils.CreateGORMDB() 33 | bloomFilter.InitBloomFilter() 34 | go func() { 35 | log.Println(http.ListenAndServe("localhost:6060", nil)) 36 | }() 37 | r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080") 38 | } 39 | 40 | // 加载项目依赖 41 | func initDeps() { 42 | utils.InitFilter() 43 | 44 | mq.InitRabbitMQ() 45 | 46 | mq.InitLikeRabbitMQ() 47 | mq.InitCommentRabbitMQ() 48 | mq.InitFollowRabbitMQ() 49 | 50 | mq.InitFollowRabbitMQ() 51 | //impl.MakeFollowGroutine() 52 | 53 | mq.MakeLikeChannel() 54 | impl.MakeLikeGroutine() 55 | 56 | mq.MakeCommentChannel() 57 | impl.MakeCommentGoroutine() 58 | 59 | mq.MakeFollowChannel() 60 | impl.MakeFollowGroutine() 61 | 62 | controller.GetUserService().MakeLikeConsumers() 63 | controller.GetUserService().MakeFollowConsumers() 64 | } 65 | -------------------------------------------------------------------------------- /controller/common.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | //type Response struct { 4 | // StatusCode int32 `json:"status_code"` 5 | // StatusMsg string `json:"status_msg,omitempty"` 6 | //} 7 | 8 | //type Video struct { 9 | // Id int64 `json:"id,omitempty"` 10 | // Author User `json:"author"` 11 | // PlayUrl string `json:"play_url" json:"play_url,omitempty"` 12 | // CoverUrl string `json:"cover_url,omitempty"` 13 | // FavoriteCount int64 `json:"favorite_count,omitempty"` 14 | // CommentCount int64 `json:"comment_count,omitempty"` 15 | // IsFavorite bool `json:"is_favorite,omitempty"` 16 | //} 17 | 18 | //type Comment struct { 19 | // Id int64 `json:"id,omitempty"` 20 | // User User `json:"user"` 21 | // Content string `json:"content,omitempty"` 22 | // CreateDate string `json:"create_date,omitempty"` 23 | //} 24 | 25 | //type User struct { 26 | // Id int64 `json:"id,omitempty"` 27 | // Name string `json:"name,omitempty"` 28 | // FollowCount int64 `json:"follow_count,omitempty"` 29 | // FollowerCount int64 `json:"follower_count,omitempty"` 30 | // IsFollow bool `json:"is_follow,omitempty"` 31 | //} 32 | 33 | //type Message struct { 34 | // Id int64 `json:"id,omitempty"` 35 | // Content string `json:"content,omitempty"` 36 | // CreateTime string `json:"create_time,omitempty"` 37 | //} 38 | 39 | //type MessageSendEvent struct { 40 | // UserId int64 `json:"user_id,omitempty"` 41 | // ToUserId int64 `json:"to_user_id,omitempty"` 42 | // MsgContent string `json:"msg_content,omitempty"` 43 | //} 44 | 45 | //type MessagePushEvent struct { 46 | // FromUserId int64 `json:"user_id,omitempty"` 47 | // MsgContent string `json:"msg_content,omitempty"` 48 | //} 49 | -------------------------------------------------------------------------------- /controller/publish.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/RaymondCode/simple-demo/models" 5 | "github.com/RaymondCode/simple-demo/utils" 6 | "github.com/gin-gonic/gin" 7 | "net/http" 8 | "strconv" 9 | ) 10 | 11 | type VideoListResponse struct { 12 | models.Response 13 | VideoList []models.VideoDVO `json:"video_list"` 14 | } 15 | 16 | // Publish check token then save upload file to public directory 17 | func Publish(c *gin.Context) { 18 | //1.获取token并解析出user_id、data、title 19 | token := c.PostForm("token") 20 | userClaims, _ := utils.AnalyseToken(token) 21 | userId := userClaims.CommonEntity.Id 22 | data, err := c.FormFile("data") 23 | if err != nil { 24 | c.JSON(http.StatusOK, models.Response{ 25 | StatusCode: 1, 26 | StatusMsg: err.Error(), 27 | }) 28 | return 29 | } 30 | title := c.PostForm("title") 31 | //2. 调用service层处理业务逻辑 32 | err = GetVideoService().Publish(data, userId, title) 33 | if err != nil { 34 | c.JSON(http.StatusOK, models.Response{ 35 | StatusCode: 1, 36 | StatusMsg: err.Error(), 37 | }) 38 | return 39 | } 40 | c.JSON(http.StatusOK, models.Response{ 41 | StatusCode: 0, 42 | StatusMsg: "投稿成功!", 43 | }) 44 | return 45 | } 46 | 47 | // PublishList all users have same publish video list 48 | func PublishList(c *gin.Context) { 49 | //获取用户id 50 | userId, err := strconv.ParseInt(c.Query("user_id"), 10, 64) 51 | if err != nil { 52 | c.JSON(http.StatusOK, VideoListResponse{ 53 | Response: models.Response{ 54 | StatusCode: 1, 55 | StatusMsg: "类型转换错误", 56 | }, 57 | VideoList: nil, 58 | }) 59 | } 60 | publishList, err := GetVideoService().PublishList(userId) 61 | if err != nil { 62 | c.JSON(http.StatusOK, VideoListResponse{ 63 | Response: models.Response{ 64 | StatusCode: 1, 65 | StatusMsg: "数据库异常", 66 | }, 67 | VideoList: nil, 68 | }) 69 | } 70 | c.JSON(http.StatusOK, VideoListResponse{ 71 | Response: models.Response{ 72 | StatusCode: 0, 73 | StatusMsg: "查询成功", 74 | }, 75 | VideoList: publishList, 76 | }) 77 | } 78 | -------------------------------------------------------------------------------- /controller/feed.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/RaymondCode/simple-demo/models" 5 | "github.com/RaymondCode/simple-demo/service/impl" 6 | "github.com/RaymondCode/simple-demo/utils" 7 | "github.com/gin-gonic/gin" 8 | "log" 9 | "net/http" 10 | "strconv" 11 | "time" 12 | ) 13 | 14 | type FeedResponse struct { 15 | models.Response 16 | VideoList []models.VideoDVO `json:"video_list,omitempty"` 17 | NextTime int64 `json:"next_time,omitempty"` 18 | } 19 | 20 | // 拼装 VideoService 21 | func GetVideoService() impl.VideoServiceImpl { 22 | var videoService impl.VideoServiceImpl 23 | var userService impl.UserServiceImpl 24 | var favoriteServer impl.FavoriteServiceImpl 25 | videoService.UserService = userService 26 | videoService.FavoriteService = favoriteServer 27 | return videoService 28 | } 29 | 30 | // Feed same demo video list for every request 31 | func Feed(c *gin.Context) { 32 | //go mq.LikeRMQ.Publish("hello world") 33 | latestTimeStr := c.Query("latest_time") 34 | token := c.Query("token") 35 | var userId int64 = -1 36 | 37 | log.Printf("时间戳", latestTimeStr) 38 | var latestTime time.Time 39 | if latestTimeStr != "0" { 40 | me, _ := strconv.ParseInt(latestTimeStr, 10, 64) 41 | latestTime = time.Unix(me, 0) 42 | // 前端传入的可能是毫秒级 43 | if latestTime.Year() > 9999 { 44 | latestTime = time.Unix(me/1000, 0) 45 | } 46 | } else { 47 | latestTime = time.Now() 48 | } 49 | log.Printf("获取到的时间 %v", latestTime) 50 | 51 | if token != "" { 52 | userClaims, err0 := utils.AnalyseToken(token) 53 | if err0 != nil { 54 | log.Println("解析token失败") 55 | } 56 | userId = userClaims.CommonEntity.Id 57 | } 58 | 59 | videoDVOList, nextTime, err := GetVideoService().GetVideoListByLastTime(latestTime, userId) 60 | if err != nil { 61 | c.JSON(http.StatusOK, models.Response{ 62 | StatusCode: 1, 63 | StatusMsg: err.Error(), 64 | }) 65 | } 66 | c.JSON(http.StatusOK, FeedResponse{ 67 | Response: models.Response{StatusCode: 0}, 68 | VideoList: videoDVOList, 69 | NextTime: nextTime.Unix(), 70 | }) 71 | } 72 | -------------------------------------------------------------------------------- /models/Video.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/RaymondCode/simple-demo/config" 5 | "github.com/RaymondCode/simple-demo/utils" 6 | "gorm.io/gorm" 7 | "time" 8 | ) 9 | 10 | type Video struct { 11 | utils.CommonEntity 12 | //Id int64 `json:"id,omitempty"` 13 | AuthorId int64 `json:"author_id"` 14 | PlayUrl string `json:"play_url" json:"play_url,omitempty"` 15 | CoverUrl string `json:"cover_url,omitempty"` 16 | FavoriteCount int64 `json:"favorite_count,omitempty"` 17 | CommentCount int64 `json:"comment_count,omitempty"` 18 | IsFavorite bool `json:"is_favorite,omitempty"` 19 | Title string `json:"title,omitempty"` 20 | } 21 | 22 | type VideoDVO struct { 23 | utils.CommonEntity 24 | Author User `json:"author"` 25 | PlayUrl string `json:"play_url"` 26 | CoverUrl string `json:"cover_url"` 27 | FavoriteCount int64 `json:"favorite_count"` 28 | CommentCount int64 `json:"comment_count"` 29 | IsFavorite bool `json:"is_favorite"` 30 | Title string `json:"title,omitempty"` 31 | } 32 | 33 | func (table *Video) TableName() string { 34 | return "video" 35 | } 36 | 37 | // GetVideoListByLastTime 在 model 层禁止操作除了数据库实体类外的其它类! 禁止调用其它model或者service! 38 | func GetVideoListByLastTime(latestTime time.Time) ([]Video, error) { 39 | videolist := make([]Video, config.VideoCount) 40 | err := utils.GetMysqlDB().Where("is_deleted != ? AND create_date < ? ", 1, latestTime).Order("create_date desc").Limit(config.VideoCount).Find(&videolist).Error 41 | if err != nil { 42 | return nil, err 43 | } 44 | return videolist, nil 45 | } 46 | 47 | func SaveVideo(video *Video) error { 48 | err := utils.GetMysqlDB().Create(video).Error 49 | return err 50 | } 51 | 52 | // GetVediosByUserId 根据用户id查询发布的视频 53 | func GetVediosByUserId(userId int64) ([]Video, error) { 54 | vedios := make([]Video, config.VideoCount) 55 | err := utils.GetMysqlDB().Where("author_id = ? AND is_deleted != ?", userId, 1).Find(&vedios).Error 56 | if err != nil { 57 | return nil, err 58 | } 59 | return vedios, nil 60 | } 61 | 62 | func GetVideoById(videoId int64) (Video, error) { 63 | var video Video 64 | err := utils.GetMysqlDB().First(&video, videoId).Error 65 | return video, err 66 | } 67 | 68 | func UpdateVideo(tx *gorm.DB, video Video) { 69 | tx.Save(&video) 70 | } 71 | 72 | func GetAllExistVideo() ([]Video, error) { 73 | var videos []Video 74 | err := utils.GetMysqlDB().Where("is_deleted != ?", 1).Find(&videos).Error 75 | return videos, err 76 | } 77 | -------------------------------------------------------------------------------- /controller/message.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/RaymondCode/simple-demo/models" 5 | "github.com/RaymondCode/simple-demo/service/impl" 6 | "github.com/RaymondCode/simple-demo/utils" 7 | "github.com/gin-gonic/gin" 8 | "net/http" 9 | "strconv" 10 | ) 11 | 12 | func GetMessageService() impl.MessageServiceImpl { 13 | return impl.MessageServiceImpl{} 14 | } 15 | 16 | type MessageListResponse struct { 17 | models.Response 18 | Data []models.MessageDVO `json:"message_list,omitempty"` 19 | } 20 | 21 | func errRespond(c *gin.Context, err error, statusCode int32, statusMsg string) bool { 22 | if err != nil { 23 | c.JSON(http.StatusOK, models.Response{StatusCode: statusCode, StatusMsg: statusMsg}) 24 | return true 25 | } 26 | return false 27 | } 28 | 29 | func responseMessageList(c *gin.Context, messageList []models.MessageDVO) { 30 | c.JSON(http.StatusOK, MessageListResponse{Response: models.Response{StatusCode: 0, StatusMsg: "Message list success"}, Data: messageList}) 31 | } 32 | 33 | // MessageAction no practical effect, just errRespond if token is valid 34 | func MessageAction(c *gin.Context) { 35 | token := c.Query("token") 36 | toUserId := c.Query("to_user_id") 37 | content := c.Query("content") 38 | 39 | userClaim, err := utils.AnalyseToken(token) 40 | if errRespond(c, err, 1, "Token is invalid") { 41 | return 42 | } 43 | 44 | user, err := GetUserService().GetUserByName(userClaim.Name) 45 | if errRespond(c, err, 1, "User doesn't exist") { 46 | return 47 | } 48 | 49 | toUserIdInt64, err := strconv.ParseInt(toUserId, 10, 64) 50 | if errRespond(c, err, 1, "to_user_id is invalid") { 51 | return 52 | } 53 | 54 | err = GetMessageService().SendMessage(user.Id, toUserIdInt64, content) 55 | if errRespond(c, err, 1, "Message send failed") { 56 | return 57 | } 58 | 59 | c.JSON(http.StatusOK, models.Response{StatusCode: 0, StatusMsg: "Message send success"}) 60 | } 61 | 62 | // MessageChat all users have same follow list 63 | func MessageChat(c *gin.Context) { 64 | token := c.Query("token") 65 | toUserId := c.Query("to_user_id") 66 | 67 | userClaim, err := utils.AnalyseToken(token) 68 | if errRespond(c, err, 1, "Token is invalid") { 69 | return 70 | } 71 | 72 | user, err := GetUserService().GetUserByName(userClaim.Name) 73 | if errRespond(c, err, 1, "User doesn't exist") { 74 | return 75 | } 76 | 77 | toUserIdInt64, err := strconv.ParseInt(toUserId, 10, 64) 78 | if errRespond(c, err, 1, "to_user_id is invalid") { 79 | return 80 | } 81 | 82 | messageList, err := GetMessageService().GetHistoryOfChat(user.Id, toUserIdInt64) 83 | if errRespond(c, err, 1, "Message get failed") { 84 | return 85 | } 86 | responseMessageList(c, messageList) 87 | } 88 | -------------------------------------------------------------------------------- /点赞与评论模块性能优化.md: -------------------------------------------------------------------------------- 1 | ## 点赞模块性能优化 2 | 3 | ### 前言 4 | 5 | ​ 点赞模块主要涉及到的业务有: 6 | 7 | - 给视频点赞或者取消点赞 8 | - 获取用户的点赞视频列表 9 | 10 | ​ 以上业务我们现阶段都是使用数据库直接交互的方式,直接访问数据库的性能相对较差,并且相应时间长对用户体验不好。于是,为了提高性能,我们将引入redis、channel以及消息队列(**Rabbitmq**)进行性能提升。同时对于一些循环,我们可以使用协程进行优化。**在模块内通信我们使用channel,模块间通信使用消息队列**。下文中**channel**统一使用管道来称呼。**文中提供的思路不一定切实可行,大家根据具体实际情况适当调整。** 11 | 12 | ### 增删查改约定 13 | 14 | #### 查询 15 | 16 | ​ 查询数据之前先从redis中查,redis中查不到再从数据库中查询,写入缓存中并返回给用户。同时为了解决**缓存穿透**问题,我们可以使用写空值或者布隆过滤的方法。 17 | 18 | #### 新增、删除、更新 19 | 20 | ​ 新增或者更新操作,再发送数据到队列中,监听到消息后调用服务先执行对数据库的修改操作,再更新缓存。删除操作,删除的话先删缓存再发送数据到队列,消费的时候再删数据库,删完数据库再删一次缓存。延迟双删保证缓存与数据库的一致。 21 | 22 | ### 数据结构设计 23 | 24 | - key(userId) set(videoId):用于存储用户点赞的视频id 25 | - LikeRabbitMq 26 | 27 | ### 点赞性能优化 28 | 29 | ​ 0. 添加分布式锁,防止重复点赞或取消点赞 30 | 31 | ​ 1. 先查看redis中有没有这个videoId,没有的话进一步查询数据库 32 | 33 | ​ 2. 当点赞时,将消息分别放入管道中和LikeRabbitMq后,主程序直接返回成功 34 | 35 | ​ 3. 子协程收到管道中的消息后,先往like表和Video中增加数据,之后往redis中添加缓存 36 | 37 | ​ 4. User监听到LikeRabbitMq中的消息后,更新当前用户的favorite_count、视频作者的total_favorited 38 | 39 | ### 取消点赞性能优化 40 | 41 | ​ 0. 添加分布式锁,防止重复点赞或取消点赞 42 | 43 | ​ 1. 先查看redis中有没有这个videoId,没有的话进一步查询数据库 44 | 45 | ​ 2.将消息分别放入管道中和LikeRabbitMq后删除redis相应数据,主程序直接返回成功 46 | 47 | ​ 3.子协程收到管道中的消息后,往like表和Video表中删除数据后,删除redis中对应的数据 48 | 49 | ​ 4.User监听到LikeRabbitMq中的消息后,更新当前用户的favorite_count、视频作者的total_favorited 50 | 51 | ### 获取用户的点赞视频列表 52 | 53 | ​ 直接从redis中根据key:userId进行查询。根据其是否存在,分为两种情况。若存在,则直接统计value中元素的数量,返回即可;若不存在,则先查询数据库,然后写入缓存并返回。 54 | 55 | ## 评论模块性能优化 56 | 57 | ### 前言 58 | 59 | 评论模块主要涉及的业务有: 60 | 61 | - 发表评论或者删除评论 62 | - 获取评论列表 63 | 64 | 以上业务中现阶段,我们都是通过直接访问数据库的方式实现的,特别是在获取评论列表这一功能中,当数据量大的时候直接查询数据库性能会特别差。于是,为了提高性能,我们将引入redis、channel以及消息队列(**Rabbitmq**)进行性能提升。**在模块内通信我们使用channel,模块间通信使用消息队列**。同时对于一些循环,我们可以使用协程进行优化。下文中统一使用**队列**来称呼,涉及到具体实现再自己区分。**文中提供的思路不一定切实可行,大家根据具体实际情况适当调整。** 65 | 66 | ### 增删查改约定 67 | 68 | #### 查询 69 | 70 | ​ 查询数据之前先从redis中查,redis中查不到再从数据库中查询,写入缓存中并返回给用户。同时为了解决**缓存穿透**问题,我们可以使用写空值或者布隆过滤的方法。 71 | 72 | #### 新增、删除、更新 73 | 74 | ​ 新增或者更新操作,再发送数据到队列中,监听到消息后调用服务先执行对数据库的修改操作,再更新缓存。删除操作,删除的话先删缓存再发送数据到队列,消费的时候再删数据库,删完数据库再删一次缓存。延迟双删保证缓存与数据库的一致。 75 | 76 | ### 数据结构设计 77 | 采用两级存储结构 78 | ​ key(videoId)-zset(评论Id,score值使用创建时间) 79 | key(评论id)--string(评论的实例对象) 80 | 查询时查出评论id后,可以根据第二种数据结构查出评论的实体 81 | 具体在设计key时需要添加公共前缀,如comment 82 | 83 | ### 发表评论性能优化 84 | ​ 1. 将消息分别放入channel中后,查询User封装结果后主程序直接返回 85 | 86 | ​ 2. 子协程收到channel中的消息后,先往comment表和Video中增加数据,之后往redis中两种数据结构添加缓存, 87 | 88 | ### 删除评论性能优化 89 | 90 | ​ 1. 先查看redis中有没有这个评论,没有的话进一步查询数据库,防止构造请求删除不存在的评论 91 | 92 | ​ 2. 将消息分别放入channel中后删除redis中的缓存,主程序直接返回 93 | 94 | ​ 3. 子协程收到channel中的消息后,先更新comment表和Video表的数据,之后删除redis中相应的缓存 95 | 96 | ### 查看评论列表优化 97 | 98 | ​ 直接从redis中根据key:videoId进行查询第一种数据结构。根据其是否存在,分为两种情况。若存在,将每一个评论的实体类从第二种数据结构中查出来拼接返回;若不存在,则先查询数据库,然后写入缓存并返回。 99 | -------------------------------------------------------------------------------- /models/Like.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/RaymondCode/simple-demo/utils" 5 | "gorm.io/gorm" 6 | ) 7 | 8 | // 点赞接口的参数 9 | type Like struct { 10 | utils.CommonEntity 11 | VideoId int64 `json:"videoId" gorm:"column:video_id"` //点赞的视频 12 | UserId int64 `json:"userId" gorm:"column:user_id"` //点赞的用户 13 | } 14 | 15 | type VideoListResponse2 struct { 16 | Response 17 | VideoList []LikeVedioListDVO `json:"video_list"` 18 | } 19 | 20 | type LikeVedioListDVO struct { 21 | Video 22 | Author *User `json:"author" gorm:"foreignKey:AuthorId"` 23 | } 24 | 25 | type LikeMQToVideo struct { 26 | UserId int64 `json:"user_id"` 27 | VideoId int64 `json:"video_id"` 28 | ActionType int `json:"action_type"` 29 | } 30 | 31 | type LikeMQToUser struct { 32 | UserId int64 `json:"user_id"` 33 | VideoId int64 `json:"video_id"` 34 | AuthorId int64 `json:"author_id"` 35 | ActionType int `json:"action_type"` 36 | } 37 | 38 | // 表名 39 | func (table *Like) TableName() string { 40 | return "like" 41 | } 42 | 43 | // Update 更新 44 | func (l *Like) Update(tx *gorm.DB) (err error) { 45 | err = tx.Where("id = ?", l.Id).Updates(l).Error 46 | return 47 | } 48 | 49 | // Insert 插入记录 50 | func (l *Like) Insert(tx *gorm.DB) (err error) { 51 | l.CommonEntity = utils.NewCommonEntity() 52 | err = tx.Create(l).Error 53 | return 54 | } 55 | 56 | // Delete 删除 57 | func (l *Like) Delete(tx *gorm.DB) (err error) { 58 | l.IsDeleted = 1 59 | return l.Update(tx) 60 | } 61 | 62 | // FindByUserIdAndVedioId 通过userId和VedioId查找 63 | func (l *Like) FindByUserIdAndVedioId() (res *Like, err error) { 64 | res = &Like{} 65 | err = utils.GetMysqlDB().Model(Like{}).Where("video_id = ? and user_id = ? and is_deleted = 0", l.VideoId, l.UserId).Find(res).Error 66 | return 67 | } 68 | 69 | func (l *Like) CountByUserIdAndVedioId(tx *gorm.DB) (res *Like, err error) { 70 | res = &Like{} 71 | err = tx.Model(Like{}).Where("video_id = ? and user_id = ? and is_deleted = 0", l.VideoId, l.UserId).Find(res).Error 72 | return 73 | } 74 | 75 | // GetLikeVedioListDVO 查询喜欢的视频列表 76 | func (l *Like) GetLikeVedioListDVO(userId int64) ([]LikeVedioListDVO, error) { 77 | tx := utils.GetMysqlDB() 78 | var err error 79 | res := make([]LikeVedioListDVO, 0) 80 | err = tx.Table("`like` l").Select("v.*").Joins(`LEFT JOIN video v ON l.video_id = v.id`).Where("l.user_id = ? and l.is_deleted = 0", userId).Preload("Author").Find(&res).Error 81 | 82 | return res, err 83 | } 84 | 85 | // GetLikeVedioListDVO 查询喜欢的视频Id 86 | func (l *Like) GetLikeVedioIdList(userId int64) ([]int64, error) { 87 | tx := utils.GetMysqlDB() 88 | var err error 89 | res := make([]int64, 0) 90 | err = tx.Table(l.TableName()).Select("video_id").Where("user_id = ? and is_deleted = 0", userId).Find(&res).Error 91 | 92 | return res, err 93 | } 94 | -------------------------------------------------------------------------------- /mq/commentMQ.go: -------------------------------------------------------------------------------- 1 | package mq 2 | 3 | import ( 4 | "github.com/RaymondCode/simple-demo/config" 5 | "github.com/RaymondCode/simple-demo/models" 6 | "github.com/streadway/amqp" 7 | "log" 8 | ) 9 | 10 | type CommentMQ struct { 11 | RabbitMQ 12 | channel *amqp.Channel 13 | queueName string 14 | exchange string 15 | key string 16 | } 17 | 18 | var CommentChannel chan models.CommentMQToVideo 19 | 20 | func MakeCommentChannel() { 21 | ch := make(chan models.CommentMQToVideo, config.BufferSize) 22 | CommentChannel = ch 23 | } 24 | 25 | // NewCommentRabbitMQ 获取commentMQ的对应管道。 26 | func NewCommentRabbitMQ() *CommentMQ { 27 | commentMQ := &CommentMQ{ 28 | RabbitMQ: *Rmq, 29 | queueName: "commentMQ", 30 | } 31 | ch, err := commentMQ.conn.Channel() 32 | commentMQ.channel = ch 33 | Rmq.failOnErr(err, "获取通道失败") 34 | return commentMQ 35 | } 36 | 37 | // Publish 评论操作的发布配置。 38 | func (commentMQ *CommentMQ) Publish(message string) { 39 | 40 | _, err := commentMQ.channel.QueueDeclare( 41 | commentMQ.queueName, 42 | //是否持久化 43 | true, 44 | //是否为自动删除 45 | false, 46 | //是否具有排他性 47 | false, 48 | //是否阻塞 49 | false, 50 | //额外属性 51 | nil, 52 | ) 53 | if err != nil { 54 | panic(err) 55 | } 56 | 57 | err1 := commentMQ.channel.Publish( 58 | commentMQ.exchange, 59 | commentMQ.queueName, 60 | false, 61 | false, 62 | amqp.Publishing{ 63 | ContentType: "text/plain", 64 | Body: []byte(message), 65 | }) 66 | if err1 != nil { 67 | panic(err) 68 | } 69 | 70 | } 71 | 72 | // Consumer 评论关系的消费逻辑。 73 | func (commentMQ *CommentMQ) Consumer() { 74 | 75 | _, err := commentMQ.channel.QueueDeclare(commentMQ.queueName, true, false, false, false, nil) 76 | 77 | if err != nil { 78 | panic(err) 79 | } 80 | 81 | //2、接收消息 82 | messages, err1 := commentMQ.channel.Consume( 83 | commentMQ.queueName, 84 | //用来区分多个消费者 85 | "", 86 | //是否自动应答 87 | true, 88 | //是否具有排他性 89 | false, 90 | //如果设置为true,表示不能将同一个connection中发送的消息传递给这个connection中的消费者 91 | false, 92 | //消息队列是否阻塞 93 | false, 94 | nil, 95 | ) 96 | if err1 != nil { 97 | panic(err1) 98 | } 99 | go commentMQ.consumer(messages) 100 | //forever := make(chan bool) 101 | log.Println(messages) 102 | 103 | log.Printf("[*] Waiting for messagees,To exit press CTRL+C") 104 | 105 | //<-forever 106 | 107 | } 108 | func (commentMQ *CommentMQ) consumer(message <-chan amqp.Delivery) { 109 | for d := range message { 110 | log.Println(string(d.Body)) 111 | } 112 | } 113 | 114 | var commentRMQ *CommentMQ 115 | 116 | // InitCommentRabbitMQ 初始化rabbitMQ连接。 117 | func InitCommentRabbitMQ() { 118 | commentRMQ = NewCommentRabbitMQ() 119 | commentRMQ.Publish("hello word !") 120 | go commentRMQ.Consumer() 121 | } 122 | -------------------------------------------------------------------------------- /test/rabbitmq_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "fmt" 5 | "github.com/RaymondCode/simple-demo/mq" 6 | "github.com/streadway/amqp" 7 | "log" 8 | "testing" 9 | ) 10 | 11 | func TestRabbitMQ(t *testing.T) { 12 | 13 | //mq.RabbitMqUrl = "amqp://rabbitMqUser:SyjwljgR&d133@114.132.217.209:5672/" 14 | conn, err := amqp.Dial(mq.RabbitMqUrl) 15 | fmt.Println(mq.RabbitMqUrl) 16 | if err != nil { 17 | log.Fatalf("Failed to connect to RabbitMQ: %v", err) 18 | } 19 | defer conn.Close() 20 | 21 | ch, err := conn.Channel() 22 | if err != nil { 23 | log.Fatalf("Failed to open a channel: %v", err) 24 | } 25 | defer ch.Close() 26 | 27 | queueName := "commentMQ" 28 | q, err := ch.QueueDeclare( 29 | queueName, // 队列名称 30 | true, // 是否持久化 31 | false, // 是否自动删除 32 | false, // 是否具有排他性 33 | false, // 是否阻塞等待 34 | nil, // 额外属性 35 | ) 36 | if err != nil { 37 | log.Fatalf("Failed to declare a queue: %v", err) 38 | } 39 | 40 | message := "Hello, RabbitMQ!" 41 | err = ch.Publish( 42 | "", // 交换机名称 43 | q.Name, // 队列名称 44 | false, // 如果设置为 true,则根据 routingKey 在队列中查找对应的队列名称 45 | false, // 是否阻塞等待 46 | amqp.Publishing{ 47 | ContentType: "text/plain", 48 | Body: []byte(message), 49 | }, 50 | ) 51 | if err != nil { 52 | log.Fatalf("Failed to publish a message: %v", err) 53 | } 54 | 55 | log.Printf("Sent message to queue %s: %s", queueName, message) 56 | 57 | } 58 | 59 | func TestConsumeRabbitMQ(t *testing.T) { 60 | conn, err := amqp.Dial("amqp://rabbitMqUser:SyjwljgR&d133@114.132.217.209:5672/") // 连接到 RabbitMQ 服务器 61 | if err != nil { 62 | log.Fatalf("Failed to connect to RabbitMQ: %v", err) 63 | } 64 | defer conn.Close() 65 | 66 | ch, err := conn.Channel() // 打开一个通道 67 | if err != nil { 68 | log.Fatalf("Failed to open a channel: %v", err) 69 | } 70 | defer ch.Close() 71 | 72 | queueName := "commentMQ" // 要监听的队列名称 73 | q, err := ch.QueueDeclare( 74 | queueName, // 队列名称 75 | true, // 持久化队列 76 | false, // 非自动删除队列 77 | false, // 非独占队列 78 | false, // 不等待服务器响应 79 | nil, // 额外的属性 80 | ) 81 | if err != nil { 82 | log.Fatalf("Failed to declare a queue: %v", err) 83 | } 84 | 85 | // 接收消息 86 | msgs, err := ch.Consume( 87 | q.Name, // 队列名称 88 | "", // 消费者标识,为空表示使用默认标识 89 | true, // 自动应答,即消费消息后自动向 RabbitMQ 确认消息已接收 90 | false, // 非独占队列 91 | false, // 不等待服务器响应 92 | false, // 额外的属性 93 | nil, // 额外的属性 94 | ) 95 | if err != nil { 96 | log.Fatalf("Failed to consume messages from queue: %v", err) 97 | } 98 | 99 | // 处理接收到的消息 100 | for msg := range msgs { 101 | log.Printf("Received a message: %s", msg.Body) 102 | } 103 | 104 | } 105 | 106 | func TestPublishMQ(t *testing.T) { 107 | mq.LikeRMQ.Publish("hello world") 108 | } 109 | -------------------------------------------------------------------------------- /controller/relation.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "fmt" 5 | "github.com/RaymondCode/simple-demo/models" 6 | "github.com/RaymondCode/simple-demo/service" 7 | "github.com/RaymondCode/simple-demo/service/impl" 8 | "github.com/RaymondCode/simple-demo/utils" 9 | "github.com/gin-gonic/gin" 10 | "github.com/sirupsen/logrus" 11 | "log" 12 | "net/http" 13 | "strconv" 14 | ) 15 | 16 | type UserListResponse struct { 17 | models.Response 18 | UserList []models.User `json:"user_list"` 19 | } 20 | 21 | func NewRelationService() service.RelationService { 22 | return &impl.RelationServiceImpl{ 23 | Logger: logrus.New(), 24 | } 25 | } 26 | 27 | // RelationAction no practical effect, just check if token is valid 28 | func RelationAction(c *gin.Context) { 29 | token := c.Query("token") 30 | toUserId := c.Query("to_user_id") 31 | actionType := c.Query("action_type") 32 | fmt.Println("RelationAction: ", token, toUserId, actionType) 33 | 34 | userClaims, _ := utils.AnalyseToken(token) 35 | toUserIdInt, _ := strconv.ParseInt(toUserId, 10, 64) 36 | actionTypeInt, _ := strconv.Atoi(actionType) 37 | 38 | err := NewRelationService().FollowUser(userClaims.CommonEntity.Id, toUserIdInt, actionTypeInt) 39 | if err != nil { 40 | c.JSON(http.StatusOK, models.Response{ 41 | StatusCode: 1, 42 | StatusMsg: err.Error(), 43 | }) 44 | return 45 | } 46 | c.JSON(http.StatusOK, models.Response{ 47 | StatusCode: 0, 48 | StatusMsg: "", 49 | }) 50 | } 51 | 52 | // FollowList all users have same follow list 53 | func FollowList(c *gin.Context) { 54 | userId := c.Query("user_id") 55 | userIdInt, _ := strconv.ParseInt(userId, 10, 64) 56 | followUser, err := NewRelationService().GetFollows(userIdInt) 57 | if err != nil { 58 | log.Printf("GetFollows fail") 59 | } 60 | c.JSON(http.StatusOK, UserListResponse{ 61 | Response: models.Response{ 62 | StatusCode: 0, 63 | }, 64 | UserList: followUser, 65 | }) 66 | } 67 | 68 | // FollowerList all users have same follower list 69 | func FollowerList(c *gin.Context) { 70 | userId := c.Query("user_id") 71 | userIdInt, _ := strconv.ParseInt(userId, 10, 64) 72 | followUser, err := NewRelationService().GetFollowers(userIdInt) 73 | if err != nil { 74 | log.Printf("GetFollows fail") 75 | } 76 | c.JSON(http.StatusOK, UserListResponse{ 77 | Response: models.Response{ 78 | StatusCode: 0, 79 | }, 80 | UserList: followUser, 81 | }) 82 | } 83 | 84 | // FriendList all users have same friend list 85 | func FriendList(c *gin.Context) { 86 | userId := c.Query("user_id") 87 | userIdInt, _ := strconv.ParseInt(userId, 10, 64) 88 | followUser, err := NewRelationService().GetFriends(userIdInt) 89 | if err != nil { 90 | log.Printf("GetFollows fail") 91 | } 92 | c.JSON(http.StatusOK, UserListResponse{ 93 | Response: models.Response{ 94 | StatusCode: 0, 95 | }, 96 | UserList: followUser, 97 | }) 98 | } 99 | -------------------------------------------------------------------------------- /controller/favorite.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "strconv" 8 | 9 | "github.com/RaymondCode/simple-demo/models" 10 | "github.com/RaymondCode/simple-demo/service/impl" 11 | "github.com/RaymondCode/simple-demo/utils" 12 | "github.com/RaymondCode/simple-demo/utils/resultutil" 13 | "github.com/gin-gonic/gin" 14 | ) 15 | 16 | // 接收点赞的结构体 17 | type FavoriteActionReq struct { 18 | Token string `form:"token"` 19 | VideoId string `form:"video_id"` // 视频id 20 | ActionType string `form:"action_type"` // 1-点赞,2-取消点赞 21 | } 22 | 23 | // FavoriteAction no practical effect, just check if token is valid 24 | func FavoriteAction(c *gin.Context) { 25 | 26 | var faReq FavoriteActionReq 27 | if err := c.ShouldBind(&faReq); err != nil { 28 | log.Printf("点赞操作,绑定参数发生异常:%v \n", err) 29 | resultutil.GenFail(c, "参数错误") 30 | return 31 | } 32 | fmt.Printf("参数 %+v \n", faReq) 33 | 34 | videoId, err := strconv.ParseInt(faReq.VideoId, 10, 64) 35 | 36 | if err != nil { 37 | log.Printf("点赞操作,videoId字符串转换发生异常 = %v", err) 38 | resultutil.GenFail(c, "参数错误") 39 | return 40 | } 41 | 42 | // 从Token中获取Uid 43 | var userClaim *utils.UserClaims 44 | userClaim, err = utils.AnalyseToken(faReq.Token) 45 | 46 | if err != nil { 47 | log.Printf("解析token发生异常 = %v", err) 48 | return 49 | } 50 | userId := userClaim.CommonEntity.Id 51 | 52 | var actionType int 53 | actionType, err = strconv.Atoi(faReq.ActionType) 54 | 55 | if err != nil { 56 | log.Printf("点赞操作,actionType字符串转换发生异常 = %v", err) 57 | resultutil.GenFail(c, "参数错误") 58 | return 59 | } 60 | 61 | var fs impl.FavoriteServiceImpl 62 | if err = fs.LikeVideo(userId, videoId, actionType); err != nil { 63 | log.Printf("点赞发生异常 = %v", err) 64 | if err.Error() == "-1" { 65 | resultutil.GenFail(c, "该视频已点赞") 66 | return 67 | } 68 | 69 | if err.Error() == "-2" { 70 | resultutil.GenFail(c, "未找到要取消的点赞记录") 71 | return 72 | } 73 | 74 | resultutil.GenFail(c, "点赞发生错误") 75 | return 76 | } 77 | 78 | resultutil.GenSuccessWithOutMsg(c) 79 | } 80 | 81 | // FavoriteList all users have same favorite video list 82 | func FavoriteList(c *gin.Context) { 83 | 84 | userIdStr := c.Query("user_id") 85 | 86 | userId, err := strconv.ParseInt(userIdStr, 10, 64) 87 | 88 | if err != nil { 89 | log.Printf("获取喜欢列表,userId字符串转换发生异常 = %v", err) 90 | resultutil.GenFail(c, "参数错误") 91 | return 92 | } 93 | 94 | var fs impl.FavoriteServiceImpl 95 | res, err := fs.QueryVideosOfLike(userId) 96 | 97 | if err != nil { 98 | log.Printf("获取喜欢列表,获取发生异常 = %v", err) 99 | resultutil.GenFail(c, "获取失败") 100 | return 101 | } 102 | 103 | c.JSON(http.StatusOK, models.VideoListResponse2{ 104 | Response: models.Response{ 105 | StatusCode: 0, 106 | }, 107 | VideoList: res, 108 | }) 109 | } 110 | -------------------------------------------------------------------------------- /models/User.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "github.com/RaymondCode/simple-demo/config" 7 | "github.com/RaymondCode/simple-demo/utils" 8 | "gorm.io/gorm" 9 | "strconv" 10 | "time" 11 | ) 12 | 13 | type User struct { 14 | utils.CommonEntity 15 | //Id int64 `json:"id,omitempty"` 16 | Name string `json:"name"` 17 | FollowCount int64 `json:"follow_count"` 18 | FollowerCount int64 `json:"follower_count"` 19 | Phone string `json:"phone"` 20 | Password string `json:"password"` 21 | Avatar string `json:"avatar"` 22 | Gender int `json:"gender"` 23 | Age int `json:"age"` 24 | Nickname string `json:"nickname"` 25 | Signature string `json:"signature"` 26 | TotalFavorited int64 `json:"total_favorited"` 27 | WorkCount int64 `json:"work_count"` 28 | FavoriteCount int64 `json:"favorite_count"` 29 | IsFollow bool `json:"is_follow"` 30 | BackgroundImage string `json:"background_image"` 31 | } 32 | 33 | func (table *User) TableName() string { 34 | return "user" 35 | } 36 | 37 | func GetUserById(Id int64) (User, error) { 38 | var user User 39 | userKey := config.UserKey + strconv.FormatInt(Id, 10) 40 | userStr, errfind := utils.GetRedisDB().Get(context.Background(), userKey).Result() 41 | if errfind == nil { 42 | errUnmarshal := json.Unmarshal([]byte(userStr), &user) 43 | if errUnmarshal != nil { 44 | return user, errUnmarshal 45 | } 46 | return user, nil 47 | } 48 | // 传参禁止直接字符串拼接,防止SQL注入 49 | err := utils.GetMysqlDB().Where("id = ? AND is_deleted != ?", Id, 1).First(&user).Error 50 | if err != nil { 51 | return user, err 52 | } 53 | jsonStr, _ := json.Marshal(user) 54 | utils.GetRedisDB().Set(context.Background(), userKey, jsonStr, time.Duration(config.UsedrKeyTTL)*time.Second) 55 | return user, nil 56 | } 57 | 58 | func GetUserByName(name string) (User, error) { 59 | var user User 60 | // 传参禁止直接字符串拼接,防止SQL注入 61 | err := utils.GetMysqlDB().Where("name = ? AND is_deleted != ?", name, 1).First(&user).Error 62 | if err != nil { 63 | return user, err 64 | } 65 | return user, nil 66 | } 67 | 68 | func SaveUser(user User) error { 69 | err := utils.GetMysqlDB().Create(&user).Error 70 | if err != nil { 71 | return err 72 | } 73 | userStr, _ := json.Marshal(user) 74 | userKey := config.UserKey + strconv.FormatInt(user.Id, 10) 75 | utils.GetRedisDB().Set(context.Background(), userKey, userStr, time.Duration(config.UsedrKeyTTL)*time.Second) 76 | return nil 77 | } 78 | 79 | func UpdateUser(tx *gorm.DB, user User) error { 80 | err := tx.Save(&user).Error 81 | if err != nil { 82 | return err 83 | } 84 | userStr, _ := json.Marshal(user) 85 | userKey := config.UserKey + strconv.FormatInt(user.Id, 10) 86 | utils.GetRedisDB().Set(context.Background(), userKey, userStr, time.Duration(config.UsedrKeyTTL)*time.Second) 87 | return nil 88 | } 89 | -------------------------------------------------------------------------------- /utils/AuthAdminCheck.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "github.com/RaymondCode/simple-demo/config" 5 | "github.com/gin-gonic/gin" 6 | "net/http" 7 | "time" 8 | ) 9 | 10 | // 免登录接口列表 11 | var notAuthArr = map[string]string{ 12 | "/douyin/feed/": "1", 13 | "/douyin/user/register/": "1", 14 | "/douyin/user/login/": "1", 15 | } 16 | 17 | /* 18 | * 19 | token刷新 20 | */ 21 | func RefreshHandler() gin.HandlerFunc { 22 | return func(c *gin.Context) { 23 | //1.获取token 24 | token := c.Query("token") 25 | //如果token为空则尝试从body中拿 26 | if token == "" { 27 | token = c.PostForm("token") 28 | } 29 | //2.判断是否携带token 30 | if token == "" { 31 | return 32 | } 33 | //3.解析token 34 | userClaims, err := AnalyseToken(token) 35 | if err != nil || userClaims == nil || userClaims.IsDeleted == 1 { 36 | return 37 | } 38 | //4.根据token查redis 39 | tokenFromRedis, err := GetTokenFromRedis(userClaims.Name) 40 | if tokenFromRedis == "" { 41 | //4.1 如果token可以被正确解析,重建redis缓存 42 | err := SaveTokenToRedis(userClaims.Name, token, time.Duration(config.TokenTTL*float64(time.Second))) 43 | if err != nil { 44 | 45 | c.JSON(http.StatusForbidden, gin.H{"StatusCode": "1", "StatusMsg": "服务器异常"}) 46 | c.Abort() 47 | return 48 | } 49 | return 50 | } 51 | //6.刷新token的有效期 52 | err = RefreshToken(userClaims.Name, time.Duration(config.TokenTTL*float64(time.Second))) 53 | if err != nil { 54 | c.JSON(http.StatusOK, gin.H{"StatusCode": "1", "StatusMsg": "用户未登录"}) 55 | return 56 | } 57 | c.Next() 58 | } 59 | } 60 | 61 | /* 62 | * 63 | 登录校验 64 | */ 65 | func AuthAdminCheck() gin.HandlerFunc { 66 | return func(c *gin.Context) { 67 | //1.不用登录的接口直接放行 68 | //log.Println(c.Request.URL.Path) 69 | inWhite := notAuthArr[c.Request.URL.Path] 70 | if inWhite == "1" { 71 | return 72 | } 73 | //2.获取token 74 | token := c.Query("token") 75 | //如果token为空则尝试从body中拿 76 | if token == "" { 77 | token = c.PostForm("token") 78 | } 79 | userClaims, err := AnalyseToken(token) 80 | if err != nil || userClaims == nil || userClaims.IsDeleted == 1 { 81 | c.JSON(http.StatusOK, gin.H{"StatusCode": "1", "StatusMsg": "用户未登录"}) 82 | //阻止该请求 83 | c.Abort() 84 | return 85 | } 86 | //3.根据token查redis 87 | tokenFromRedis, err := GetTokenFromRedis(userClaims.Name) 88 | if tokenFromRedis == "" || err != nil { 89 | c.JSON(http.StatusOK, gin.H{"StatusCode": "1", "StatusMsg": "用户未登录"}) 90 | //阻止该请求 91 | c.Abort() 92 | return 93 | } 94 | c.Next() 95 | } 96 | } 97 | 98 | ///* 99 | //* 100 | //鉴权 101 | //*/ 102 | //func AuthAdminCheck(token string) error { 103 | // claims, err := AnalyseToken(token) 104 | // if err != nil || claims == nil { 105 | // log.Printf("Can not find this token !") 106 | // return errors.New("Can not find this token !") 107 | // } 108 | // return nil 109 | //} 110 | -------------------------------------------------------------------------------- /utils/UploadToFTPServer.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/RaymondCode/simple-demo/config" 8 | "io" 9 | "io/ioutil" 10 | "mime/multipart" 11 | "net/http" 12 | "path/filepath" 13 | "strings" 14 | ) 15 | 16 | type ResponseBody struct { 17 | Url string `json:"url"` 18 | CoverUrl string `json:"cover_url"` 19 | } 20 | 21 | func UploadToServer(data *multipart.FileHeader) (string, error) { 22 | fileName := data.Filename 23 | fileType := getFileCategory(getFileType(fileName)) 24 | // 构建HTTP请求的Body 25 | body := &bytes.Buffer{} 26 | writer := multipart.NewWriter(body) 27 | 28 | // 添加文件数据 29 | file, _ := data.Open() 30 | defer file.Close() 31 | 32 | part, err := writer.CreateFormFile("file", data.Filename) 33 | if err != nil { 34 | return "", fmt.Errorf("error writing file to request: %w", err) 35 | } 36 | _, err = io.Copy(part, file) 37 | if err != nil { 38 | return "", fmt.Errorf("error copying file to request: %w", err) 39 | } 40 | 41 | // 添加fileType参数 42 | err = writer.WriteField("fileType", fmt.Sprintf("%d", fileType)) 43 | if err != nil { 44 | return "", fmt.Errorf("error writing filetype to request: %w", err) 45 | } 46 | 47 | // 结束写入 48 | err = writer.Close() 49 | if err != nil { 50 | return "", fmt.Errorf("error closing writer: %w", err) 51 | } 52 | 53 | req, err := http.NewRequest( 54 | config.Config.VideoServer.Api.Upload.Method, 55 | "http://"+config.Config.VideoServer.Addr+config.Config.VideoServer.Api.Upload.Path, 56 | body, 57 | ) 58 | if err != nil { 59 | return "", fmt.Errorf("error creating request: %w", err) 60 | } 61 | 62 | // 设置请求头 63 | req.Header.Set("Content-Type", writer.FormDataContentType()) 64 | 65 | // 发起请求 66 | client := &http.Client{} 67 | resp, err := client.Do(req) 68 | if err != nil { 69 | return "", fmt.Errorf("error sending request: %w", err) 70 | } 71 | respBody, err := ioutil.ReadAll(resp.Body) 72 | fmt.Printf("Response: %s\n", respBody) 73 | defer resp.Body.Close() 74 | 75 | // 处理响应 76 | if resp.StatusCode != http.StatusOK { 77 | return "", fmt.Errorf("resource upload failed, HTTP status code: %d", resp.StatusCode) 78 | } 79 | 80 | var responseBody ResponseBody 81 | err = json.Unmarshal(respBody, &responseBody) 82 | fmt.Println("资源保存成功!") 83 | return responseBody.CoverUrl, nil 84 | } 85 | 86 | func getFileType(filename string) string { 87 | // 使用path/filepath包的Ext函数获取文件扩展名 88 | extension := filepath.Ext(filename) 89 | 90 | // 去除扩展名中的点号,并转换为小写字母 91 | fileType := strings.ToLower(strings.TrimPrefix(extension, ".")) 92 | 93 | return fileType 94 | } 95 | 96 | func getFileCategory(fileType string) int { 97 | // 视频类型判断 98 | videoTypes := []string{"mp4", "avi", "mov"} 99 | for _, t := range videoTypes { 100 | if fileType == t { 101 | return 1 // 视频类返回1 102 | } 103 | } 104 | 105 | // 图片类型判断 106 | imageTypes := []string{"jpg", "jpeg", "png", "gif"} 107 | for _, t := range imageTypes { 108 | if fileType == t { 109 | return 2 // 图片类返回2 110 | } 111 | } 112 | 113 | // 其他类型 114 | return 0 115 | } 116 | -------------------------------------------------------------------------------- /test/base_api_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "net/http" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func TestFeed(t *testing.T) { 12 | e := newExpect(t) 13 | 14 | feedResp := e.GET("/douyin/feed/").Expect().Status(http.StatusOK).JSON().Object() 15 | feedResp.Value("status_code").Number().Equal(0) 16 | feedResp.Value("video_list").Array().Length().Gt(0) 17 | 18 | for _, element := range feedResp.Value("video_list").Array().Iter() { 19 | video := element.Object() 20 | video.ContainsKey("id") 21 | video.ContainsKey("author") 22 | video.Value("play_url").String().NotEmpty() 23 | video.Value("cover_url").String().NotEmpty() 24 | } 25 | } 26 | 27 | func TestUserAction(t *testing.T) { 28 | e := newExpect(t) 29 | 30 | rand.Seed(time.Now().UnixNano()) 31 | registerValue := fmt.Sprintf("douyin%d", rand.Intn(65536)) 32 | 33 | registerResp := e.POST("/douyin/user/register/"). 34 | WithQuery("username", registerValue).WithQuery("password", registerValue). 35 | WithFormField("username", registerValue).WithFormField("password", registerValue). 36 | Expect(). 37 | Status(http.StatusOK). 38 | JSON().Object() 39 | registerResp.Value("status_code").Number().Equal(0) 40 | registerResp.Value("user_id").Number().Gt(0) 41 | registerResp.Value("token").String().Length().Gt(0) 42 | 43 | loginResp := e.POST("/douyin/user/login/"). 44 | WithQuery("username", registerValue).WithQuery("password", registerValue). 45 | WithFormField("username", registerValue).WithFormField("password", registerValue). 46 | Expect(). 47 | Status(http.StatusOK). 48 | JSON().Object() 49 | loginResp.Value("status_code").Number().Equal(0) 50 | loginResp.Value("user_id").Number().Gt(0) 51 | loginResp.Value("token").String().Length().Gt(0) 52 | 53 | token := loginResp.Value("token").String().Raw() 54 | userResp := e.GET("/douyin/user/"). 55 | WithQuery("token", token). 56 | Expect(). 57 | Status(http.StatusOK). 58 | JSON().Object() 59 | userResp.Value("status_code").Number().Equal(0) 60 | userInfo := userResp.Value("user").Object() 61 | userInfo.NotEmpty() 62 | userInfo.Value("id").Number().Gt(0) 63 | userInfo.Value("name").String().Length().Gt(0) 64 | } 65 | 66 | func TestPublish(t *testing.T) { 67 | e := newExpect(t) 68 | 69 | userId, token := getTestUserToken(testUserA, e) 70 | 71 | publishResp := e.POST("/douyin/publish/action/"). 72 | WithMultipart(). 73 | WithFile("data", "../public/bear.mp4"). 74 | WithFormField("token", token). 75 | WithFormField("title", "Bear"). 76 | Expect(). 77 | Status(http.StatusOK). 78 | JSON().Object() 79 | publishResp.Value("status_code").Number().Equal(0) 80 | 81 | publishListResp := e.GET("/douyin/publish/list/"). 82 | WithQuery("user_id", userId).WithQuery("token", token). 83 | Expect(). 84 | Status(http.StatusOK). 85 | JSON().Object() 86 | publishListResp.Value("status_code").Number().Equal(0) 87 | publishListResp.Value("video_list").Array().Length().Gt(0) 88 | 89 | for _, element := range publishListResp.Value("video_list").Array().Iter() { 90 | video := element.Object() 91 | video.ContainsKey("id") 92 | video.ContainsKey("author") 93 | video.Value("play_url").String().NotEmpty() 94 | video.Value("cover_url").String().NotEmpty() 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "gopkg.in/yaml.v2" 5 | "io/ioutil" 6 | "log" 7 | "os" 8 | ) 9 | 10 | type Configuration struct { 11 | MySQL string `yaml:"MySQL"` 12 | VideoServer VideoServerConfig `yaml:"VideoServer"` 13 | Redis RedisConfig `yaml:"Redis"` 14 | RabbitMQ RabbitMQConfig `yaml:"RabbitMQ"` 15 | } 16 | 17 | type RedisConfig struct { 18 | Addr string `yaml:"Addr"` 19 | Password string `yaml:"Password"` 20 | DB int `yaml:"DB"` 21 | PoolSize int `yaml:"PoolSize"` 22 | } 23 | 24 | type RabbitMQConfig struct { 25 | Addr string `yaml:"Addr"` 26 | User string `yaml:"User"` 27 | Port string `yaml:"Port"` 28 | Password string `yaml:"Password"` 29 | } 30 | 31 | type VideoServerConfig struct { 32 | Addr2 string `yaml:"Addr2"` //拼接play_url 33 | Addr string `yaml:"Addr"` 34 | Api struct { 35 | Upload struct { 36 | Path string `yaml:"Path"` // /ftpServer/upload/ 37 | Method string `yaml:"Method"` // POST 38 | } `yaml:"Upload"` 39 | } `yaml:"Api"` 40 | } 41 | 42 | var Config Configuration 43 | 44 | var ( 45 | DefaultPage = "1" 46 | DefaultSize = "20" 47 | VideoCount = 5 48 | BufferSize = 1000 49 | // redis 50 | TokenTTL float64 = 3600 51 | TokenKey string = "token:" 52 | LikeKey string = "Like:" 53 | LikeKeyTTL float64 = 3600 54 | LikeLock string = "likeLock" 55 | UnLikeLock string = "unLikeLock" 56 | LikeLockTTL float64 = 60 57 | UserKey string = "user:" 58 | UsedrKeyTTL float64 = 3600 59 | FollowLock string = "followLock:" 60 | UnFollowLock string = "unFollowLock:" 61 | FollowLockTTL float64 = 60 62 | FollowKey string = "follow:" 63 | FollowerKey string = "follower:" 64 | FollowKeyTTL float64 = 3600 65 | 66 | //filter 67 | WordDictPath = "./public/sensitiveDict.txt" 68 | ) 69 | 70 | var MailPassword = os.Getenv("MailPassword") 71 | 72 | type ProblemBasic struct { 73 | Identity string `json:"identity"` // 问题表的唯一标识 74 | Title string `json:"title"` // 问题标题 75 | Content string `json:"content"` // 问题内容 76 | ProblemCategories []int `json:"problem_categories"` // 关联问题分类表 77 | MaxRuntime int `json:"max_runtime"` // 最大运行时长 78 | MaxMem int `json:"max_mem"` // 最大运行内存 79 | TestCases []*TestCase `json:"test_cases"` // 关联测试用例表 80 | } 81 | 82 | type TestCase struct { 83 | Input string `json:"input"` // 输入 84 | Output string `json:"output"` // 输出 85 | } 86 | 87 | var ( 88 | DateLayout = "2006-01-02 15:04:05" 89 | ValidGolangPackageMap = map[string]struct{}{ 90 | "bytes": {}, 91 | "fmt": {}, 92 | "math": {}, 93 | "sort": {}, 94 | "strings": {}, 95 | } 96 | ) 97 | 98 | // 首字母大写其他的包才能调用 99 | func ReadConfig() { 100 | configFile, err := ioutil.ReadFile("config/configuration.yaml") 101 | if err != nil { 102 | log.Fatalf("Error reading config file: %v", err) 103 | } 104 | err = yaml.Unmarshal(configFile, &Config) 105 | if err != nil { 106 | log.Fatalf("Error parsing config file: %v", err) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /test/social_api_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "net/http" 6 | "testing" 7 | ) 8 | 9 | func TestRelation(t *testing.T) { 10 | e := newExpect(t) 11 | 12 | userIdA, tokenA := getTestUserToken(testUserA, e) 13 | userIdB, tokenB := getTestUserToken(testUserB, e) 14 | 15 | relationResp := e.POST("/douyin/relation/action/"). 16 | WithQuery("token", tokenA).WithQuery("to_user_id", userIdB).WithQuery("action_type", 1). 17 | WithFormField("token", tokenA).WithFormField("to_user_id", userIdB).WithFormField("action_type", 1). 18 | Expect(). 19 | Status(http.StatusOK). 20 | JSON().Object() 21 | relationResp.Value("status_code").Number().Equal(0) 22 | 23 | followListResp := e.GET("/douyin/relation/follow/list/"). 24 | WithQuery("token", tokenA).WithQuery("user_id", userIdA). 25 | WithFormField("token", tokenA).WithFormField("user_id", userIdA). 26 | Expect(). 27 | Status(http.StatusOK). 28 | JSON().Object() 29 | followListResp.Value("status_code").Number().Equal(0) 30 | 31 | containTestUserB := false 32 | for _, element := range followListResp.Value("user_list").Array().Iter() { 33 | user := element.Object() 34 | user.ContainsKey("id") 35 | if int(user.Value("id").Number().Raw()) == userIdB { 36 | containTestUserB = true 37 | } 38 | } 39 | assert.True(t, containTestUserB, "Follow test user failed") 40 | 41 | followerListResp := e.GET("/douyin/relation/follower/list/"). 42 | WithQuery("token", tokenB).WithQuery("user_id", userIdB). 43 | WithFormField("token", tokenB).WithFormField("user_id", userIdB). 44 | Expect(). 45 | Status(http.StatusOK). 46 | JSON().Object() 47 | followerListResp.Value("status_code").Number().Equal(0) 48 | 49 | containTestUserA := false 50 | for _, element := range followerListResp.Value("user_list").Array().Iter() { 51 | user := element.Object() 52 | user.ContainsKey("id") 53 | if int(user.Value("id").Number().Raw()) == userIdA { 54 | containTestUserA = true 55 | } 56 | } 57 | assert.True(t, containTestUserA, "Follower test user failed") 58 | } 59 | 60 | func TestChat(t *testing.T) { 61 | e := newExpect(t) 62 | 63 | userIdA, tokenA := getTestUserToken(testUserA, e) 64 | userIdB, tokenB := getTestUserToken(testUserB, e) 65 | 66 | messageResp := e.POST("/douyin/message/action/"). 67 | WithQuery("token", tokenA).WithQuery("to_user_id", userIdB).WithQuery("action_type", 1).WithQuery("content", "Send to UserB"). 68 | WithFormField("token", tokenA).WithFormField("to_user_id", userIdB).WithFormField("action_type", 1).WithQuery("content", "Send to UserB"). 69 | Expect(). 70 | Status(http.StatusOK). 71 | JSON().Object() 72 | messageResp.Value("status_code").Number().Equal(0) 73 | 74 | chatResp := e.GET("/douyin/message/chat/"). 75 | WithQuery("token", tokenA).WithQuery("to_user_id", userIdB). 76 | WithFormField("token", tokenA).WithFormField("to_user_id", userIdB). 77 | Expect(). 78 | Status(http.StatusOK). 79 | JSON().Object() 80 | chatResp.Value("status_code").Number().Equal(0) 81 | chatResp.Value("message_list").Array().Length().Gt(0) 82 | 83 | chatResp = e.GET("/douyin/message/chat/"). 84 | WithQuery("token", tokenB).WithQuery("to_user_id", userIdA). 85 | WithFormField("token", tokenB).WithFormField("to_user_id", userIdA). 86 | Expect(). 87 | Status(http.StatusOK). 88 | JSON().Object() 89 | chatResp.Value("status_code").Number().Equal(0) 90 | chatResp.Value("message_list").Array().Length().Gt(0) 91 | } 92 | -------------------------------------------------------------------------------- /controller/comment.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/RaymondCode/simple-demo/models" 5 | "github.com/RaymondCode/simple-demo/service/impl" 6 | "github.com/RaymondCode/simple-demo/utils" 7 | "github.com/gin-gonic/gin" 8 | "log" 9 | "net/http" 10 | "strconv" 11 | ) 12 | 13 | func GetCommentService() impl.CommentServiceImpl { 14 | return impl.CommentServiceImpl{} 15 | } 16 | 17 | type CommentListResponse struct { 18 | models.Response 19 | CommentList []models.Comment `json:"comment_list,omitempty"` 20 | } 21 | 22 | type CommentActionResponse struct { 23 | models.Response 24 | Comment models.Comment `json:"comment,omitempty"` 25 | } 26 | 27 | func parseVideoId(c *gin.Context) int64 { 28 | video_id, err := strconv.ParseInt(c.Query("video_id"), 10, 64) 29 | if err != nil { 30 | log.Println(err) 31 | c.JSON(http.StatusBadRequest, models.Response{StatusCode: 1, StatusMsg: "video_id is invalid"}) 32 | return -1 33 | } 34 | return video_id 35 | } 36 | 37 | func parseCommetId(c *gin.Context) int64 { 38 | comment_id, err := strconv.ParseInt(c.Query("comment_id"), 10, 64) 39 | if err != nil { 40 | log.Println(err) 41 | c.JSON(http.StatusBadRequest, models.Response{StatusCode: 1, StatusMsg: "comment_id is invalid"}) 42 | return -1 43 | } 44 | return comment_id 45 | } 46 | 47 | // CommentAction comment action, 1 for post, 2 for delete 48 | func CommentAction(c *gin.Context) { 49 | token := c.Query("token") 50 | actionType := c.Query("action_type") 51 | 52 | userClaim, err := utils.AnalyseToken(token) 53 | if err != nil { 54 | c.JSON(http.StatusOK, models.Response{StatusCode: 1, StatusMsg: "Token is invalid"}) 55 | return 56 | } 57 | user, err := GetUserService().GetUserByName(userClaim.Name) 58 | if err != nil { 59 | c.JSON(http.StatusOK, models.Response{StatusCode: 1, StatusMsg: "User doesn't exist"}) 60 | return 61 | } 62 | 63 | if actionType == "1" { 64 | text := c.Query("comment_text") 65 | utils.InitFilter() 66 | 67 | textAfterFilter := utils.Filter.Replace(text, '*') 68 | 69 | comment := models.Comment{ 70 | CommonEntity: utils.NewCommonEntity(), 71 | //Id: 1, 72 | User: user, 73 | Content: textAfterFilter, 74 | } 75 | 76 | video_id := parseVideoId(c) 77 | err := GetCommentService().PostComments(comment, video_id) 78 | if err != nil { 79 | log.Println(err) 80 | c.JSON(http.StatusBadRequest, models.Response{StatusCode: 1, StatusMsg: "Comment failed"}) 81 | return 82 | } 83 | 84 | c.JSON(http.StatusOK, CommentActionResponse{Response: models.Response{StatusCode: 0, StatusMsg: ""}, 85 | Comment: comment}) 86 | return 87 | } else if actionType == "2" { 88 | comment_id := parseCommetId(c) 89 | err := GetCommentService().DeleteComments(comment_id) 90 | if err != nil { 91 | log.Println(err) 92 | c.JSON(http.StatusBadRequest, models.Response{StatusCode: 1, StatusMsg: "Delete comment failed"}) 93 | return 94 | } 95 | c.JSON(http.StatusOK, models.Response{StatusCode: 0}) 96 | return 97 | } 98 | c.JSON(http.StatusOK, models.Response{StatusCode: 0}) 99 | } 100 | 101 | // CommentList all videos have same demo comment list 102 | func CommentList(c *gin.Context) { 103 | c.JSON(http.StatusOK, CommentListResponse{ 104 | Response: models.Response{StatusCode: 0, StatusMsg: "Comment list"}, 105 | CommentList: GetCommentService().CommentList(parseVideoId(c)), 106 | }) 107 | } 108 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/RaymondCode/simple-demo 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/bits-and-blooms/bloom/v3 v3.5.0 7 | github.com/dgrijalva/jwt-go v3.2.0+incompatible 8 | github.com/gavv/httpexpect/v2 v2.8.0 9 | github.com/gin-contrib/pprof v1.4.0 10 | github.com/gin-gonic/gin v1.9.1 11 | github.com/go-redis/redis/v8 v8.11.5 12 | github.com/importcjj/sensitive v0.0.0-20200106142752-42d1c505be7b 13 | github.com/jinzhu/copier v0.3.5 14 | github.com/sirupsen/logrus v1.9.3 15 | github.com/streadway/amqp v1.1.0 16 | github.com/stretchr/testify v1.8.3 17 | golang.org/x/crypto v0.9.0 18 | golang.org/x/net v0.10.0 19 | gopkg.in/errgo.v2 v2.1.0 20 | gopkg.in/yaml.v2 v2.4.0 21 | gorm.io/driver/mysql v1.3.2 22 | gorm.io/gorm v1.23.3 23 | 24 | ) 25 | 26 | require ( 27 | github.com/ajg/form v1.5.1 // indirect 28 | github.com/andybalholm/brotli v1.0.4 // indirect 29 | github.com/bits-and-blooms/bitset v1.8.0 // indirect 30 | github.com/bytedance/sonic v1.9.1 // indirect 31 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 32 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect 33 | github.com/davecgh/go-spew v1.1.1 // indirect 34 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 35 | github.com/fatih/structs v1.1.0 // indirect 36 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect 37 | github.com/gin-contrib/sse v0.1.0 // indirect 38 | github.com/go-playground/locales v0.14.1 // indirect 39 | github.com/go-playground/universal-translator v0.18.1 // indirect 40 | github.com/go-playground/validator/v10 v10.14.0 // indirect 41 | github.com/go-sql-driver/mysql v1.6.0 // indirect 42 | github.com/goccy/go-json v0.10.2 // indirect 43 | github.com/google/go-querystring v1.1.0 // indirect 44 | github.com/gorilla/websocket v1.4.2 // indirect 45 | github.com/imkira/go-interpol v1.1.0 // indirect 46 | github.com/jinzhu/inflection v1.0.0 // indirect 47 | github.com/jinzhu/now v1.1.5 // indirect 48 | github.com/json-iterator/go v1.1.12 // indirect 49 | github.com/klauspost/compress v1.15.0 // indirect 50 | github.com/klauspost/cpuid/v2 v2.2.4 // indirect 51 | github.com/leodido/go-urn v1.2.4 // indirect 52 | github.com/mattn/go-isatty v0.0.19 // indirect 53 | github.com/mitchellh/go-wordwrap v1.0.1 // indirect 54 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 55 | github.com/modern-go/reflect2 v1.0.2 // indirect 56 | github.com/pelletier/go-toml/v2 v2.0.8 // indirect 57 | github.com/pmezard/go-difflib v1.0.0 // indirect 58 | github.com/sanity-io/litter v1.5.5 // indirect 59 | github.com/sergi/go-diff v1.0.0 // indirect 60 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 61 | github.com/ugorji/go/codec v1.2.11 // indirect 62 | github.com/valyala/bytebufferpool v1.0.0 // indirect 63 | github.com/valyala/fasthttp v1.34.0 // indirect 64 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect 65 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect 66 | github.com/xeipuuv/gojsonschema v1.2.0 // indirect 67 | github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 // indirect 68 | github.com/yudai/gojsondiff v1.0.0 // indirect 69 | github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect 70 | golang.org/x/arch v0.3.0 // indirect 71 | golang.org/x/sys v0.8.0 // indirect 72 | golang.org/x/text v0.9.0 // indirect 73 | google.golang.org/protobuf v1.30.0 // indirect 74 | gopkg.in/yaml.v3 v3.0.1 // indirect 75 | moul.io/http2curl/v2 v2.3.0 // indirect 76 | ) 77 | -------------------------------------------------------------------------------- /mq/followMQ.go: -------------------------------------------------------------------------------- 1 | package mq 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/RaymondCode/simple-demo/config" 6 | "github.com/RaymondCode/simple-demo/models" 7 | "github.com/RaymondCode/simple-demo/utils" 8 | "github.com/streadway/amqp" 9 | "log" 10 | ) 11 | 12 | type FollowMQ struct { 13 | RabbitMQ 14 | Channel *amqp.Channel 15 | QueueName string 16 | Exchange string 17 | key string 18 | } 19 | 20 | // 初始化 channel 21 | // var LikeChannel chan models.LikeMQToVideo 22 | var FollowChannel chan models.FollowMQToUser 23 | 24 | func MakeFollowChannel() { 25 | ch := make(chan models.FollowMQToUser, config.BufferSize) 26 | FollowChannel = ch 27 | } 28 | 29 | // NewFollowRabbitMQ 获取followMQ的对应管道。 30 | func NewFollowRabbitMQ() *FollowMQ { 31 | followMQ := &FollowMQ{ 32 | RabbitMQ: *Rmq, 33 | QueueName: "followMQ", 34 | } 35 | ch, err := followMQ.conn.Channel() 36 | followMQ.Channel = ch 37 | Rmq.failOnErr(err, "获取通道失败") 38 | return followMQ 39 | } 40 | 41 | // Publish 关注操作的发布配置。 42 | func (followMQ *FollowMQ) Publish(message string) { 43 | _, err := followMQ.Channel.QueueDeclare( 44 | followMQ.QueueName, 45 | //是否持久化 46 | true, 47 | //是否为自动删除 48 | false, 49 | //是否具有排他性 50 | false, 51 | //是否阻塞 52 | false, 53 | //额外属性 54 | nil, 55 | ) 56 | if err != nil { 57 | panic(err) 58 | } 59 | 60 | err1 := followMQ.Channel.Publish( 61 | followMQ.Exchange, 62 | followMQ.QueueName, 63 | false, 64 | false, 65 | amqp.Publishing{ 66 | ContentType: "text/plain", 67 | Body: []byte(message), 68 | }) 69 | if err1 != nil { 70 | panic(err) 71 | } 72 | 73 | } 74 | 75 | // Consumer 关注关系的消费逻辑。 76 | //func (followMQ *FollowMQ) Consumer() { 77 | // _, err := followMQ.Channel.QueueDeclare(followMQ.QueueName, true, false, false, false, nil) 78 | // if err != nil { 79 | // panic(err) 80 | // } 81 | // 82 | // messages, err1 := followMQ.Channel.Consume( 83 | // followMQ.QueueName, 84 | // //用来区分多个消费者 85 | // "", 86 | // //是否自动应答 87 | // true, 88 | // //是否具有排他性 89 | // false, 90 | // //如果设置为true,表示不能将同一个connection中发送的消息传递给这个connection中的消费者 91 | // false, 92 | // //消息队列是否阻塞 93 | // false, 94 | // nil, 95 | // ) 96 | // if err1 != nil { 97 | // panic(err1) 98 | // } 99 | // go followMQ.consumer(messages) 100 | //} 101 | 102 | func (followMQ *FollowMQ) consumer(message <-chan amqp.Delivery) { 103 | for d := range message { 104 | // Handle the received message 105 | var data models.FollowMQToUser 106 | err := json.Unmarshal(d.Body, &data) 107 | if err != nil { 108 | log.Printf("Error decoding message: %v", err) 109 | continue 110 | } 111 | // Now, process the follow action based on the data received 112 | follow := models.Follow{ 113 | UserId: data.UserId, 114 | FollowUserId: data.FollowUserId, 115 | } 116 | switch data.ActionType { 117 | case 1: // Follow action 118 | err := follow.Insert(utils.GetMysqlDB()) 119 | if err != nil { 120 | log.Printf("Error inserting follow record: %v", err) 121 | continue 122 | } 123 | case 2: // Unfollow action 124 | err := follow.Delete(utils.GetMysqlDB()) 125 | if err != nil { 126 | log.Printf("Error deleting follow record: %v", err) 127 | continue 128 | } 129 | default: 130 | log.Printf("Invalid action type received: %d", data.ActionType) 131 | } 132 | } 133 | } 134 | 135 | var FollowRMQ *FollowMQ 136 | 137 | // InitFollowRabbitMQ 初始化rabbitMQ连接。 138 | func InitFollowRabbitMQ() { 139 | FollowRMQ = NewFollowRabbitMQ() 140 | //go FollowRMQ.Consumer() 141 | } 142 | -------------------------------------------------------------------------------- /test/interact_api_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "net/http" 6 | "testing" 7 | ) 8 | 9 | func TestFavorite(t *testing.T) { 10 | e := newExpect(t) 11 | 12 | feedResp := e.GET("/douyin/feed/").Expect().Status(http.StatusOK).JSON().Object() 13 | feedResp.Value("status_code").Number().Equal(0) 14 | feedResp.Value("video_list").Array().Length().Gt(0) 15 | firstVideo := feedResp.Value("video_list").Array().First().Object() 16 | videoId := firstVideo.Value("id").Number().Raw() 17 | 18 | userId, token := getTestUserToken(testUserA, e) 19 | 20 | favoriteResp := e.POST("/douyin/favorite/action/"). 21 | WithQuery("token", token).WithQuery("video_id", videoId).WithQuery("action_type", 1). 22 | WithFormField("token", token).WithFormField("video_id", videoId).WithFormField("action_type", 1). 23 | Expect(). 24 | Status(http.StatusOK). 25 | JSON().Object() 26 | favoriteResp.Value("status_code").Number().Equal(0) 27 | 28 | favoriteListResp := e.GET("/douyin/favorite/list/"). 29 | WithQuery("token", token).WithQuery("user_id", userId). 30 | WithFormField("token", token).WithFormField("user_id", userId). 31 | Expect(). 32 | Status(http.StatusOK). 33 | JSON().Object() 34 | favoriteListResp.Value("status_code").Number().Equal(0) 35 | for _, element := range favoriteListResp.Value("video_list").Array().Iter() { 36 | video := element.Object() 37 | video.ContainsKey("id") 38 | video.ContainsKey("author") 39 | video.Value("play_url").String().NotEmpty() 40 | video.Value("cover_url").String().NotEmpty() 41 | } 42 | } 43 | 44 | func TestComment(t *testing.T) { 45 | e := newExpect(t) 46 | 47 | feedResp := e.GET("/douyin/feed/").Expect().Status(http.StatusOK).JSON().Object() 48 | feedResp.Value("status_code").Number().Equal(0) 49 | feedResp.Value("video_list").Array().Length().Gt(0) 50 | firstVideo := feedResp.Value("video_list").Array().First().Object() 51 | videoId := firstVideo.Value("id").Number().Raw() 52 | 53 | _, token := getTestUserToken(testUserA, e) 54 | 55 | addCommentResp := e.POST("/douyin/comment/action/"). 56 | WithQuery("token", token).WithQuery("video_id", videoId).WithQuery("action_type", 1).WithQuery("comment_text", "测试评论"). 57 | WithFormField("token", token).WithFormField("video_id", videoId).WithFormField("action_type", 1).WithFormField("comment_text", "测试评论"). 58 | Expect(). 59 | Status(http.StatusOK). 60 | JSON().Object() 61 | addCommentResp.Value("status_code").Number().Equal(0) 62 | addCommentResp.Value("comment").Object().Value("id").Number().Gt(0) 63 | commentId := int(addCommentResp.Value("comment").Object().Value("id").Number().Raw()) 64 | 65 | commentListResp := e.GET("/douyin/comment/list/"). 66 | WithQuery("token", token).WithQuery("video_id", videoId). 67 | WithFormField("token", token).WithFormField("video_id", videoId). 68 | Expect(). 69 | Status(http.StatusOK). 70 | JSON().Object() 71 | commentListResp.Value("status_code").Number().Equal(0) 72 | containTestComment := false 73 | for _, element := range commentListResp.Value("comment_list").Array().Iter() { 74 | comment := element.Object() 75 | comment.ContainsKey("id") 76 | comment.ContainsKey("user") 77 | comment.Value("content").String().NotEmpty() 78 | comment.Value("create_date").String().NotEmpty() 79 | if int(comment.Value("id").Number().Raw()) == commentId { 80 | containTestComment = true 81 | } 82 | } 83 | 84 | assert.True(t, containTestComment, "Can't find test comment in list") 85 | 86 | delCommentResp := e.POST("/douyin/comment/action/"). 87 | WithQuery("token", token).WithQuery("video_id", videoId).WithQuery("action_type", 2).WithQuery("comment_id", commentId). 88 | WithFormField("token", token).WithFormField("video_id", videoId).WithFormField("action_type", 2).WithFormField("comment_id", commentId). 89 | Expect(). 90 | Status(http.StatusOK). 91 | JSON().Object() 92 | delCommentResp.Value("status_code").Number().Equal(0) 93 | } 94 | -------------------------------------------------------------------------------- /mq/likeMQ.go: -------------------------------------------------------------------------------- 1 | package mq 2 | 3 | import ( 4 | "github.com/RaymondCode/simple-demo/config" 5 | "github.com/RaymondCode/simple-demo/models" 6 | "github.com/streadway/amqp" 7 | ) 8 | 9 | type LikeMQ struct { 10 | RabbitMQ 11 | Channel *amqp.Channel 12 | QueueUserName string 13 | QueueVideoName string 14 | exchange string 15 | key string 16 | } 17 | 18 | // 初始化 channel 19 | // var LikeChannel chan models.LikeMQToVideo 20 | var LikeChannel chan models.LikeMQToUser 21 | 22 | func MakeLikeChannel() { 23 | ch := make(chan models.LikeMQToUser, config.BufferSize) 24 | LikeChannel = ch 25 | } 26 | 27 | // NewLikeRabbitMQ 获取likeMQ的对应队列。 28 | func NewLikeRabbitMQ() *LikeMQ { 29 | likeMQ := &LikeMQ{ 30 | RabbitMQ: *Rmq, 31 | QueueUserName: "userLikeMQ", 32 | QueueVideoName: "videoLikeMQ", 33 | exchange: "likeExchange", 34 | } 35 | ch, err := likeMQ.conn.Channel() 36 | likeMQ.Channel = ch 37 | Rmq.failOnErr(err, "获取通道失败") 38 | return likeMQ 39 | } 40 | 41 | // Publish like操作的发布配置。 42 | func (l *LikeMQ) Publish(message string) { 43 | //声明交换机 44 | err := l.Channel.ExchangeDeclare( 45 | //1.交换机名称 46 | l.exchange, 47 | //2、kind:交换机类型 48 | // //amqp.ExchangeDirect 定向 49 | // //amqp.ExchangeFanout 扇形(广播),发送消息到每个队列 50 | // //amqp.ExchangeTopic 通配符的方式 51 | // //amqp.ExchangeHeaders 参数匹配 52 | amqp.ExchangeFanout, 53 | //是否持久化 54 | true, 55 | //自动删除 56 | false, 57 | //内部使用 58 | false, 59 | false, 60 | nil, 61 | ) 62 | if err != nil { 63 | panic(err) 64 | } 65 | _, err = l.Channel.QueueDeclare( 66 | l.QueueUserName, 67 | //是否持久化 68 | true, 69 | //是否为自动删除 70 | false, 71 | //是否具有排他性 72 | false, 73 | //是否阻塞 74 | false, 75 | //额外属性 76 | nil, 77 | ) 78 | if err != nil { 79 | panic(err) 80 | } 81 | //_, err = l.Channel.QueueDeclare( 82 | // l.QueueVideoName, 83 | // //是否持久化 84 | // true, 85 | // //是否为自动删除 86 | // false, 87 | // //是否具有排他性 88 | // false, 89 | // //是否阻塞 90 | // false, 91 | // //额外属性 92 | // nil, 93 | //) 94 | //if err != nil { 95 | // panic(err) 96 | //} 97 | //绑定队列和交换机 98 | err = l.Channel.QueueBind(l.QueueUserName, "", l.exchange, false, nil) 99 | if err != nil { 100 | panic(err) 101 | } 102 | //err = l.Channel.QueueBind(l.QueueVideoName, "", l.exchange, false, nil) 103 | //if err != nil { 104 | // panic(err) 105 | //} 106 | 107 | err1 := l.Channel.Publish( 108 | l.exchange, 109 | "", 110 | false, 111 | false, 112 | amqp.Publishing{ 113 | ContentType: "text/plain", 114 | Body: []byte(message), 115 | }) 116 | if err1 != nil { 117 | panic(err) 118 | } 119 | 120 | } 121 | 122 | //// Consumer like关系的消费逻辑。 123 | //func (l *LikeMQ) Consumer() { 124 | // 125 | // _, err := l.Channel.QueueDeclare(l.queueName, true, false, false, false, nil) 126 | // 127 | // if err != nil { 128 | // panic(err) 129 | // } 130 | // 131 | // //2、接收消息 132 | // messages, err1 := l.Channel.Consume( 133 | // l.queueName, 134 | // //用来区分多个消费者 135 | // "", 136 | // //是否自动应答 137 | // true, 138 | // //是否具有排他性 139 | // false, 140 | // //如果设置为true,表示不能将同一个connection中发送的消息传递给这个connection中的消费者 141 | // false, 142 | // //消息队列是否阻塞 143 | // false, 144 | // nil, 145 | // ) 146 | // if err1 != nil { 147 | // panic(err1) 148 | // } 149 | // go l.consumer(messages) 150 | // //forever := make(chan bool) 151 | // log.Println(messages) 152 | // 153 | // log.Printf("[*] Waiting for messagees,To exit press CTRL+C") 154 | // 155 | // //<-forever 156 | // 157 | //} 158 | //func (l *LikeMQ) consumer(message <-chan amqp.Delivery) { 159 | // for d := range message { 160 | // log.Println(string(d.Body)) 161 | // } 162 | //} 163 | 164 | var LikeRMQ *LikeMQ 165 | 166 | // InitLikeRabbitMQ 初始化rabbitMQ连接。 167 | func InitLikeRabbitMQ() { 168 | LikeRMQ = NewLikeRabbitMQ() 169 | //LikeRMQ.Publish("hello word !") 170 | //go LikeRMQ.Consumer() 171 | } 172 | -------------------------------------------------------------------------------- /service/impl/MessageServiceImpl.go: -------------------------------------------------------------------------------- 1 | package impl 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/RaymondCode/simple-demo/models" 7 | "github.com/RaymondCode/simple-demo/utils" 8 | "io" 9 | "net" 10 | "sort" 11 | "sync" 12 | ) 13 | 14 | var chatConnMap = sync.Map{} 15 | 16 | type MessageServiceImpl struct { 17 | } 18 | 19 | func (messageService MessageServiceImpl) SendMessage(userId int64, toUserId int64, content string) error { 20 | err := models.SaveMessage(&models.Message{ 21 | CommonEntity: utils.NewCommonEntity(), 22 | Content: content, 23 | }) 24 | if err != nil { 25 | return err 26 | } 27 | err = models.SaveMessageSendEvent(&models.MessageSendEvent{ 28 | CommonEntity: utils.NewCommonEntity(), 29 | UserId: userId, 30 | ToUserId: toUserId, 31 | MsgContent: content, 32 | }) 33 | if err != nil { 34 | return err 35 | } 36 | err = models.SaveMessagePushEvent(&models.MessagePushEvent{ 37 | CommonEntity: utils.NewCommonEntity(), 38 | FromUserId: userId, 39 | MsgContent: content, 40 | }) 41 | if err != nil { 42 | return err 43 | } 44 | return nil 45 | } 46 | 47 | func (messageService MessageServiceImpl) GetHistoryOfChat(userId int64, toUserId int64) ([]models.MessageDVO, error) { 48 | //find from meesageSendEvent table 49 | messageSendEvents, err := models.FindMessageSendEventByUserIdAndToUserId(userId, toUserId) 50 | if err != nil { 51 | return nil, err 52 | } 53 | messageSendEventsOpposite, err := models.FindMessageSendEventByUserIdAndToUserId(toUserId, userId) 54 | if err != nil { 55 | return nil, err 56 | } 57 | messageSendEvents = append(messageSendEvents, messageSendEventsOpposite...) 58 | sort.Sort(models.ByCreateTime(messageSendEvents)) 59 | 60 | var messages []models.MessageDVO 61 | var wg sync.WaitGroup 62 | for _, messageSendEvent := range messageSendEvents { 63 | wg.Add(1) 64 | go func(messageSendEvent models.MessageSendEvent) { 65 | defer wg.Done() 66 | messages = append(messages, models.MessageDVO{ 67 | Id: messageSendEvent.Id, 68 | UserId: messageSendEvent.UserId, 69 | ToUserId: messageSendEvent.ToUserId, 70 | Content: messageSendEvent.MsgContent, 71 | CreateTime: messageSendEvent.CreateDate.Unix(), 72 | }) 73 | }(messageSendEvent) 74 | } 75 | wg.Wait() 76 | return messages, nil 77 | } 78 | 79 | func RunMessageServer() { 80 | listen, err := net.Listen("tcp", "127.0.0.1:9090") 81 | if err != nil { 82 | fmt.Printf("Run message sever failed: %v\n", err) 83 | return 84 | } 85 | 86 | for { 87 | conn, err := listen.Accept() 88 | if err != nil { 89 | fmt.Printf("Accept conn failed: %v\n", err) 90 | continue 91 | } 92 | 93 | go process(conn) 94 | } 95 | } 96 | 97 | func process(conn net.Conn) { 98 | defer conn.Close() 99 | 100 | var buf [256]byte 101 | for { 102 | n, err := conn.Read(buf[:]) 103 | if n == 0 { 104 | if err == io.EOF { 105 | break 106 | } 107 | fmt.Printf("Read message failed: %v\n", err) 108 | continue 109 | } 110 | 111 | var event = models.MessageSendEvent{} 112 | _ = json.Unmarshal(buf[:n], &event) 113 | fmt.Printf("Receive Message:%+v\n", event) 114 | 115 | fromChatKey := fmt.Sprintf("%d_%d", event.UserId, event.ToUserId) 116 | if len(event.MsgContent) == 0 { 117 | chatConnMap.Store(fromChatKey, conn) 118 | continue 119 | } 120 | 121 | toChatKey := fmt.Sprintf("%d_%d", event.ToUserId, event.UserId) 122 | writeConn, exist := chatConnMap.Load(toChatKey) 123 | if !exist { 124 | fmt.Printf("User %d offline\n", event.ToUserId) 125 | continue 126 | } 127 | 128 | pushEvent := models.MessagePushEvent{ 129 | FromUserId: event.UserId, 130 | MsgContent: event.MsgContent, 131 | } 132 | pushData, _ := json.Marshal(pushEvent) 133 | _, err = writeConn.(net.Conn).Write(pushData) 134 | if err != nil { 135 | fmt.Printf("Push message failed: %v\n", err) 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /controller/user.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/RaymondCode/simple-demo/models" 5 | "github.com/RaymondCode/simple-demo/service/impl" 6 | "github.com/RaymondCode/simple-demo/utils" 7 | "github.com/gin-gonic/gin" 8 | "log" 9 | "net/http" 10 | "strconv" 11 | ) 12 | 13 | // usersLoginInfo use map to store user info, and key is username+password for demo 14 | // user data will be cleared every time the server starts 15 | // test data: username=zhanglei, password=douyin 16 | var usersLoginInfo = map[string]models.User{ 17 | "zhangleidouyin": { 18 | CommonEntity: utils.NewCommonEntity(), 19 | //Id: 1, 20 | FollowCount: 10, 21 | FollowerCount: 5, 22 | }, 23 | } 24 | 25 | var userIdSequence = int64(1) 26 | 27 | type UserLoginResponse struct { 28 | models.Response 29 | UserId int64 `json:"user_id,omitempty"` 30 | Token string `json:"token"` 31 | } 32 | 33 | type UserResponse struct { 34 | models.Response 35 | User models.User `json:"user"` 36 | } 37 | 38 | type UserRequest struct { 39 | Username string `json:"username"` 40 | Password string `json:"password"` 41 | } 42 | 43 | // 拼装 UserService 44 | func GetUserService() impl.UserServiceImpl { 45 | var userService impl.UserServiceImpl 46 | return userService 47 | } 48 | 49 | func Register(c *gin.Context) { 50 | username := c.Query("username") 51 | password := c.Query("password") 52 | //_, errName := GetUserService().GetUserByName(username) 53 | //if errName == nil { 54 | // c.JSON(http.StatusBadRequest, UserLoginResponse{ 55 | // Response: models.Response{StatusCode: 1, StatusMsg: "用户名重复"}, 56 | // }) 57 | // return 58 | //} 59 | ////var userRequest UserRequest 60 | ////if err := c.ShouldBindJSON(&userRequest); err != nil { 61 | //// c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 62 | //// return 63 | ////} 64 | ////username := userRequest.Username 65 | ////password := userRequest.Password 66 | ////加密 67 | //encrypt, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) 68 | //password = string(encrypt) 69 | // 70 | //atomic.AddInt64(&userIdSequence, 1) 71 | //newUser := models.User{ 72 | // CommonEntity: models.NewCommonEntity(), 73 | // Name: username, 74 | // Password: password, 75 | //} 76 | // 77 | //err := GetUserService().Save(newUser) 78 | //if err != nil { 79 | // c.JSON(http.StatusInternalServerError, UserLoginResponse{ 80 | // Response: models.Response{StatusCode: 1, StatusMsg: "Cant not save the User!"}, 81 | // }) 82 | //} else { 83 | // token, err1 := models.GenerateToken(username, password, newUser.CommonEntity) 84 | // if err1 != nil { 85 | // log.Printf("Can not get the token!") 86 | // } 87 | // err2 := utils.SaveTokenToRedis(newUser.Name, token, time.Duration(config.TokenTTL*float64(time.Second))) 88 | // if err2 != nil { 89 | // log.Printf("Fail : Save token in redis !") 90 | // } else { 91 | // c.JSON(http.StatusOK, UserLoginResponse{ 92 | // Response: models.Response{StatusCode: 0}, 93 | // UserId: newUser.Id, 94 | // Token: token, 95 | // }) 96 | // } 97 | //} 98 | err := GetUserService().Register(username, password, c) 99 | if err != nil { 100 | log.Printf("Register Error!") 101 | } 102 | 103 | } 104 | 105 | func Login(c *gin.Context) { 106 | username := c.Query("username") 107 | password := c.Query("password") 108 | err := GetUserService().Login(username, password, c) 109 | if err != nil { 110 | log.Printf("Login Error !") 111 | } 112 | } 113 | 114 | func UserInfo(c *gin.Context) { 115 | token := c.Query("token") 116 | userId := c.Query("user_id") 117 | userIdInt, _ := strconv.ParseInt(userId, 10, 64) 118 | user, err := GetUserService().UserInfo(userIdInt, token) 119 | if err != nil { 120 | log.Printf(err.Error()) 121 | c.JSON(http.StatusOK, UserResponse{ 122 | Response: models.Response{StatusCode: 1, StatusMsg: err.Error()}, 123 | }) 124 | } else { 125 | c.JSON(http.StatusOK, UserResponse{ 126 | Response: models.Response{StatusCode: 0}, 127 | User: *user, 128 | }) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 抖音简易后端 2 | ### 字节跳动青训营 GOGOG O队项目 3 | 4 | ## 前期准备进度 5 | by 关竣佑 谢声儒 6 | 7 | * 集成 GORM 框架 8 | * 准备实体类 Model 9 | 所有的实体类均继承 CommonEntity (Id , CreateTime , IsDelete) 10 | * 数据库设计 11 | 所有删除均采用逻辑删除 (规定删除后is_delete字段为 1 ) 12 | 所有的主键 id 采用bingint 对应的 go 实体类使用 int64 , 生成的时候使用雪花算法 13 | 14 | * 分级结构 : controller - service - serviceImpl - model (MVC架构) 15 | * 使用 jwt 生成 token , 注册/登录时将 token 存在 redis 中 16 | 17 | 18 | 19 | 接口文档 20 | 21 | https://apifox.com/apidoc/shared-09d88f32-0b6c-4157-9d07-a36d32d7a75c/api-50707521 22 | 23 | ## 开发规约 24 | 25 | #### 禁止/必须 26 | 27 | 1. 主体逻辑代码必须放在service层中的impl层,禁止在controller层写过多大的业务的代码,controller层应尽量调用service层的方法实现业务逻辑 28 | 29 | 2. model 层的函数禁止调用其它model 层相同包下不同 model 的函数 30 | 31 | 3. 返回给前端的数据若要组装成一个 stauct 必须使用 xxxDVO来命名,参见 models.VideoDVO 32 | 33 | 4. model中 禁止进行sql字符串拼接,避免造成sql注入风险,如需使用参数拼接必须使用 ? 传参 如 34 | 35 | ```go 36 | err := utils.DB.Where("is_deleted != ?", 1).Find(&videolist).Error 37 | ``` 38 | 39 | 5. 遇到的所有 error 返回都必须进程处理或返回给上级(如使用 log 输出日志) 40 | 41 | ```go 42 | if err1 != nil { 43 | log.Printf("Can not get the token!") 44 | } 45 | ``` 46 | 47 | 48 | 49 | 6. 所需用到的参数均放在config.go中,禁止在代码中出现魔法值。(所谓魔法值,是代码中莫名其妙出现的数字,数字意义必须通过阅读其他代码才能推断出来,这样给后期维护或者其他人员阅读代码,带来了极大不便。)如以下代码便出现了魔法值 50 | 51 | ```go 52 | // 遍历查询出的审查人对象集合 53 | for(AuditPersonInfoDTO adp : auditPersonInfoDTO){ 54 | // 判断审查结果是否为空 55 | if(adp.getStatus()!=null){ 56 | // 设置审查状态,status为2代表审核通过,为3代表退回修改 57 | switch (adp.getStatus()){ 58 | case "2" : 59 | adp.setStatus("审查通过"); 60 | break; 61 | case "3" : 62 | adp.setStatus("退回修改"); 63 | break; 64 | ...... 65 | ``` 66 | 67 | 7. 每次开发前都必须pull代码!!!不然可能会造成冲突,很难解决。尽量先新建一个分支,测试功能正常后再与main分支合并 68 | 69 | 8. 禁止对已有文件进行移动(比如说移到其它包内),如需对结构有较大修改请提前说明 70 | 71 | 9. 每次 push 代码时禁止直接提交到 Master 分支 !必须新建分支,运行测试正常后再提交分支!合并分支时遇到冲突需慎重解决,不明白的及时提出或让其他人帮忙合并 72 | 73 | 10. 所有实体类的成员必须使用**首字母大写**的驼峰命名法,Go 语言只用大写首字母才能被其它包访问。 74 | 75 | 11. 如需更改数据库请提前说明! 76 | 12. 如需提交更改后的数据库禁止删掉之前的数据库文件,以 日期-版本号.sql命名 (如:2023-7-21-v1douyin.sql) 77 | 13. 分支合并之后必须删除GitHub上的分支,每个人在GitHub上最多拥有一个分支 78 | 14. 校验token是否存在且合法使用 utils 包的 AuthAdminCheck 函数 79 | 15. 编写接口时返回的数据一定要按照接口文档要求返回的数据 80 | 81 | 82 | 83 | #### 建议 84 | 85 | 1. 推荐使用 Goland 进行开发,使用Goland 的 git 图形化工具操作 git 86 | 87 | ​ 2.合并分支解决冲突的时候如遇不理解的问题及时提出 88 | 89 | 3. 开发一个函数后,建议在 test 包下编写测试代码进行测试 90 | 3. 如果业务操作间没有太多的关联,建议开启协程,使用 channel 通信。 91 | 3. 创建切片数组前,如果能估计大小,建议预先设置好大小,减少后期扩容开销 92 | 93 | 94 | 95 | #### 注意 96 | 97 | 1. 请求格式特别是 POST 请求的格式参照原本的代码。它里面有的POST请求不放json而使用拼接URL(我也不知道为什么),这里很坑 98 | 99 | # 接口基本思路 100 | 101 | ## 互动接口 102 | 103 | ### 赞操作 (王奕丹) 104 | 105 | URL:**POST** **/douyin/favorite/action/** 106 | 107 | 基本思路:主要操控like表,当action_type=1时写入对应的一条点赞关系记录,反之则删除 108 | 109 | ### 喜欢列表(王奕丹) 110 | 111 | URL: **GET** **/douyin/favorite/list/** 112 | 113 | 基本思路:主要操控like表、user表和veido表,查出用户所喜欢的所有视频id,根据视频id进一步查询作者信息、视频信息,具体字段需求查看api文档,建议封装成DTO类 114 | 115 | ### 评论操作 (邱祥凯) 116 | 117 | URL: **POST** **/douyin/comment/action/** 118 | 119 | 基本思路:主要操控comment表,将对应的评论信息添加到数据库中即可。 120 | 121 | ### 评论列表(邱祥凯) 122 | 123 | URL: **GET** **/douyin/comment/list/** 124 | 125 | 基本思路:查询出conmment表中对应视频id的所有记录,按照创建时间进行倒序排序。如果想要通过redis进行优化,可以使用zset数据类型,该类型可以存入键值对,可以根据值进行排序,为一个有序集合 126 | 127 | ## 社交接口 128 | 129 | ### 关注操作(杨伟宁) 130 | 131 | URL: **POST** **/douyin/relation/action/** 132 | 133 | 基本思路:与点赞操作类似,只是操控的数据表变成follo表 134 | 135 | ### 关注列表(杨伟宁) 136 | 137 | URL: **GET** **/douyin/relation/follow/list/** 138 | 139 | 基本思路:根据请求中的user_id联合查询follow表和user表,返回对应的关注用户信息集合 140 | 141 | ### 粉丝列表(杨伟宁) 142 | 143 | URL: **GET** **/douyin/relation/follow/list/** 144 | 145 | 基本思路:与关注列表其实逻辑类似的,操控表也一样 146 | 147 | ### 好友列表(杨伟宁) 148 | 149 | URL: **GET** **/douyin/relation/friend/list/** 150 | 151 | 基本思路:根据现在的抖音的定义,只有两个用户相互关注才是好友,那么我们就可以先查询当前用户的关注列表,对列表中的每个用户判断其是否也关注了当前用户,将不符合条件的用户过滤。最后得到的列表就是好友列表 152 | 153 | ### 发送消息(邱祥凯) 154 | 155 | URL: **POST** **/douyin/message/action/** 156 | 157 | 基本思路:逻辑很简单,我们只需要将记录 存入3张表:message、message_push_event、message_send_event 158 | 159 | ### 聊天记录 (邱祥凯) 160 | 161 | URL: **GET** **/douyin/message/chat/** 162 | 163 | 基本思路:根据当前用户id以及to_user_id从message_send_event表中查出对应的聊天记录返回即可 164 | 165 | # 登录鉴权 166 | 167 | ​ 全局鉴权采用两层中间件(即spring中的拦截器)完成。第一层拦截器用于刷新redis中的token有效期,无论什么请求都放行到第二个中间件处理;第二层拦截器用于真正的用户鉴权,此时若用户在未登录状态访问了非法资源则会立刻拒绝该请求。现在所有请求的鉴权操作都会在拦截器中自动完成。 168 | -------------------------------------------------------------------------------- /models/Comment.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/RaymondCode/simple-demo/utils" 5 | "gorm.io/gorm" 6 | "strconv" 7 | ) 8 | 9 | type Comment struct { 10 | utils.CommonEntity 11 | //Id int64 `json:"id,omitempty"` 12 | User User `json:"user"` 13 | Content string `json:"content,omitempty"` 14 | } 15 | 16 | type ByCreateDate []Comment 17 | 18 | func (a ByCreateDate) Len() int { 19 | return len(a) 20 | } 21 | 22 | func (a ByCreateDate) Swap(i, j int) { 23 | a[i], a[j] = a[j], a[i] 24 | } 25 | 26 | func (a ByCreateDate) Less(i, j int) bool { 27 | return a[i].CreateDate.Compare(a[j].CreateDate) > 0 28 | } 29 | 30 | type CommentMQToVideo struct { 31 | utils.CommonEntity 32 | ActionType int `json:"action_type"` 33 | UserId User `json:"user"` 34 | VideoId int64 `json:"video_id"` 35 | Content string `json:"content"` 36 | CommentID int64 `json:"id"` 37 | } 38 | 39 | func (comment *CommentMQToVideo) ToCommentDB() CommentDB { 40 | return CommentDB{ 41 | CommonEntity: comment.CommonEntity, 42 | UserId: comment.UserId.Id, 43 | VideoId: comment.VideoId, 44 | Content: comment.Content, 45 | } 46 | } 47 | 48 | func (comment *CommentMQToVideo) ToComment() Comment { 49 | return Comment{ 50 | CommonEntity: comment.CommonEntity, 51 | User: comment.UserId, 52 | Content: comment.Content, 53 | } 54 | } 55 | 56 | // CommentDB是数据库储存的Entity 57 | type CommentDB struct { 58 | utils.CommonEntity 59 | //Id int64 `json:"id,omitempty"` 60 | UserId int64 `json:"user_id"` 61 | VideoId int64 `json:"video_id"` 62 | Content string `json:"content,omitempty"` 63 | } 64 | 65 | func (comment *CommentDB) ToComment() Comment { 66 | user, _ := GetUserById(comment.UserId) 67 | return Comment{ 68 | CommonEntity: comment.CommonEntity, 69 | User: user, 70 | Content: comment.Content, 71 | } 72 | } 73 | 74 | func (comment *Comment) ToCommentDB() CommentDB { 75 | return CommentDB{ 76 | CommonEntity: comment.CommonEntity, 77 | UserId: comment.User.Id, 78 | VideoId: -1, 79 | Content: comment.Content, 80 | } 81 | } 82 | 83 | func (commentDB *CommentDB) TableName() string { 84 | return "comment" 85 | } 86 | 87 | func SaveComment(commentDB *CommentDB) error { 88 | videoID := commentDB.VideoId 89 | //comment_count++ 90 | tx := utils.GetMysqlDB().Begin() 91 | 92 | err := tx.Model(&Video{}).Where("id = ?", videoID).Update("comment_count", gorm.Expr("comment_count + ?", 1)).Error 93 | if err != nil { 94 | tx.Rollback() 95 | return err 96 | } 97 | 98 | err = tx.Create(commentDB).Error 99 | if err != nil { 100 | tx.Rollback() 101 | return err 102 | } 103 | 104 | return tx.Commit().Error 105 | } 106 | 107 | func DeleteComment(commentId int64) error { 108 | //set is_deleted = 1 109 | tx := utils.GetMysqlDB().Begin() 110 | 111 | var commentDB CommentDB 112 | err := tx.Where("id = ?", commentId).First(&commentDB).Error 113 | if err != nil { 114 | tx.Rollback() 115 | return err 116 | } 117 | 118 | err = tx.Model(&Video{}).Where("id = ?", commentDB.VideoId).Update("comment_count", gorm.Expr("comment_count - ?", 1)).Error 119 | if err != nil { 120 | tx.Rollback() 121 | return err 122 | } 123 | 124 | err = tx.Model(&CommentDB{}).Where("id = ?", commentId).Update("is_deleted", 1).Error 125 | if err != nil { 126 | tx.Rollback() 127 | return err 128 | } 129 | 130 | return tx.Commit().Error 131 | } 132 | 133 | func GetCommentDBById(commentId int64) (CommentDB, error) { 134 | var commentDB CommentDB 135 | err := utils.GetMysqlDB().Where("id = ? AND is_deleted != ?", commentId, 1).First(&commentDB).Error 136 | if err != nil { 137 | return commentDB, err 138 | } 139 | return commentDB, nil 140 | } 141 | 142 | func GetCommentByVideoId(videoId int64) []Comment { 143 | var comments []Comment 144 | var commentDBs []CommentDB 145 | // 找到对应User 146 | 147 | err := utils.GetMysqlDB().Debug().Where("video_id = ? AND is_deleted != ?", strconv.Itoa(int(videoId)), 1).Find(&commentDBs).Error 148 | if err != nil { 149 | return comments 150 | } 151 | //change comment_count 152 | err = utils.GetMysqlDB().Model(&Video{}).Where("id = ?", videoId).Update("comment_count", len(commentDBs)).Error 153 | if err != nil { 154 | } 155 | 156 | for _, commentDB := range commentDBs { 157 | 158 | comments = append(comments, commentDB.ToComment()) 159 | } 160 | return comments 161 | } 162 | 163 | func GetAllCommentDBs() []CommentDB { 164 | var commentDBs []CommentDB 165 | err := utils.GetMysqlDB().Where("is_deleted != ?", 1).Find(&commentDBs).Error 166 | if err != nil { 167 | return commentDBs 168 | } 169 | return commentDBs 170 | } 171 | -------------------------------------------------------------------------------- /service/impl/VideoServiceImpl.go: -------------------------------------------------------------------------------- 1 | package impl 2 | 3 | import ( 4 | "github.com/RaymondCode/simple-demo/config" 5 | "github.com/RaymondCode/simple-demo/models" 6 | "github.com/RaymondCode/simple-demo/service" 7 | "github.com/RaymondCode/simple-demo/utils" 8 | "github.com/jinzhu/copier" 9 | "mime/multipart" 10 | "path/filepath" 11 | "sync" 12 | "time" 13 | ) 14 | 15 | type VideoServiceImpl struct { 16 | service.UserService 17 | service.FavoriteService 18 | } 19 | 20 | func (videoService VideoServiceImpl) GetVideoListByLastTime(latestTime time.Time, userId int64) ([]models.VideoDVO, time.Time, error) { 21 | videolist, err := models.GetVideoListByLastTime(latestTime) 22 | size := len(videolist) 23 | var wg sync.WaitGroup 24 | VideoDVOList := make([]models.VideoDVO, size) 25 | if err != nil { 26 | return nil, time.Time{}, err 27 | } 28 | //for i := range videolist { 29 | // var authorId = videolist[i].AuthorId 30 | // 31 | // //一定要通过videoService来调用 userSevice 32 | // user, err1 := videoService.UserService.GetUserById(authorId) 33 | // if err1 != nil { 34 | // return nil, time.Time{}, err1 35 | // } 36 | // var videoDVO models.VideoDVO 37 | // err2 := copier.Copy(&videoDVO, &videolist[i]) 38 | // if err2 != nil { 39 | // return nil, time.Time{}, err2 40 | // } 41 | // videoDVO.Author = user 42 | // videoDVO.IsFavorite = videoService.FavoriteService.FindIsFavouriteByUserIdAndVideoId(userId, videoDVO.Id) 43 | // VideoDVOList[i] = videoDVO 44 | //} 45 | var err0 error 46 | for i := range videolist { 47 | var authorId = videolist[i].AuthorId 48 | wg.Add(1) 49 | go func(i int) { 50 | defer wg.Done() 51 | // 通过 videoService 来调用 userService 52 | user, err1 := videoService.UserService.GetUserById(authorId) 53 | if err1 != nil { 54 | err0 = err1 55 | return 56 | } 57 | var videoDVO models.VideoDVO 58 | err2 := copier.Copy(&videoDVO, &videolist[i]) 59 | if err2 != nil { 60 | err0 = err2 61 | return 62 | } 63 | videoDVO.Author = user 64 | if userId != -1 { 65 | videoDVO.IsFavorite = videoService.FavoriteService.FindIsFavouriteByUserIdAndVideoId(userId, videoDVO.Id) 66 | } else { 67 | videoDVO.IsFavorite = false 68 | } 69 | VideoDVOList[i] = videoDVO 70 | }(i) 71 | } 72 | 73 | wg.Wait() 74 | if err0 != nil { 75 | return nil, time.Time{}, err0 76 | } 77 | nextTime := time.Now() 78 | if len(videolist) != 0 { 79 | nextTime = videolist[len(videolist)-1].CreateDate 80 | } 81 | return VideoDVOList, nextTime, nil 82 | } 83 | 84 | // Publish 投稿接口 85 | // TODO 借助redis协助实现feed流 86 | func (videoService VideoServiceImpl) Publish(data *multipart.FileHeader, userId int64, title string) error { 87 | //从title中过滤敏感词汇 88 | replaceTitle := utils.Filter.Replace(title, '#') 89 | //文件名 90 | filename := filepath.Base(data.Filename) 91 | ////将文件名拼接用户id 92 | //finalName := fmt.Sprintf("%d_%s", userId, filename) 93 | ////保存文件的路径,暂时保存在本队public文件夹下 94 | //saveFile := filepath.Join("./public/", finalName) 95 | //保存视频在本地中 96 | // if err = c.SaveUploadedFile(data, saveFile); err != nil { 97 | coverName, err := utils.UploadToServer(data) 98 | if err != nil { 99 | return err 100 | } 101 | user, err1 := models.GetUserById(userId) 102 | if err1 != nil { 103 | return nil 104 | } 105 | //将扩展名修改为.png并返回新的string作为封面文件名 106 | //ext := filepath.Ext(filename) 107 | //name := filename[:len(filename)-len(ext)] 108 | //coverName := name + ".png" 109 | //保存视频在数据库中 110 | video := models.Video{ 111 | CommonEntity: utils.NewCommonEntity(), 112 | AuthorId: userId, 113 | PlayUrl: "http://" + config.Config.VideoServer.Addr2 + "/videos/" + filename, 114 | CoverUrl: "http://" + config.Config.VideoServer.Addr2 + "/photos/" + coverName, 115 | Title: replaceTitle, 116 | } 117 | err2 := models.SaveVideo(&video) 118 | if err2 != nil { 119 | return err2 120 | } 121 | //用户发布作品数加1 122 | user.WorkCount = user.WorkCount + 1 123 | err = models.UpdateUser(utils.GetMysqlDB(), user) 124 | if err != nil { 125 | return err 126 | } 127 | return nil 128 | } 129 | 130 | // PublishList 发布列表 131 | func (videoService VideoServiceImpl) PublishList(userId int64) ([]models.VideoDVO, error) { 132 | videoList, err := models.GetVediosByUserId(userId) 133 | if err != nil { 134 | return nil, err 135 | } 136 | size := len(videoList) 137 | VideoDVOList := make([]models.VideoDVO, size) 138 | //创建多个协程并发更新 139 | var wg sync.WaitGroup 140 | //接收协程产生的错误 141 | var err0 error 142 | for i := range videoList { 143 | wg.Add(1) 144 | go func(i int) { 145 | defer wg.Done() 146 | var userId = videoList[i].AuthorId 147 | //一定要通过videoService来调用 userSevice 148 | user, err1 := models.GetUserById(userId) 149 | if err1 != nil { 150 | err0 = err1 151 | } 152 | var videoDVO models.VideoDVO 153 | err := copier.Copy(&videoDVO, &videoList[i]) 154 | if err != nil { 155 | err0 = err1 156 | } 157 | videoDVO.Author = user 158 | VideoDVOList[i] = videoDVO 159 | }(i) 160 | } 161 | wg.Wait() 162 | //处理协程内的错误 163 | if err0 != nil { 164 | return nil, err0 165 | } 166 | return VideoDVOList, nil 167 | } 168 | -------------------------------------------------------------------------------- /service/impl/CommentServiceImpl.go: -------------------------------------------------------------------------------- 1 | package impl 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "github.com/RaymondCode/simple-demo/models" 8 | "github.com/RaymondCode/simple-demo/mq" 9 | "github.com/RaymondCode/simple-demo/utils" 10 | "github.com/RaymondCode/simple-demo/utils/bloomFilter" 11 | "github.com/go-redis/redis/v8" 12 | "log" 13 | "strconv" 14 | "time" 15 | ) 16 | 17 | type CommentServiceImpl struct { 18 | } 19 | 20 | func (commentService CommentServiceImpl) PostComments(comment models.Comment, video_id int64) error { 21 | user, err := UserServiceImpl{}.GetUserById(comment.User.Id) 22 | if err != nil { 23 | return err 24 | } 25 | 26 | bloomFilter.BloomFilter.Add([]byte(strconv.Itoa(int(comment.Id)))) 27 | 28 | toMQ := models.CommentMQToVideo{ 29 | CommonEntity: comment.CommonEntity, 30 | ActionType: 1, 31 | UserId: user, 32 | VideoId: video_id, 33 | Content: comment.Content, 34 | CommentID: -1, 35 | } 36 | mq.CommentChannel <- toMQ 37 | return nil 38 | } 39 | 40 | // CommentList 查看视频的所有评论,按发布时间倒序 41 | func (commentService CommentServiceImpl) CommentList(videoId int64) []models.Comment { 42 | rdb := utils.GetRedisDB() 43 | 44 | exist := bloomFilter.BloomFilter.Test([]byte(strconv.Itoa(int(videoId)))) 45 | if !exist { 46 | return nil 47 | } 48 | 49 | //get by id 50 | commentID := rdb.ZRange(context.Background(), strconv.Itoa(int(videoId)), 0, -1) 51 | if len(commentID.Val()) == 0 { 52 | comments := models.GetCommentByVideoId(videoId) 53 | //save to redis 54 | for _, comment := range comments { 55 | commentJSON, err := json.Marshal(comment) 56 | if err != nil { 57 | log.Print(err) 58 | continue 59 | } 60 | rdb.Set(context.Background(), "comment:"+strconv.Itoa(int(comment.Id)), commentJSON, time.Hour*24) 61 | rdb.ZAdd(context.Background(), strconv.Itoa(int(videoId)), &redis.Z{ 62 | Score: float64(comment.CreateDate.Unix()), 63 | Member: strconv.Itoa(int(comment.Id)), 64 | }) 65 | } 66 | return comments 67 | } 68 | 69 | var comments []models.Comment 70 | //从redis中读取评论实体 71 | for _, id := range commentID.Val() { 72 | commentJSON := rdb.Get(context.Background(), "comment:"+id) 73 | var comment models.Comment 74 | if commentJSON.Val() == "" { 75 | commentID, err := strconv.ParseInt(id, 10, 64) 76 | if err != nil { 77 | log.Print(err) 78 | continue 79 | } 80 | commentDB, err := models.GetCommentDBById(commentID) 81 | if err != nil { 82 | log.Print(err) 83 | continue 84 | } 85 | commentJSON, err := json.Marshal(commentDB.ToComment()) 86 | rdb.Set(context.Background(), "comment:"+id, commentJSON, time.Hour*24) 87 | comments = append(comments, commentDB.ToComment()) 88 | continue 89 | } 90 | 91 | err := json.Unmarshal([]byte(commentJSON.Val()), &comment) 92 | if err != nil { 93 | log.Print(err) 94 | continue 95 | } 96 | comments = append(comments, comment) 97 | } 98 | log.Print(comments) 99 | return comments 100 | } 101 | 102 | func (commentService CommentServiceImpl) DeleteComments(commentId int64) error { 103 | rdb := utils.GetRedisDB() 104 | 105 | exist := bloomFilter.BloomFilter.Test([]byte(strconv.Itoa(int(commentId)))) 106 | if !exist { 107 | return errors.New("comment id not exist") 108 | } 109 | 110 | //check id exist 111 | commentExistKey := "comment:" + strconv.Itoa(int(commentId)) 112 | if (rdb.Exists(context.Background(), commentExistKey)).Val() == 0 { 113 | _, err := models.GetCommentDBById(commentId) 114 | if err != nil { 115 | return err 116 | } 117 | } 118 | //delete comment id 119 | rdb.Del(context.Background(), commentExistKey) 120 | 121 | toMQ := models.CommentMQToVideo{ 122 | ActionType: 2, 123 | UserId: models.User{}, 124 | VideoId: -1, 125 | Content: "", 126 | CommentID: commentId, 127 | } 128 | mq.CommentChannel <- toMQ 129 | return nil 130 | } 131 | 132 | func commentActionConsumer() { 133 | for { 134 | select { 135 | case commentMQ := <-mq.CommentChannel: 136 | switch commentMQ.ActionType { 137 | case 1: 138 | //save comment 139 | commentDB := commentMQ.ToCommentDB() 140 | err := models.SaveComment(&commentDB) 141 | if err != nil { 142 | log.Print(err) 143 | continue 144 | } 145 | //save to redis 146 | rdb := utils.GetRedisDB() 147 | //set comment id 148 | commentExistKey := "comment:" + strconv.Itoa(int(commentDB.Id)) 149 | //set comment to video 150 | rdb.ZAdd(context.Background(), strconv.Itoa(int(commentDB.VideoId)), &redis.Z{ 151 | Score: float64(commentDB.CreateDate.Unix()), 152 | Member: commentDB.Id, 153 | }) 154 | commentJSON, err := json.Marshal(commentMQ.ToComment()) 155 | rdb.Set(context.Background(), commentExistKey, commentJSON, 0) 156 | if err != nil { 157 | log.Print(err) 158 | continue 159 | } 160 | case 2: 161 | commentDB, _ := models.GetCommentDBById(commentMQ.CommentID) 162 | videoID := commentDB.VideoId 163 | models.DeleteComment(commentMQ.CommentID) 164 | 165 | rdb := utils.GetRedisDB() 166 | rdb.Del(context.Background(), "comment:"+strconv.Itoa(int(commentMQ.CommentID))) 167 | rdb.ZRem(context.Background(), strconv.Itoa(int(videoID)), commentMQ.CommentID) 168 | 169 | default: 170 | time.Sleep(1 * time.Millisecond) 171 | } 172 | } 173 | } 174 | } 175 | 176 | func MakeCommentGoroutine() { 177 | numConsumer := 20 178 | for i := 0; i < numConsumer; i++ { 179 | go commentActionConsumer() 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /sql/20230728v1douyin.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Navicat Premium Data Transfer 3 | 4 | Source Server : mydp 5 | Source Server Type : MySQL 6 | Source Server Version : 50714 (5.7.14) 7 | Source Host : localhost:3306 8 | Source Schema : douyin 9 | 10 | Target Server Type : MySQL 11 | Target Server Version : 50714 (5.7.14) 12 | File Encoding : 65001 13 | 14 | Date: 28/07/2023 12:35:23 15 | */ 16 | 17 | SET NAMES utf8mb4; 18 | SET FOREIGN_KEY_CHECKS = 0; 19 | 20 | -- ---------------------------- 21 | -- Table structure for comment 22 | -- ---------------------------- 23 | DROP TABLE IF EXISTS `comment`; 24 | CREATE TABLE `comment` ( 25 | `id` bigint(64) NOT NULL, 26 | `user_id` bigint(64) NOT NULL COMMENT '评论用户的id', 27 | `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '评论内容', 28 | `vedio_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '评论的视频id', 29 | `create_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, 30 | `is_deleted` int(1) NULL DEFAULT NULL, 31 | PRIMARY KEY (`id`) USING BTREE 32 | ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; 33 | 34 | -- ---------------------------- 35 | -- Records of comment 36 | -- ---------------------------- 37 | 38 | -- ---------------------------- 39 | -- Table structure for follow 40 | -- ---------------------------- 41 | DROP TABLE IF EXISTS `follow`; 42 | CREATE TABLE `follow` ( 43 | `id` bigint(64) NOT NULL, 44 | `user_id` bigint(64) NULL DEFAULT NULL COMMENT '用户id', 45 | `follow_user_id` bigint(64) NULL DEFAULT NULL COMMENT '关注的用户id', 46 | `create_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, 47 | `is_deleted` int(1) NULL DEFAULT NULL, 48 | PRIMARY KEY (`id`) USING BTREE 49 | ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; 50 | 51 | -- ---------------------------- 52 | -- Records of follow 53 | -- ---------------------------- 54 | 55 | -- ---------------------------- 56 | -- Table structure for like 57 | -- ---------------------------- 58 | DROP TABLE IF EXISTS `like`; 59 | CREATE TABLE `like` ( 60 | `id` bigint(64) NOT NULL, 61 | `vedio_id` bigint(64) NULL DEFAULT NULL, 62 | `user_id` bigint(64) NULL DEFAULT NULL, 63 | `create_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, 64 | `is_deleted` int(1) NULL DEFAULT NULL, 65 | PRIMARY KEY (`id`) USING BTREE 66 | ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; 67 | 68 | -- ---------------------------- 69 | -- Records of like 70 | -- ---------------------------- 71 | 72 | -- ---------------------------- 73 | -- Table structure for message 74 | -- ---------------------------- 75 | DROP TABLE IF EXISTS `message`; 76 | CREATE TABLE `message` ( 77 | `id` bigint(64) NOT NULL, 78 | `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '消息内容', 79 | `create_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, 80 | `is_deleted` int(1) NULL DEFAULT NULL, 81 | PRIMARY KEY (`id`) USING BTREE 82 | ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; 83 | 84 | -- ---------------------------- 85 | -- Records of message 86 | -- ---------------------------- 87 | 88 | -- ---------------------------- 89 | -- Table structure for message_push_event 90 | -- ---------------------------- 91 | DROP TABLE IF EXISTS `message_push_event`; 92 | CREATE TABLE `message_push_event` ( 93 | `id` bigint(64) NOT NULL, 94 | `from_user_id` bigint(64) NULL DEFAULT NULL COMMENT '发送者的id', 95 | `msg_content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '消息内容', 96 | `create_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, 97 | `is_deleted` int(1) NULL DEFAULT NULL, 98 | PRIMARY KEY (`id`) USING BTREE 99 | ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; 100 | 101 | -- ---------------------------- 102 | -- Records of message_push_event 103 | -- ---------------------------- 104 | 105 | -- ---------------------------- 106 | -- Table structure for message_send_event 107 | -- ---------------------------- 108 | DROP TABLE IF EXISTS `message_send_event`; 109 | CREATE TABLE `message_send_event` ( 110 | `id` bigint(64) NOT NULL, 111 | `user_id` bigint(64) NOT NULL, 112 | `to_user_id` bigint(64) NOT NULL, 113 | `msg_content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, 114 | `create_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, 115 | `is_deleted` int(1) NULL DEFAULT NULL, 116 | PRIMARY KEY (`id`) USING BTREE 117 | ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; 118 | 119 | -- ---------------------------- 120 | -- Records of message_send_event 121 | -- ---------------------------- 122 | 123 | -- ---------------------------- 124 | -- Table structure for user 125 | -- ---------------------------- 126 | DROP TABLE IF EXISTS `user`; 127 | CREATE TABLE `user` ( 128 | `id` bigint(64) NOT NULL COMMENT '用户id', 129 | `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '姓名', 130 | `follow_count` int(8) NULL DEFAULT NULL COMMENT '关注数', 131 | `follower_count` int(8) NULL DEFAULT NULL COMMENT '粉丝数', 132 | `phone` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '电话', 133 | `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '密码', 134 | `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '头像', 135 | `gender` int(2) NULL DEFAULT NULL COMMENT '性别', 136 | `age` int(2) NULL DEFAULT NULL COMMENT '年龄', 137 | `create_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, 138 | `is_deleted` int(1) NULL DEFAULT NULL, 139 | `nickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '昵称', 140 | `signature` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '个人简介', 141 | `total_favorited` int(22) NULL DEFAULT NULL COMMENT '获赞数量', 142 | `work_count` int(22) NULL DEFAULT NULL COMMENT '作品数', 143 | `favorite_count` int(22) NULL DEFAULT NULL COMMENT '喜欢数', 144 | `is_follow` int(11) NULL DEFAULT NULL COMMENT '是否关注', 145 | `background_image` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '个人背景图片', 146 | PRIMARY KEY (`id`) USING BTREE 147 | ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; 148 | 149 | -- ---------------------------- 150 | -- Records of user 151 | -- ---------------------------- 152 | INSERT INTO `user` VALUES (7089783222816474111, '张四', 1, 1, '1', '1', '1', 1, 1, '2023-07-27 20:41:55', 0, '1', '0', 0, 0, 0, 1, NULL); 153 | INSERT INTO `user` VALUES (7090306410939941888, '20202231014@163.com', 0, 0, '', '$2a$10$t7RCzWVc1A/ReQPi8awWsu0MnnhAdwBTLzCsW1CWaHw1TU/64XIkG', '', 0, 0, '2023-07-27 20:26:20', 0, '', '', 0, 0, 0, 0, ''); 154 | 155 | -- ---------------------------- 156 | -- Table structure for video 157 | -- ---------------------------- 158 | DROP TABLE IF EXISTS `video`; 159 | CREATE TABLE `video` ( 160 | `id` bigint(64) NOT NULL, 161 | `author_id` bigint(64) NOT NULL COMMENT '视频作者', 162 | `play_url` varchar(2048) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '播放路径', 163 | `cover_url` varchar(2048) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, 164 | `favorite_count` int(8) NULL DEFAULT NULL COMMENT '喜欢数量', 165 | `comment_count` int(8) NULL DEFAULT NULL COMMENT '评论数量', 166 | `is_favorite` int(2) NULL DEFAULT NULL, 167 | `create_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, 168 | `is_deleted` int(1) NULL DEFAULT NULL, 169 | `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '视频标题', 170 | PRIMARY KEY (`id`) USING BTREE 171 | ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; 172 | 173 | -- ---------------------------- 174 | -- Records of video 175 | -- ---------------------------- 176 | INSERT INTO `video` VALUES (7089783222816474111, 7089783222816474111, 'https://www.w3schools.com/html/movie.mp4', 'https://cdn.pixabay.com/photo/2016/03/27/18/10/bear-1283347_1280.jpg', 0, 0, 0, '2023-07-28 12:33:04', 0, '台风'); 177 | INSERT INTO `video` VALUES (7089783222816474112, 7089783222816474111, 'https://cccimg.com/view.php/686384315ac21f0f67170063e07b1f75.mp4', 'https://img1.imgtp.com/2023/07/28/PGnC0crf.png', 0, 0, 0, '2023-07-28 10:36:42', 0, '熊'); 178 | 179 | -- ---------------------------- 180 | -- Procedure structure for addFollowRelation 181 | -- ---------------------------- 182 | DROP PROCEDURE IF EXISTS `addFollowRelation`; 183 | delimiter ;; 184 | CREATE PROCEDURE `addFollowRelation`(IN user_id int,IN follower_id int) 185 | BEGIN 186 | #Routine body goes here... 187 | # 声明记录个数变量。 188 | DECLARE cnt INT DEFAULT 0; 189 | # 获取记录个数变量。 190 | SELECT COUNT(1) FROM follows f where f.user_id = user_id AND f.follower_id = follower_id INTO cnt; 191 | # 判断是否已经存在该记录,并做出相应的插入关系、更新关系动作。 192 | # 插入操作。 193 | IF cnt = 0 THEN 194 | INSERT INTO follows(`user_id`,`follower_id`) VALUES(user_id,follower_id); 195 | END IF; 196 | # 更新操作 197 | IF cnt != 0 THEN 198 | UPDATE follows f SET f.cancel = 0 WHERE f.user_id = user_id AND f.follower_id = follower_id; 199 | END IF; 200 | END 201 | ;; 202 | delimiter ; 203 | 204 | -- ---------------------------- 205 | -- Procedure structure for delFollowRelation 206 | -- ---------------------------- 207 | DROP PROCEDURE IF EXISTS `delFollowRelation`; 208 | delimiter ;; 209 | CREATE PROCEDURE `delFollowRelation`(IN `user_id` int,IN `follower_id` int) 210 | BEGIN 211 | #Routine body goes here... 212 | # 定义记录个数变量,记录是否存在此关系,默认没有关系。 213 | DECLARE cnt INT DEFAULT 0; 214 | # 查看是否之前有关系。 215 | SELECT COUNT(1) FROM follows f WHERE f.user_id = user_id AND f.follower_id = follower_id INTO cnt; 216 | # 有关系,则需要update cancel = 1,使其关系无效。 217 | IF cnt = 1 THEN 218 | UPDATE follows f SET f.cancel = 1 WHERE f.user_id = user_id AND f.follower_id = follower_id; 219 | END IF; 220 | END 221 | ;; 222 | delimiter ; 223 | 224 | SET FOREIGN_KEY_CHECKS = 1; 225 | -------------------------------------------------------------------------------- /sql/20230728v2douyin.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Navicat Premium Data Transfer 3 | 4 | Source Server : mydp 5 | Source Server Type : MySQL 6 | Source Server Version : 50714 (5.7.14) 7 | Source Host : localhost:3306 8 | Source Schema : douyin 9 | 10 | Target Server Type : MySQL 11 | Target Server Version : 50714 (5.7.14) 12 | File Encoding : 65001 13 | 14 | Date: 28/07/2023 17:30:49 15 | */ 16 | 17 | SET NAMES utf8mb4; 18 | SET FOREIGN_KEY_CHECKS = 0; 19 | 20 | -- ---------------------------- 21 | -- Table structure for comment 22 | -- ---------------------------- 23 | DROP TABLE IF EXISTS `comment`; 24 | CREATE TABLE `comment` ( 25 | `id` bigint(64) NOT NULL, 26 | `user_id` bigint(64) NOT NULL COMMENT '评论用户的id', 27 | `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '评论内容', 28 | `video_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '评论的视频id', 29 | `create_date` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, 30 | `is_deleted` int(1) NULL DEFAULT NULL, 31 | PRIMARY KEY (`id`) USING BTREE 32 | ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; 33 | 34 | -- ---------------------------- 35 | -- Records of comment 36 | -- ---------------------------- 37 | 38 | -- ---------------------------- 39 | -- Table structure for follow 40 | -- ---------------------------- 41 | DROP TABLE IF EXISTS `follow`; 42 | CREATE TABLE `follow` ( 43 | `id` bigint(64) NOT NULL, 44 | `user_id` bigint(64) NULL DEFAULT NULL COMMENT '用户id', 45 | `follow_user_id` bigint(64) NULL DEFAULT NULL COMMENT '关注的用户id', 46 | `create_date` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, 47 | `is_deleted` int(1) NULL DEFAULT NULL, 48 | PRIMARY KEY (`id`) USING BTREE 49 | ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; 50 | 51 | -- ---------------------------- 52 | -- Records of follow 53 | -- ---------------------------- 54 | 55 | -- ---------------------------- 56 | -- Table structure for like 57 | -- ---------------------------- 58 | DROP TABLE IF EXISTS `like`; 59 | CREATE TABLE `like` ( 60 | `id` bigint(64) NOT NULL, 61 | `video_id` bigint(64) NULL DEFAULT NULL, 62 | `user_id` bigint(64) NULL DEFAULT NULL, 63 | `create_date` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, 64 | `is_deleted` int(1) NULL DEFAULT NULL, 65 | PRIMARY KEY (`id`) USING BTREE 66 | ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; 67 | 68 | -- ---------------------------- 69 | -- Records of like 70 | -- ---------------------------- 71 | 72 | -- ---------------------------- 73 | -- Table structure for message 74 | -- ---------------------------- 75 | DROP TABLE IF EXISTS `message`; 76 | CREATE TABLE `message` ( 77 | `id` bigint(64) NOT NULL, 78 | `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '消息内容', 79 | `create_date` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, 80 | `is_deleted` int(1) NULL DEFAULT NULL, 81 | PRIMARY KEY (`id`) USING BTREE 82 | ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; 83 | 84 | -- ---------------------------- 85 | -- Records of message 86 | -- ---------------------------- 87 | 88 | -- ---------------------------- 89 | -- Table structure for message_push_event 90 | -- ---------------------------- 91 | DROP TABLE IF EXISTS `message_push_event`; 92 | CREATE TABLE `message_push_event` ( 93 | `id` bigint(64) NOT NULL, 94 | `from_user_id` bigint(64) NULL DEFAULT NULL COMMENT '发送者的id', 95 | `msg_content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '消息内容', 96 | `create_date` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, 97 | `is_deleted` int(1) NULL DEFAULT NULL, 98 | PRIMARY KEY (`id`) USING BTREE 99 | ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; 100 | 101 | -- ---------------------------- 102 | -- Records of message_push_event 103 | -- ---------------------------- 104 | 105 | -- ---------------------------- 106 | -- Table structure for message_send_event 107 | -- ---------------------------- 108 | DROP TABLE IF EXISTS `message_send_event`; 109 | CREATE TABLE `message_send_event` ( 110 | `id` bigint(64) NOT NULL, 111 | `user_id` bigint(64) NOT NULL, 112 | `to_user_id` bigint(64) NOT NULL, 113 | `msg_content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, 114 | `create_date` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, 115 | `is_deleted` int(1) NULL DEFAULT NULL, 116 | PRIMARY KEY (`id`) USING BTREE 117 | ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; 118 | 119 | -- ---------------------------- 120 | -- Records of message_send_event 121 | -- ---------------------------- 122 | 123 | -- ---------------------------- 124 | -- Table structure for user 125 | -- ---------------------------- 126 | DROP TABLE IF EXISTS `user`; 127 | CREATE TABLE `user` ( 128 | `id` bigint(64) NOT NULL COMMENT '用户id', 129 | `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '姓名', 130 | `follow_count` int(8) NULL DEFAULT NULL COMMENT '关注数', 131 | `follower_count` int(8) NULL DEFAULT NULL COMMENT '粉丝数', 132 | `phone` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '电话', 133 | `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '密码', 134 | `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '头像', 135 | `gender` int(2) NULL DEFAULT NULL COMMENT '性别', 136 | `age` int(2) NULL DEFAULT NULL COMMENT '年龄', 137 | `create_date` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, 138 | `is_deleted` int(1) NULL DEFAULT NULL, 139 | `nickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '昵称', 140 | `signature` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '个人简介', 141 | `total_favorited` int(22) NULL DEFAULT NULL COMMENT '获赞数量', 142 | `work_count` int(22) NULL DEFAULT NULL COMMENT '作品数', 143 | `favorite_count` int(22) NULL DEFAULT NULL COMMENT '喜欢数', 144 | `is_follow` int(11) NULL DEFAULT NULL COMMENT '是否关注', 145 | `background_image` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '个人背景图片', 146 | PRIMARY KEY (`id`) USING BTREE 147 | ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; 148 | 149 | -- ---------------------------- 150 | -- Records of user 151 | -- ---------------------------- 152 | INSERT INTO `user` VALUES (7089783222816474111, '张四', 1, 1, '1', '1', '1', 1, 1, '2023-07-27 20:41:55', 0, '1', '0', 0, 0, 0, 1, NULL); 153 | INSERT INTO `user` VALUES (7090306410939941888, '20202231014@163.com', 0, 0, '', '$2a$10$t7RCzWVc1A/ReQPi8awWsu0MnnhAdwBTLzCsW1CWaHw1TU/64XIkG', '', 0, 0, '2023-07-27 20:26:20', 0, '', '', 0, 0, 0, 0, ''); 154 | 155 | -- ---------------------------- 156 | -- Table structure for video 157 | -- ---------------------------- 158 | DROP TABLE IF EXISTS `video`; 159 | CREATE TABLE `video` ( 160 | `id` bigint(64) NOT NULL, 161 | `author_id` bigint(64) NOT NULL COMMENT '视频作者', 162 | `play_url` varchar(2048) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '播放路径', 163 | `cover_url` varchar(2048) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, 164 | `favorite_count` int(8) NULL DEFAULT NULL COMMENT '喜欢数量', 165 | `comment_count` int(8) NULL DEFAULT NULL COMMENT '评论数量', 166 | `is_favorite` int(2) NULL DEFAULT NULL, 167 | `create_date` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, 168 | `is_deleted` int(1) NULL DEFAULT NULL, 169 | `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '视频标题', 170 | PRIMARY KEY (`id`) USING BTREE 171 | ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; 172 | 173 | -- ---------------------------- 174 | -- Records of video 175 | -- ---------------------------- 176 | INSERT INTO `video` VALUES (7089783222816474111, 7089783222816474111, 'https://www.w3schools.com/html/movie.mp4', 'https://cdn.pixabay.com/photo/2016/03/27/18/10/bear-1283347_1280.jpg', 0, 0, 0, '2023-07-28 12:33:04', 0, '台风'); 177 | INSERT INTO `video` VALUES (7089783222816474112, 7089783222816474111, 'https://cccimg.com/view.php/686384315ac21f0f67170063e07b1f75.mp4', 'https://img1.imgtp.com/2023/07/28/PGnC0crf.png', 0, 0, 0, '2023-07-28 10:36:42', 0, '熊'); 178 | 179 | -- ---------------------------- 180 | -- Procedure structure for addFollowRelation 181 | -- ---------------------------- 182 | DROP PROCEDURE IF EXISTS `addFollowRelation`; 183 | delimiter ;; 184 | CREATE PROCEDURE `addFollowRelation`(IN user_id int,IN follower_id int) 185 | BEGIN 186 | #Routine body goes here... 187 | # 声明记录个数变量。 188 | DECLARE cnt INT DEFAULT 0; 189 | # 获取记录个数变量。 190 | SELECT COUNT(1) FROM follows f where f.user_id = user_id AND f.follower_id = follower_id INTO cnt; 191 | # 判断是否已经存在该记录,并做出相应的插入关系、更新关系动作。 192 | # 插入操作。 193 | IF cnt = 0 THEN 194 | INSERT INTO follows(`user_id`,`follower_id`) VALUES(user_id,follower_id); 195 | END IF; 196 | # 更新操作 197 | IF cnt != 0 THEN 198 | UPDATE follows f SET f.cancel = 0 WHERE f.user_id = user_id AND f.follower_id = follower_id; 199 | END IF; 200 | END 201 | ;; 202 | delimiter ; 203 | 204 | -- ---------------------------- 205 | -- Procedure structure for delFollowRelation 206 | -- ---------------------------- 207 | DROP PROCEDURE IF EXISTS `delFollowRelation`; 208 | delimiter ;; 209 | CREATE PROCEDURE `delFollowRelation`(IN `user_id` int,IN `follower_id` int) 210 | BEGIN 211 | #Routine body goes here... 212 | # 定义记录个数变量,记录是否存在此关系,默认没有关系。 213 | DECLARE cnt INT DEFAULT 0; 214 | # 查看是否之前有关系。 215 | SELECT COUNT(1) FROM follows f WHERE f.user_id = user_id AND f.follower_id = follower_id INTO cnt; 216 | # 有关系,则需要update cancel = 1,使其关系无效。 217 | IF cnt = 1 THEN 218 | UPDATE follows f SET f.cancel = 1 WHERE f.user_id = user_id AND f.follower_id = follower_id; 219 | END IF; 220 | END 221 | ;; 222 | delimiter ; 223 | 224 | SET FOREIGN_KEY_CHECKS = 1; 225 | -------------------------------------------------------------------------------- /service/impl/UserServiceImpl.go: -------------------------------------------------------------------------------- 1 | package impl 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "github.com/RaymondCode/simple-demo/config" 7 | "github.com/RaymondCode/simple-demo/models" 8 | "github.com/RaymondCode/simple-demo/mq" 9 | "github.com/RaymondCode/simple-demo/utils" 10 | "github.com/gin-gonic/gin" 11 | "github.com/streadway/amqp" 12 | "golang.org/x/crypto/bcrypt" 13 | "log" 14 | "net/http" 15 | "sync/atomic" 16 | "time" 17 | ) 18 | 19 | type UserServiceImpl struct { 20 | } 21 | 22 | func (userService UserServiceImpl) GetUserById(Id int64) (models.User, error) { 23 | result, err := models.GetUserById(Id) 24 | if err != nil { 25 | log.Printf("方法GetUserById() 失败 %v", err) 26 | return result, err 27 | } 28 | return result, nil 29 | } 30 | 31 | func (userService UserServiceImpl) GetUserByName(name string) (models.User, error) { 32 | result, err := models.GetUserByName(name) 33 | if err != nil { 34 | log.Printf("方法GetUserById() 失败 %v", err) 35 | return result, err 36 | } 37 | return result, nil 38 | } 39 | 40 | func (userService UserServiceImpl) Save(user models.User) error { 41 | return models.SaveUser(user) 42 | } 43 | 44 | /* 45 | ( 46 | 已完成 47 | */ 48 | func (userService UserServiceImpl) Register(username string, password string, c *gin.Context) error { 49 | var userIdSequence = int64(1) 50 | _, errName := userService.GetUserByName(username) 51 | if errName == nil { 52 | c.JSON(http.StatusBadRequest, UserLoginResponse{ 53 | Response: models.Response{StatusCode: 1, StatusMsg: "用户名重复"}, 54 | }) 55 | return nil 56 | } 57 | //var userRequest UserRequest 58 | //if err := c.ShouldBindJSON(&userRequest); err != nil { 59 | // c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 60 | // return 61 | //} 62 | //username := userRequest.Username 63 | //password := userRequest.Password 64 | //加密 65 | encrypt, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) 66 | password = string(encrypt) 67 | 68 | atomic.AddInt64(&userIdSequence, 1) 69 | newUser := models.User{ 70 | CommonEntity: utils.NewCommonEntity(), 71 | Name: username, 72 | Password: password, 73 | } 74 | 75 | err := userService.Save(newUser) 76 | if err != nil { 77 | c.JSON(http.StatusInternalServerError, UserLoginResponse{ 78 | Response: models.Response{StatusCode: 1, StatusMsg: "Cant not save the User!"}, 79 | }) 80 | } else { 81 | token, err1 := utils.GenerateToken(username, newUser.CommonEntity) 82 | if err1 != nil { 83 | log.Printf("Can not get the token!") 84 | } 85 | err2 := utils.SaveTokenToRedis(newUser.Name, token, time.Duration(config.TokenTTL*float64(time.Second))) 86 | if err2 != nil { 87 | log.Printf("Fail : Save token in redis !") 88 | } else { 89 | c.JSON(http.StatusOK, UserLoginResponse{ 90 | Response: models.Response{StatusCode: 0}, 91 | UserId: newUser.Id, 92 | Token: token, 93 | }) 94 | } 95 | } 96 | return nil 97 | } 98 | 99 | /* 100 | * 101 | 已完成 102 | */ 103 | func (userService UserServiceImpl) Login(username string, password string, c *gin.Context) error { 104 | 105 | _, err := userService.GetUserByName(username) 106 | if err != nil { 107 | c.JSON(http.StatusBadRequest, UserLoginResponse{ 108 | Response: models.Response{StatusCode: 1, StatusMsg: "用户不存在,请注册!"}, 109 | }) 110 | return nil 111 | } 112 | 113 | user, err1 := userService.GetUserByName(username) 114 | if err1 != nil { 115 | return err1 116 | } 117 | 118 | pwdErr := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) 119 | if pwdErr != nil { 120 | c.JSON(http.StatusOK, UserLoginResponse{ 121 | Response: models.Response{StatusCode: 1, StatusMsg: "密码错误!"}, 122 | }) 123 | return pwdErr 124 | } 125 | 126 | token, err2 := utils.GenerateToken(username, user.CommonEntity) 127 | if err2 != nil { 128 | c.JSON(http.StatusInternalServerError, UserLoginResponse{ 129 | Response: models.Response{StatusCode: 1, StatusMsg: "生成token失败"}, 130 | }) 131 | return err2 132 | } 133 | 134 | err3 := utils.SaveTokenToRedis(user.Name, token, time.Duration(config.TokenTTL*float64(time.Second))) 135 | if err3 != nil { 136 | log.Printf("Fail : Save token in redis !") 137 | // TODO 开发完成后整理这个返回体 返回信息不能这么填 138 | c.JSON(http.StatusInternalServerError, UserLoginResponse{ 139 | Response: models.Response{StatusCode: 1, StatusMsg: "无法保存token 请检查redis连接"}, 140 | }) 141 | return err3 142 | } 143 | 144 | c.JSON(http.StatusOK, UserLoginResponse{ 145 | Response: models.Response{StatusCode: 0, StatusMsg: "登录成功!"}, 146 | UserId: user.Id, 147 | Token: token, 148 | }) 149 | return nil 150 | } 151 | 152 | // LikeConsume 消费"userLikeMQ"中的消息 153 | func (userService UserServiceImpl) LikeConsume(l *mq.LikeMQ) { 154 | _, err := l.Channel.QueueDeclare(l.QueueUserName, true, false, false, false, nil) 155 | if err != nil { 156 | panic(err) 157 | } 158 | //2、接收消息 159 | messages, err1 := l.Channel.Consume( 160 | l.QueueUserName, 161 | //用来区分多个消费者 162 | "", 163 | //是否自动应答 164 | true, 165 | //是否具有排他性 166 | false, 167 | //如果设置为true,表示不能将同一个connection中发送的消息传递给这个connection中的消费者 168 | false, 169 | //消息队列是否阻塞 170 | false, 171 | nil, 172 | ) 173 | if err1 != nil { 174 | panic(err1) 175 | } 176 | go userService.likeConsume(messages) 177 | //forever := make(chan bool) 178 | //log.Println(messages) 179 | 180 | log.Printf("[*] Waiting for messagees,To exit press CTRL+C") 181 | } 182 | 183 | // FollowConsume 消费"followMQ"中的消息 184 | func (userService UserServiceImpl) FollowConsume(followMQ *mq.FollowMQ) { 185 | _, err := followMQ.Channel.QueueDeclare(followMQ.QueueName, true, false, false, false, nil) 186 | if err != nil { 187 | panic(err) 188 | } 189 | 190 | messages, err1 := followMQ.Channel.Consume( 191 | followMQ.QueueName, 192 | //用来区分多个消费者 193 | "", 194 | //是否自动应答 195 | true, 196 | //是否具有排他性 197 | false, 198 | //如果设置为true,表示不能将同一个connection中发送的消息传递给这个connection中的消费者 199 | false, 200 | //消息队列是否阻塞 201 | false, 202 | nil, 203 | ) 204 | if err1 != nil { 205 | panic(err1) 206 | } 207 | go userService.followConsume(messages) 208 | //forever := make(chan bool) 209 | //log.Println(messages) 210 | 211 | log.Printf("[*] Waiting for messagees,To exit press CTRL+C") 212 | } 213 | 214 | // 点赞具体消费逻辑 215 | func (userService UserServiceImpl) likeConsume(message <-chan amqp.Delivery) { 216 | for d := range message { 217 | jsonData := string(d.Body) 218 | log.Printf("user收到的消息为 %s\n", jsonData) 219 | data := models.LikeMQToUser{} 220 | err := json.Unmarshal([]byte(jsonData), &data) 221 | if err != nil { 222 | panic(err) 223 | } 224 | userId := data.UserId 225 | tx := utils.GetMysqlDB().Begin() 226 | //获得当前用户 227 | user, err := models.GetUserById(userId) 228 | 229 | //查询视频作者 230 | author, err2 := models.GetUserById(data.AuthorId) 231 | if err2 != nil { 232 | panic(err2) 233 | } 234 | actionType := data.ActionType 235 | 236 | if actionType == 1 { 237 | //喜欢数量+一 238 | user.FavoriteCount = user.FavoriteCount + 1 239 | //如果是同一个作者,在同一个事务中必须保证针对同一行的操作只出现一次 240 | if user.Id == author.Id { 241 | user.TotalFavorited++ 242 | } 243 | err = models.UpdateUser(tx, user) 244 | if err != nil { 245 | log.Println("err:", err) 246 | tx.Rollback() 247 | panic(err) 248 | } 249 | if user.Id != author.Id { 250 | //总点赞数+1 251 | author.TotalFavorited = author.TotalFavorited + 1 252 | err = models.UpdateUser(tx, author) 253 | if err != nil { 254 | log.Println("err:", err) 255 | tx.Rollback() 256 | panic(err) 257 | } 258 | } 259 | 260 | } else { 261 | //喜欢数量-1 262 | user.FavoriteCount = user.FavoriteCount - 1 263 | //如果是同一个作者,在同一个事务中必须保证针对同一行的操作只出现一次 264 | if user.Id == author.Id { 265 | user.TotalFavorited-- 266 | } 267 | err = models.UpdateUser(tx, user) 268 | if err != nil { 269 | log.Println("err:", err) 270 | tx.Rollback() 271 | panic(err) 272 | } 273 | //总点赞数-1 274 | if user.Id != author.Id { 275 | author.TotalFavorited = author.TotalFavorited - 1 276 | err = models.UpdateUser(tx, author) 277 | if err != nil { 278 | log.Println("err:", err) 279 | tx.Rollback() 280 | panic(err) 281 | } 282 | } 283 | } 284 | tx.Commit() 285 | } 286 | } 287 | 288 | // 关注具体消费逻辑 289 | func (userService UserServiceImpl) followConsume(message <-chan amqp.Delivery) { 290 | for d := range message { 291 | jsonData := string(d.Body) 292 | log.Printf("user收到的消息为 %s\n", jsonData) 293 | data := models.FollowMQToUser{} 294 | err := json.Unmarshal([]byte(jsonData), &data) 295 | if err != nil { 296 | panic(err) 297 | } 298 | userId := data.UserId 299 | tx := utils.GetMysqlDB().Begin() 300 | //获得当前用户 301 | user, err := models.GetUserById(userId) 302 | 303 | //查询视频作者 304 | toUser, err2 := models.GetUserById(data.FollowUserId) 305 | if err2 != nil { 306 | panic(err2) 307 | } 308 | actionType := data.ActionType 309 | 310 | if actionType == 1 { 311 | //喜欢数量+一 312 | user.FollowCount = user.FollowCount + 1 313 | err = models.UpdateUser(tx, user) 314 | if err != nil { 315 | log.Println("err:", err) 316 | tx.Rollback() 317 | panic(err) 318 | } 319 | //总点赞数+1 320 | toUser.FollowerCount = toUser.FollowerCount + 1 321 | err = models.UpdateUser(tx, toUser) 322 | if err != nil { 323 | log.Println("err:", err) 324 | tx.Rollback() 325 | panic(err) 326 | } 327 | 328 | } else { 329 | //喜欢数量-1 330 | user.FollowCount = user.FollowCount - 1 331 | 332 | err = models.UpdateUser(tx, user) 333 | if err != nil { 334 | log.Println("err:", err) 335 | tx.Rollback() 336 | panic(err) 337 | } 338 | //总点赞数-1 339 | toUser.FollowerCount = toUser.FollowerCount - 1 340 | err = models.UpdateUser(tx, toUser) 341 | if err != nil { 342 | log.Println("err:", err) 343 | tx.Rollback() 344 | panic(err) 345 | } 346 | } 347 | tx.Commit() 348 | } 349 | } 350 | 351 | // 创建点赞消费者协程 352 | func (userService UserServiceImpl) MakeLikeConsumers() { 353 | numConsumers := 20 354 | for i := 0; i < numConsumers; i++ { 355 | go userService.LikeConsume(mq.LikeRMQ) 356 | } 357 | } 358 | 359 | // 创建关注消费者协程 360 | func (userService UserServiceImpl) MakeFollowConsumers() { 361 | numConsumers := 20 362 | for i := 0; i < numConsumers; i++ { 363 | go userService.FollowConsume(mq.FollowRMQ) 364 | } 365 | } 366 | 367 | func (userService UserServiceImpl) UserInfo(userId int64, token string) (*models.User, error) { 368 | //userClaims, err := utils.AnalyseToken(token) 369 | //if err != nil || userClaims == nil { 370 | // return nil, errors.New("用户未登录") 371 | //} 372 | user, err1 := userService.GetUserById(userId) 373 | if err1 != nil { 374 | return nil, errors.New("用户不存在!") 375 | } 376 | return &user, nil 377 | } 378 | 379 | type UserResponse struct { 380 | models.Response 381 | User models.User `json:"user"` 382 | } 383 | 384 | type UserRequest struct { 385 | Username string `json:"username"` 386 | Password string `json:"password"` 387 | } 388 | 389 | type UserLoginResponse struct { 390 | models.Response 391 | UserId int64 `json:"user_id,omitempty"` 392 | Token string `json:"token"` 393 | } 394 | -------------------------------------------------------------------------------- /service/impl/FavoriteServiceImpl.go: -------------------------------------------------------------------------------- 1 | package impl 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "github.com/RaymondCode/simple-demo/config" 9 | "github.com/RaymondCode/simple-demo/mq" 10 | "log" 11 | "strconv" 12 | "sync" 13 | "time" 14 | 15 | "github.com/RaymondCode/simple-demo/models" 16 | "github.com/RaymondCode/simple-demo/utils" 17 | "gorm.io/gorm" 18 | ) 19 | 20 | // "github.com/RaymondCode/simple-demo/service" 21 | 22 | type FavoriteServiceImpl struct { 23 | } 24 | 25 | // LikeVedio 点赞或者取消点赞 26 | func (favoriteService FavoriteServiceImpl) LikeVideo(userId int64, videoId int64, actionType int) error { 27 | //分布式锁 不能让用户连续两次点赞或者取消同一个视频的请求进入 28 | userIdStr := strconv.FormatInt(userId, 10) 29 | videoIdStr := strconv.FormatInt(videoId, 10) 30 | 31 | lockKey := config.LikeLock + userIdStr + videoIdStr 32 | unLockKey := config.UnLikeLock + userIdStr + videoIdStr 33 | 34 | if actionType == 1 { 35 | isSuccess, _ := utils.GetRedisDB().SetNX(context.Background(), lockKey, "0", time.Duration(config.LikeLockTTL)*time.Second).Result() 36 | if isSuccess == false { 37 | log.Println("已点赞") 38 | return errors.New("-1") 39 | } else { 40 | utils.GetRedisDB().Del(context.Background(), unLockKey) 41 | } 42 | } else { 43 | isSuccess, _ := utils.GetRedisDB().SetNX(context.Background(), unLockKey, "0", time.Duration(config.LikeLockTTL)*time.Second).Result() 44 | if isSuccess == false { 45 | log.Println("已取消") 46 | return errors.New("-2") 47 | } else { 48 | utils.GetRedisDB().Del(context.Background(), lockKey) 49 | } 50 | } 51 | var err error 52 | //tx := utils.GetMysqlDB().Begin() 53 | l := models.Like{ 54 | UserId: userId, 55 | VideoId: videoId, 56 | } 57 | var faInDB *(models.Like) 58 | 59 | var isExists bool = false 60 | userLikeKey := config.LikeKey + userIdStr 61 | // 看看缓存中有没有这个集合 62 | exits, _ := utils.GetRedisDB().Exists(context.Background(), userLikeKey).Result() 63 | if exits != 0 { 64 | // 看看这个集合中有没有这个ID 65 | result, _ := utils.GetRedisDB().SIsMember(context.Background(), userLikeKey, videoIdStr).Result() 66 | isExists = result 67 | // 如果缓存里面的 Set 里面没有就要从数据库Z]里面查 68 | if !isExists { 69 | faInDB, err = l.FindByUserIdAndVedioId() 70 | if err != nil { 71 | log.Printf("查询点赞记录发生异常 = %v", err) 72 | return err 73 | } 74 | if faInDB.Id != 0 { 75 | isExists = true 76 | } 77 | } 78 | } else { 79 | // 缓存中没有则从数据库找 80 | faInDB, err = l.FindByUserIdAndVedioId() 81 | if err != nil { 82 | log.Printf("查询点赞记录发生异常 = %v", err) 83 | return err 84 | } 85 | 86 | if faInDB.Id != 0 { 87 | isExists = true 88 | } 89 | 90 | } 91 | 92 | if actionType == 1 { 93 | 94 | if isExists { 95 | log.Printf("该视频已点赞") 96 | //tx.Rollback() 97 | err = errors.New("-1") 98 | return err 99 | } 100 | 101 | //if err = findVideoAndUpdateFavoriteCount(tx, videoId, 1); err != nil { 102 | // log.Printf("修改视频点赞数量发生异常 = %v", err) 103 | // //tx.Rollback() 104 | // return err 105 | //} 106 | // 查视频的作者 107 | author, errAuthorId := models.GetVideoById(videoId) 108 | if errAuthorId != nil { 109 | log.Println("不能找到这个作者") 110 | return errAuthorId 111 | } 112 | authorId := author.AuthorId 113 | 114 | //mqData := models.LikeMQToVideo{UserId: userId, VideoId: videoId, ActionType: actionType} 115 | mqData := models.LikeMQToUser{UserId: userId, VideoId: videoId, ActionType: actionType, AuthorId: authorId} 116 | // 加入 channel 117 | mq.LikeChannel <- mqData 118 | jsonData, err := json.Marshal(mqData) 119 | if err != nil { 120 | log.Println("json序列化失败 = #{err}") 121 | //TODO 处理失败导致的数据不一致 122 | } 123 | //加入消息队列 124 | mq.LikeRMQ.Publish(string(jsonData)) 125 | 126 | return nil 127 | 128 | } else if actionType == 2 { 129 | 130 | if !isExists && (faInDB == nil || faInDB.Id == 0) { 131 | log.Printf("未找到要取消的点赞记录") 132 | err = errors.New("-2") 133 | //tx.Rollback() 134 | return err 135 | } 136 | 137 | //if err = faInDB.Delete(tx); err != nil { 138 | // log.Printf("删除点赞记录发生异常 = %v", err) 139 | // tx.Rollback() 140 | // return err 141 | //} 142 | // 143 | //if err = findVideoAndUpdateFavoriteCount(tx, videoId, -1); err != nil { 144 | // log.Printf("修改视频点赞数量发生异常 = %v", err) 145 | // tx.Rollback() 146 | // return err 147 | //} 148 | // 查视频的作者 149 | author, errAuthorId := models.GetVideoById(videoId) 150 | if errAuthorId != nil { 151 | log.Println("不能找到这个作者") 152 | return errAuthorId 153 | } 154 | authorId := author.AuthorId 155 | 156 | //mqData := models.LikeMQToVideo{UserId: userId, VideoId: videoId, ActionType: actionType} 157 | mqData := models.LikeMQToUser{UserId: userId, VideoId: videoId, ActionType: actionType, AuthorId: authorId} 158 | // 加入 channel 159 | mq.LikeChannel <- mqData 160 | jsonData, err := json.Marshal(mqData) 161 | if err != nil { 162 | log.Printf("json序列化失败 = #{err}") 163 | //TODO 处理失败导致的数据不一致 164 | } 165 | //加入消息队列 166 | mq.LikeRMQ.Publish(string(jsonData)) 167 | // TODO 消息队列处理失败会导致数据不一致 168 | 169 | return nil 170 | 171 | } 172 | 173 | //tx.Commit() 174 | return err 175 | } 176 | 177 | // findVideoAndUpdateFavoriteCount 修改视频的点赞数量,count 为 +-1 178 | func findVideoAndUpdateFavoriteCount(tx *gorm.DB, vid int64, count int64) (err error) { 179 | var vInDB models.Video 180 | if err = tx.Model(&models.Video{}).Where("id = ? and is_deleted = 0", vid).Take(&vInDB).Error; err != nil { 181 | log.Printf("查询视频发生异常 = %v", err) 182 | return 183 | } 184 | fmt.Println(vInDB.CreateDate) 185 | if err = tx.Model(&models.Video{}).Where("id = ?", vid).Update("favorite_count", vInDB.FavoriteCount+count).Error; err != nil { 186 | log.Printf("修改视频点赞数量发生异常 = %v", err) 187 | return 188 | } 189 | fmt.Println(vInDB.CreateDate) 190 | return 191 | } 192 | 193 | func (favoriteService FavoriteServiceImpl) QueryVideosOfLike(userId int64) ([]models.LikeVedioListDVO, error) { 194 | likeKey := config.LikeKey + strconv.FormatInt(userId, 10) 195 | exists, _ := utils.GetRedisDB().Exists(context.Background(), likeKey).Result() 196 | if exists != 0 { 197 | likeIdsSet, _ := utils.GetRedisDB().SMembers(context.Background(), likeKey).Result() 198 | /* var res []models.LikeVedioListDVO 199 | 200 | for i := range likeIdsSet { 201 | id, _ := strconv.ParseInt(likeIdsSet[i], 10, 64) 202 | video, _ := models.GetVideoById(id) 203 | author, _ := models.GetUserById(video.AuthorId) 204 | var likeVideoListDVO models.LikeVedioListDVO 205 | likeVideoListDVO.Author = &author 206 | likeVideoListDVO.Video = video 207 | res = append(res, likeVideoListDVO) 208 | } 209 | */ 210 | var res []models.LikeVedioListDVO 211 | var wg sync.WaitGroup 212 | var mu sync.Mutex // 用于保护 res 的并发访问 213 | var errReturn error 214 | 215 | for i := range likeIdsSet { 216 | wg.Add(1) 217 | //协程并发组装 218 | go func(idStr string) { 219 | defer wg.Done() 220 | 221 | id, _ := strconv.ParseInt(idStr, 10, 64) 222 | 223 | video, err := models.GetVideoById(id) 224 | if err != nil { 225 | errReturn = err 226 | } 227 | 228 | author, err := models.GetUserById(video.AuthorId) 229 | if err != nil { 230 | errReturn = err 231 | } 232 | 233 | var likeVideoListDVO models.LikeVedioListDVO 234 | likeVideoListDVO.Author = &author 235 | likeVideoListDVO.Video = video 236 | // 使用锁保护 res 的并发访问 237 | mu.Lock() 238 | res = append(res, likeVideoListDVO) 239 | mu.Unlock() 240 | }(likeIdsSet[i]) 241 | } 242 | // 等待所有协程完成 243 | wg.Wait() 244 | 245 | if errReturn != nil { 246 | return []models.LikeVedioListDVO{}, errReturn 247 | } 248 | // 现在 res 包含了所有视频的作者和视频信息 249 | 250 | return res, nil 251 | } 252 | 253 | var l models.Like 254 | 255 | var res []models.LikeVedioListDVO 256 | var err error 257 | res, err = l.GetLikeVedioListDVO(userId) 258 | 259 | // 重建缓存 260 | _ = BuildLikeRedis(userId) 261 | if err != nil { 262 | return res, err 263 | } 264 | 265 | return res, err 266 | } 267 | 268 | func (favoriteService FavoriteServiceImpl) FindIsFavouriteByUserIdAndVideoId(userId int64, videoId int64) bool { 269 | //tx := utils.GetMysqlDB() 270 | likeKey := config.LikeKey + strconv.FormatInt(userId, 10) 271 | videoIdStr := strconv.FormatInt(videoId, 10) 272 | exists, _ := utils.GetRedisDB().Exists(context.Background(), likeKey).Result() 273 | if exists != 0 { 274 | videoExists, _ := utils.GetRedisDB().SIsMember(context.Background(), likeKey, videoIdStr).Result() 275 | return videoExists 276 | } 277 | like := models.Like{ 278 | UserId: userId, 279 | VideoId: videoId, 280 | } 281 | 282 | isLike, _ := like.FindByUserIdAndVedioId() 283 | 284 | if isLike.Id != 0 { 285 | return true 286 | } else { 287 | return false 288 | } 289 | } 290 | 291 | func LikeConsumer(ch <-chan models.LikeMQToUser) { 292 | for { 293 | select { 294 | case msg := <-ch: 295 | // 在这里处理接收到的消息 296 | tx := utils.GetMysqlDB().Begin() 297 | if msg.ActionType == 1 { 298 | like := models.Like{ 299 | CommonEntity: utils.NewCommonEntity(), 300 | UserId: msg.UserId, 301 | VideoId: msg.VideoId, 302 | } 303 | err1 := like.Insert(tx) 304 | if err1 != nil { 305 | log.Printf(err1.Error()) 306 | tx.Rollback() 307 | } 308 | video, err2 := models.GetVideoById(msg.VideoId) 309 | if err2 != nil { 310 | log.Printf(err2.Error()) 311 | tx.Rollback() 312 | } 313 | video.FavoriteCount++ 314 | models.UpdateVideo(tx, video) 315 | tx.Commit() 316 | 317 | userIdStr := strconv.FormatInt(msg.UserId, 10) 318 | videoIdStr := strconv.FormatInt(msg.VideoId, 10) 319 | likeSetKey := config.LikeKey + userIdStr 320 | exists, _ := utils.GetRedisDB().Exists(context.Background(), likeSetKey).Result() 321 | if exists == 0 { //缓存里面没有 322 | errBuildRedis := BuildLikeRedis(msg.UserId) 323 | if errBuildRedis != nil { 324 | log.Println("重建缓存失败", errBuildRedis) 325 | } 326 | } else { 327 | utils.GetRedisDB().SAdd(context.Background(), likeSetKey, videoIdStr) 328 | } 329 | } 330 | 331 | if msg.ActionType == 2 { 332 | like := models.Like{ 333 | CommonEntity: utils.NewCommonEntity(), 334 | UserId: msg.UserId, 335 | VideoId: msg.VideoId, 336 | } 337 | findLike, err := like.FindByUserIdAndVedioId() 338 | if err != nil { 339 | tx.Rollback() 340 | } 341 | err1 := findLike.Delete(tx) 342 | if err1 != nil { 343 | log.Printf(err1.Error()) 344 | tx.Rollback() 345 | } 346 | video, err2 := models.GetVideoById(msg.VideoId) 347 | if err2 != nil { 348 | log.Printf(err2.Error()) 349 | tx.Rollback() 350 | } 351 | //TODO 防止减到负数 352 | video.FavoriteCount-- 353 | models.UpdateVideo(tx, video) 354 | tx.Commit() 355 | 356 | userIdStr := strconv.FormatInt(msg.UserId, 10) 357 | videoIdStr := strconv.FormatInt(msg.VideoId, 10) 358 | likeSetKey := config.LikeKey + userIdStr 359 | // 删除缓存 SET 中的ID , 避免脏数据产生 360 | utils.GetRedisDB().SRem(context.Background(), likeSetKey, videoIdStr) 361 | } 362 | default: 363 | // 如果channel为空,暂停一段时间后重新监听 364 | time.Sleep(time.Millisecond * 1) 365 | } 366 | } 367 | } 368 | 369 | // 重建缓存 370 | func BuildLikeRedis(userId int64) error { 371 | like := models.Like{} 372 | idSet, err := like.GetLikeVedioIdList(userId) 373 | if err != nil { 374 | return err 375 | } 376 | userIdStr := strconv.FormatInt(userId, 10) 377 | likeSetKey := config.LikeKey + userIdStr 378 | var strValues []string 379 | for i := range idSet { 380 | strValues = append(strValues, strconv.FormatInt(idSet[i], 10)) 381 | } 382 | 383 | ctx := context.Background() 384 | err = utils.GetRedisDB().SAdd(ctx, likeSetKey, strValues).Err() 385 | errSetTime := utils.GetRedisDB().Expire(ctx, likeSetKey, time.Duration(config.LikeKeyTTL)*time.Second).Err() 386 | if errSetTime != nil { 387 | log.Println("redis 时间设置失败", errSetTime.Error()) 388 | } 389 | return err 390 | } 391 | 392 | // 创建消费者协程 393 | func MakeLikeGroutine() { 394 | numConsumers := 20 395 | for i := 0; i < numConsumers; i++ { 396 | go LikeConsumer(mq.LikeChannel) 397 | } 398 | } 399 | -------------------------------------------------------------------------------- /sql/20230801v1douyin.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Navicat Premium Data Transfer 3 | 4 | Source Server : mydp 5 | Source Server Type : MySQL 6 | Source Server Version : 50714 (5.7.14) 7 | Source Host : localhost:3306 8 | Source Schema : douyin 9 | 10 | Target Server Type : MySQL 11 | Target Server Version : 50714 (5.7.14) 12 | File Encoding : 65001 13 | 14 | Date: 28/07/2023 17:30:49 15 | */ 16 | USE douyin; 17 | SET NAMES utf8mb4; 18 | SET FOREIGN_KEY_CHECKS = 0; 19 | 20 | -- ---------------------------- 21 | -- Table structure for comment 22 | -- ---------------------------- 23 | DROP TABLE IF EXISTS `comment`; 24 | CREATE TABLE `comment` 25 | ( 26 | `id` bigint(64) NOT NULL, 27 | `user_id` bigint(64) NOT NULL COMMENT '评论用户的id', 28 | `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '评论内容', 29 | `video_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '评论的视频id', 30 | `create_date` timestamp NULL DEFAULT CURRENT_TIMESTAMP, 31 | `is_deleted` int(1) NULL DEFAULT NULL, 32 | PRIMARY KEY (`id`) USING BTREE 33 | ) ENGINE = InnoDB 34 | CHARACTER SET = utf8mb4 35 | COLLATE = utf8mb4_general_ci 36 | ROW_FORMAT = DYNAMIC; 37 | 38 | -- ---------------------------- 39 | -- Records of comment 40 | -- ---------------------------- 41 | 42 | -- ---------------------------- 43 | -- Table structure for follow 44 | -- ---------------------------- 45 | DROP TABLE IF EXISTS `follow`; 46 | CREATE TABLE `follow` 47 | ( 48 | `id` bigint(64) NOT NULL AUTO_INCREMENT, 49 | `user_id` bigint(64) NULL DEFAULT NULL COMMENT '用户id', 50 | `follow_user_id` bigint(64) NULL DEFAULT NULL COMMENT '关注的用户id', 51 | `create_date` timestamp NULL DEFAULT CURRENT_TIMESTAMP, 52 | `is_deleted` int(1) NULL DEFAULT 0, 53 | PRIMARY KEY (`id`) USING BTREE 54 | ) ENGINE = InnoDB 55 | CHARACTER SET = utf8mb4 56 | COLLATE = utf8mb4_general_ci 57 | ROW_FORMAT = DYNAMIC; 58 | 59 | -- ---------------------------- 60 | -- Records of follow 61 | -- ---------------------------- 62 | 63 | -- ---------------------------- 64 | -- Table structure for like 65 | -- ---------------------------- 66 | DROP TABLE IF EXISTS `like`; 67 | CREATE TABLE `like` 68 | ( 69 | `id` bigint(64) NOT NULL, 70 | `video_id` bigint(64) NULL DEFAULT NULL, 71 | `user_id` bigint(64) NULL DEFAULT NULL, 72 | `create_date` timestamp NULL DEFAULT CURRENT_TIMESTAMP, 73 | `is_deleted` int(1) NULL DEFAULT NULL, 74 | PRIMARY KEY (`id`) USING BTREE 75 | ) ENGINE = InnoDB 76 | CHARACTER SET = utf8mb4 77 | COLLATE = utf8mb4_general_ci 78 | ROW_FORMAT = DYNAMIC; 79 | 80 | -- ---------------------------- 81 | -- Records of like 82 | -- ---------------------------- 83 | 84 | -- ---------------------------- 85 | -- Table structure for message 86 | -- ---------------------------- 87 | DROP TABLE IF EXISTS `message`; 88 | CREATE TABLE `message` 89 | ( 90 | `id` bigint(64) NOT NULL, 91 | `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '消息内容', 92 | `create_date` timestamp NULL DEFAULT CURRENT_TIMESTAMP, 93 | `is_deleted` int(1) NULL DEFAULT NULL, 94 | PRIMARY KEY (`id`) USING BTREE 95 | ) ENGINE = InnoDB 96 | CHARACTER SET = utf8mb4 97 | COLLATE = utf8mb4_general_ci 98 | ROW_FORMAT = DYNAMIC; 99 | 100 | -- ---------------------------- 101 | -- Records of message 102 | -- ---------------------------- 103 | 104 | -- ---------------------------- 105 | -- Table structure for message_push_event 106 | -- ---------------------------- 107 | DROP TABLE IF EXISTS `message_push_event`; 108 | CREATE TABLE `message_push_event` 109 | ( 110 | `id` bigint(64) NOT NULL, 111 | `from_user_id` bigint(64) NULL DEFAULT NULL COMMENT '发送者的id', 112 | `msg_content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '消息内容', 113 | `create_date` timestamp NULL DEFAULT CURRENT_TIMESTAMP, 114 | `is_deleted` int(1) NULL DEFAULT NULL, 115 | PRIMARY KEY (`id`) USING BTREE 116 | ) ENGINE = InnoDB 117 | CHARACTER SET = utf8mb4 118 | COLLATE = utf8mb4_general_ci 119 | ROW_FORMAT = DYNAMIC; 120 | 121 | -- ---------------------------- 122 | -- Records of message_push_event 123 | -- ---------------------------- 124 | 125 | -- ---------------------------- 126 | -- Table structure for message_send_event 127 | -- ---------------------------- 128 | DROP TABLE IF EXISTS `message_send_event`; 129 | CREATE TABLE `message_send_event` 130 | ( 131 | `id` bigint(64) NOT NULL, 132 | `user_id` bigint(64) NOT NULL, 133 | `to_user_id` bigint(64) NOT NULL, 134 | `msg_content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, 135 | `create_date` timestamp NULL DEFAULT CURRENT_TIMESTAMP, 136 | `is_deleted` int(1) NULL DEFAULT NULL, 137 | PRIMARY KEY (`id`) USING BTREE 138 | ) ENGINE = InnoDB 139 | CHARACTER SET = utf8mb4 140 | COLLATE = utf8mb4_general_ci 141 | ROW_FORMAT = DYNAMIC; 142 | 143 | -- ---------------------------- 144 | -- Records of message_send_event 145 | -- ---------------------------- 146 | 147 | -- ---------------------------- 148 | -- Table structure for user 149 | -- ---------------------------- 150 | DROP TABLE IF EXISTS `user`; 151 | CREATE TABLE `user` 152 | ( 153 | `id` bigint(64) NOT NULL COMMENT '用户id', 154 | `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '姓名', 155 | `follow_count` int(8) NULL DEFAULT NULL COMMENT '关注数', 156 | `follower_count` int(8) NULL DEFAULT NULL COMMENT '粉丝数', 157 | `phone` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '电话', 158 | `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '密码', 159 | `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '头像', 160 | `gender` int(2) NULL DEFAULT NULL COMMENT '性别', 161 | `age` int(2) NULL DEFAULT NULL COMMENT '年龄', 162 | `create_date` timestamp NULL DEFAULT CURRENT_TIMESTAMP, 163 | `is_deleted` int(1) NULL DEFAULT NULL, 164 | `nickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '昵称', 165 | `signature` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '个人简介', 166 | `total_favorited` int(22) NULL DEFAULT NULL COMMENT '获赞数量', 167 | `work_count` int(22) NULL DEFAULT NULL COMMENT '作品数', 168 | `favorite_count` int(22) NULL DEFAULT NULL COMMENT '喜欢数', 169 | `is_follow` int(11) NULL DEFAULT NULL COMMENT '是否关注', 170 | `background_image` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '个人背景图片', 171 | PRIMARY KEY (`id`) USING BTREE 172 | ) ENGINE = InnoDB 173 | CHARACTER SET = utf8mb4 174 | COLLATE = utf8mb4_general_ci 175 | ROW_FORMAT = DYNAMIC; 176 | 177 | -- ---------------------------- 178 | -- Records of user 179 | -- ---------------------------- 180 | INSERT INTO `user` 181 | VALUES (7089783222816474111, '张四', 1, 1, '1', '1', '1', 1, 1, '2023-07-27 20:41:55', 0, '1', '0', 0, 0, 0, 1, NULL); 182 | INSERT INTO `user` 183 | VALUES (7090306410939941888, '20202231014@163.com', 0, 0, '', 184 | '$2a$10$t7RCzWVc1A/ReQPi8awWsu0MnnhAdwBTLzCsW1CWaHw1TU/64XIkG', '', 0, 0, '2023-07-27 20:26:20', 0, '', '', 0, 185 | 0, 0, 0, ''); 186 | 187 | -- ---------------------------- 188 | -- Table structure for video 189 | -- ---------------------------- 190 | DROP TABLE IF EXISTS `video`; 191 | CREATE TABLE `video` 192 | ( 193 | `id` bigint(64) NOT NULL, 194 | `author_id` bigint(64) NOT NULL COMMENT '视频作者', 195 | `play_url` varchar(2048) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '播放路径', 196 | `cover_url` varchar(2048) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, 197 | `favorite_count` int(8) NULL DEFAULT NULL COMMENT '喜欢数量', 198 | `comment_count` int(8) NULL DEFAULT NULL COMMENT '评论数量', 199 | `is_favorite` int(2) NULL DEFAULT NULL, 200 | `create_date` timestamp NULL DEFAULT CURRENT_TIMESTAMP, 201 | `is_deleted` int(1) NULL DEFAULT NULL, 202 | `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '视频标题', 203 | PRIMARY KEY (`id`) USING BTREE 204 | ) ENGINE = InnoDB 205 | CHARACTER SET = utf8mb4 206 | COLLATE = utf8mb4_general_ci 207 | ROW_FORMAT = DYNAMIC; 208 | 209 | -- ---------------------------- 210 | -- Records of video 211 | -- ---------------------------- 212 | INSERT INTO `video` 213 | VALUES (7089783222816474111, 7089783222816474111, 'https://www.w3schools.com/html/movie.mp4', 214 | 'https://cdn.pixabay.com/photo/2016/03/27/18/10/bear-1283347_1280.jpg', 0, 0, 0, '2023-07-28 12:33:04', 0, 215 | '台风'); 216 | INSERT INTO `video` 217 | VALUES (7089783222816474112, 7089783222816474111, 'https://cccimg.com/view.php/686384315ac21f0f67170063e07b1f75.mp4', 218 | 'https://img1.imgtp.com/2023/07/28/PGnC0crf.png', 0, 0, 0, '2023-07-28 10:36:42', 0, '熊'); 219 | 220 | -- ---------------------------- 221 | -- Procedure structure for addFollowRelation 222 | -- ---------------------------- 223 | DROP PROCEDURE IF EXISTS `addFollowRelation`; 224 | delimiter ;; 225 | CREATE PROCEDURE `addFollowRelation`(IN user_id bigint, IN follower_id bigint) 226 | BEGIN 227 | #Routine body goes here... 228 | # 声明记录个数变量。 229 | DECLARE cnt INT DEFAULT 0; 230 | # 获取记录个数变量。 231 | SELECT COUNT(1) FROM follow f where f.user_id = user_id AND f.follow_user_id = follower_id INTO cnt; 232 | # 判断是否已经存在该记录,并做出相应的插入关系、更新关系动作。 233 | # 插入操作。 234 | IF cnt = 0 THEN 235 | INSERT INTO follow(`user_id`, `follow_user_id`) VALUES (user_id, follower_id); 236 | END IF; 237 | # 更新操作 238 | IF cnt != 0 THEN 239 | UPDATE follow f SET f.is_deleted = 0 WHERE f.user_id = user_id AND f.follow_user_id = follower_id; 240 | END IF; 241 | END 242 | ;; 243 | delimiter ; 244 | 245 | -- ---------------------------- 246 | -- Procedure structure for delFollowRelation 247 | -- ---------------------------- 248 | DROP PROCEDURE IF EXISTS `delFollowRelation`; 249 | delimiter ;; 250 | CREATE PROCEDURE `delFollowRelation`(IN `user_id` bigint, IN `follower_id` bigint) 251 | BEGIN 252 | #Routine body goes here... 253 | # 定义记录个数变量,记录是否存在此关系,默认没有关系。 254 | DECLARE cnt INT DEFAULT 0; 255 | # 查看是否之前有关系。 256 | SELECT COUNT(1) FROM follow f WHERE f.user_id = user_id AND f.follow_user_id = follower_id INTO cnt; 257 | # 有关系,则需要update cancel = 1,使其关系无效。 258 | IF cnt = 1 THEN 259 | UPDATE follow f SET f.is_deleted = 1 WHERE f.user_id = user_id AND f.follow_user_id = follower_id; 260 | END IF; 261 | END 262 | ;; 263 | delimiter ; 264 | 265 | SET FOREIGN_KEY_CHECKS = 1; 266 | -------------------------------------------------------------------------------- /sql/20230731v3douyin.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Navicat Premium Data Transfer 3 | 4 | Source Server : mydp 5 | Source Server Type : MySQL 6 | Source Server Version : 50714 (5.7.14) 7 | Source Host : localhost:3306 8 | Source Schema : douyin 9 | 10 | Target Server Type : MySQL 11 | Target Server Version : 50714 (5.7.14) 12 | File Encoding : 65001 13 | 14 | Date: 28/07/2023 17:30:49 15 | */ 16 | 17 | SET NAMES utf8mb4; 18 | SET FOREIGN_KEY_CHECKS = 0; 19 | 20 | -- ---------------------------- 21 | -- Table structure for comment 22 | -- ---------------------------- 23 | DROP TABLE IF EXISTS `comment`; 24 | CREATE TABLE `comment` 25 | ( 26 | `id` bigint(64) NOT NULL, 27 | `user_id` bigint(64) NOT NULL COMMENT '评论用户的id', 28 | `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '评论内容', 29 | `video_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '评论的视频id', 30 | `create_date` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, 31 | `is_deleted` int(1) NULL DEFAULT NULL, 32 | PRIMARY KEY (`id`) USING BTREE 33 | ) ENGINE = InnoDB 34 | CHARACTER SET = utf8mb4 35 | COLLATE = utf8mb4_general_ci 36 | ROW_FORMAT = DYNAMIC; 37 | 38 | -- ---------------------------- 39 | -- Records of comment 40 | -- ---------------------------- 41 | 42 | -- ---------------------------- 43 | -- Table structure for follow 44 | -- ---------------------------- 45 | DROP TABLE IF EXISTS `follow`; 46 | CREATE TABLE `follow` 47 | ( 48 | `id` bigint(64) NOT NULL AUTO_INCREMENT, 49 | `user_id` bigint(64) NULL DEFAULT NULL COMMENT '用户id', 50 | `follow_user_id` bigint(64) NULL DEFAULT NULL COMMENT '关注的用户id', 51 | `create_date` timestamp NULL DEFAULT CURRENT_TIMESTAMP, 52 | `is_deleted` int(1) NULL DEFAULT 0, 53 | PRIMARY KEY (`id`) USING BTREE 54 | ) ENGINE = InnoDB 55 | CHARACTER SET = utf8mb4 56 | COLLATE = utf8mb4_general_ci 57 | ROW_FORMAT = DYNAMIC; 58 | 59 | -- ---------------------------- 60 | -- Records of follow 61 | -- ---------------------------- 62 | 63 | -- ---------------------------- 64 | -- Table structure for like 65 | -- ---------------------------- 66 | DROP TABLE IF EXISTS `like`; 67 | CREATE TABLE `like` 68 | ( 69 | `id` bigint(64) NOT NULL, 70 | `video_id` bigint(64) NULL DEFAULT NULL, 71 | `user_id` bigint(64) NULL DEFAULT NULL, 72 | `create_date` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, 73 | `is_deleted` int(1) NULL DEFAULT NULL, 74 | PRIMARY KEY (`id`) USING BTREE 75 | ) ENGINE = InnoDB 76 | CHARACTER SET = utf8mb4 77 | COLLATE = utf8mb4_general_ci 78 | ROW_FORMAT = DYNAMIC; 79 | 80 | -- ---------------------------- 81 | -- Records of like 82 | -- ---------------------------- 83 | 84 | -- ---------------------------- 85 | -- Table structure for message 86 | -- ---------------------------- 87 | DROP TABLE IF EXISTS `message`; 88 | CREATE TABLE `message` 89 | ( 90 | `id` bigint(64) NOT NULL, 91 | `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '消息内容', 92 | `create_date` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, 93 | `is_deleted` int(1) NULL DEFAULT NULL, 94 | PRIMARY KEY (`id`) USING BTREE 95 | ) ENGINE = InnoDB 96 | CHARACTER SET = utf8mb4 97 | COLLATE = utf8mb4_general_ci 98 | ROW_FORMAT = DYNAMIC; 99 | 100 | -- ---------------------------- 101 | -- Records of message 102 | -- ---------------------------- 103 | 104 | -- ---------------------------- 105 | -- Table structure for message_push_event 106 | -- ---------------------------- 107 | DROP TABLE IF EXISTS `message_push_event`; 108 | CREATE TABLE `message_push_event` 109 | ( 110 | `id` bigint(64) NOT NULL, 111 | `from_user_id` bigint(64) NULL DEFAULT NULL COMMENT '发送者的id', 112 | `msg_content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '消息内容', 113 | `create_date` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, 114 | `is_deleted` int(1) NULL DEFAULT NULL, 115 | PRIMARY KEY (`id`) USING BTREE 116 | ) ENGINE = InnoDB 117 | CHARACTER SET = utf8mb4 118 | COLLATE = utf8mb4_general_ci 119 | ROW_FORMAT = DYNAMIC; 120 | 121 | -- ---------------------------- 122 | -- Records of message_push_event 123 | -- ---------------------------- 124 | 125 | -- ---------------------------- 126 | -- Table structure for message_send_event 127 | -- ---------------------------- 128 | DROP TABLE IF EXISTS `message_send_event`; 129 | CREATE TABLE `message_send_event` 130 | ( 131 | `id` bigint(64) NOT NULL, 132 | `user_id` bigint(64) NOT NULL, 133 | `to_user_id` bigint(64) NOT NULL, 134 | `msg_content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, 135 | `create_date` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, 136 | `is_deleted` int(1) NULL DEFAULT NULL, 137 | PRIMARY KEY (`id`) USING BTREE 138 | ) ENGINE = InnoDB 139 | CHARACTER SET = utf8mb4 140 | COLLATE = utf8mb4_general_ci 141 | ROW_FORMAT = DYNAMIC; 142 | 143 | -- ---------------------------- 144 | -- Records of message_send_event 145 | -- ---------------------------- 146 | 147 | -- ---------------------------- 148 | -- Table structure for user 149 | -- ---------------------------- 150 | DROP TABLE IF EXISTS `user`; 151 | CREATE TABLE `user` 152 | ( 153 | `id` bigint(64) NOT NULL COMMENT '用户id', 154 | `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '姓名', 155 | `follow_count` int(8) NULL DEFAULT NULL COMMENT '关注数', 156 | `follower_count` int(8) NULL DEFAULT NULL COMMENT '粉丝数', 157 | `phone` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '电话', 158 | `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '密码', 159 | `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '头像', 160 | `gender` int(2) NULL DEFAULT NULL COMMENT '性别', 161 | `age` int(2) NULL DEFAULT NULL COMMENT '年龄', 162 | `create_date` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, 163 | `is_deleted` int(1) NULL DEFAULT NULL, 164 | `nickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '昵称', 165 | `signature` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '个人简介', 166 | `total_favorited` int(22) NULL DEFAULT NULL COMMENT '获赞数量', 167 | `work_count` int(22) NULL DEFAULT NULL COMMENT '作品数', 168 | `favorite_count` int(22) NULL DEFAULT NULL COMMENT '喜欢数', 169 | `is_follow` int(11) NULL DEFAULT NULL COMMENT '是否关注', 170 | `background_image` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '个人背景图片', 171 | PRIMARY KEY (`id`) USING BTREE 172 | ) ENGINE = InnoDB 173 | CHARACTER SET = utf8mb4 174 | COLLATE = utf8mb4_general_ci 175 | ROW_FORMAT = DYNAMIC; 176 | 177 | -- ---------------------------- 178 | -- Records of user 179 | -- ---------------------------- 180 | INSERT INTO `user` 181 | VALUES (7089783222816474111, '张四', 1, 1, '1', '1', '1', 1, 1, '2023-07-27 20:41:55', 0, '1', '0', 0, 0, 0, 1, NULL); 182 | INSERT INTO `user` 183 | VALUES (7090306410939941888, '20202231014@163.com', 0, 0, '', 184 | '$2a$10$t7RCzWVc1A/ReQPi8awWsu0MnnhAdwBTLzCsW1CWaHw1TU/64XIkG', '', 0, 0, '2023-07-27 20:26:20', 0, '', '', 0, 185 | 0, 0, 0, ''); 186 | 187 | -- ---------------------------- 188 | -- Table structure for video 189 | -- ---------------------------- 190 | DROP TABLE IF EXISTS `video`; 191 | CREATE TABLE `video` 192 | ( 193 | `id` bigint(64) NOT NULL, 194 | `author_id` bigint(64) NOT NULL COMMENT '视频作者', 195 | `play_url` varchar(2048) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '播放路径', 196 | `cover_url` varchar(2048) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, 197 | `favorite_count` int(8) NULL DEFAULT NULL COMMENT '喜欢数量', 198 | `comment_count` int(8) NULL DEFAULT NULL COMMENT '评论数量', 199 | `is_favorite` int(2) NULL DEFAULT NULL, 200 | `create_date` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, 201 | `is_deleted` int(1) NULL DEFAULT NULL, 202 | `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '视频标题', 203 | PRIMARY KEY (`id`) USING BTREE 204 | ) ENGINE = InnoDB 205 | CHARACTER SET = utf8mb4 206 | COLLATE = utf8mb4_general_ci 207 | ROW_FORMAT = DYNAMIC; 208 | 209 | -- ---------------------------- 210 | -- Records of video 211 | -- ---------------------------- 212 | INSERT INTO `video` 213 | VALUES (7089783222816474111, 7089783222816474111, 'https://www.w3schools.com/html/movie.mp4', 214 | 'https://cdn.pixabay.com/photo/2016/03/27/18/10/bear-1283347_1280.jpg', 0, 0, 0, '2023-07-28 12:33:04', 0, 215 | '台风'); 216 | INSERT INTO `video` 217 | VALUES (7089783222816474112, 7089783222816474111, 'https://cccimg.com/view.php/686384315ac21f0f67170063e07b1f75.mp4', 218 | 'https://img1.imgtp.com/2023/07/28/PGnC0crf.png', 0, 0, 0, '2023-07-28 10:36:42', 0, '熊'); 219 | 220 | -- ---------------------------- 221 | -- Procedure structure for addFollowRelation 222 | -- ---------------------------- 223 | DROP PROCEDURE IF EXISTS `addFollowRelation`; 224 | delimiter ;; 225 | CREATE PROCEDURE `addFollowRelation`(IN user_id bigint, IN follower_id bigint) 226 | BEGIN 227 | #Routine body goes here... 228 | # 声明记录个数变量。 229 | DECLARE cnt INT DEFAULT 0; 230 | # 获取记录个数变量。 231 | SELECT COUNT(1) FROM follow f where f.user_id = user_id AND f.follow_user_id = follower_id INTO cnt; 232 | # 判断是否已经存在该记录,并做出相应的插入关系、更新关系动作。 233 | # 插入操作。 234 | IF cnt = 0 THEN 235 | INSERT INTO follow(`user_id`, `follow_user_id`) VALUES (user_id, follower_id); 236 | END IF; 237 | # 更新操作 238 | IF cnt != 0 THEN 239 | UPDATE follow f SET f.is_deleted = 0 WHERE f.user_id = user_id AND f.follow_user_id = follower_id; 240 | END IF; 241 | END 242 | ;; 243 | delimiter ; 244 | 245 | -- ---------------------------- 246 | -- Procedure structure for delFollowRelation 247 | -- ---------------------------- 248 | DROP PROCEDURE IF EXISTS `delFollowRelation`; 249 | delimiter ;; 250 | CREATE PROCEDURE `delFollowRelation`(IN `user_id` bigint, IN `follower_id` bigint) 251 | BEGIN 252 | #Routine body goes here... 253 | # 定义记录个数变量,记录是否存在此关系,默认没有关系。 254 | DECLARE cnt INT DEFAULT 0; 255 | # 查看是否之前有关系。 256 | SELECT COUNT(1) FROM follow f WHERE f.user_id = user_id AND f.follow_user_id = follower_id INTO cnt; 257 | # 有关系,则需要update cancel = 1,使其关系无效。 258 | IF cnt = 1 THEN 259 | UPDATE follow f SET f.is_deleted = 1 WHERE f.user_id = user_id AND f.follow_user_id = follower_id; 260 | END IF; 261 | END 262 | ;; 263 | delimiter ; 264 | 265 | SET FOREIGN_KEY_CHECKS = 1; 266 | -------------------------------------------------------------------------------- /sql/douyin-with-test-data.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Navicat Premium Data Transfer 3 | 4 | Source Server : mydp 5 | Source Server Type : MySQL 6 | Source Server Version : 50714 (5.7.14) 7 | Source Host : localhost:3306 8 | Source Schema : douyin 9 | 10 | Target Server Type : MySQL 11 | Target Server Version : 50714 (5.7.14) 12 | File Encoding : 65001 13 | 14 | Date: 02/08/2023 15:15:55 15 | */ 16 | 17 | SET NAMES utf8mb4; 18 | SET FOREIGN_KEY_CHECKS = 0; 19 | 20 | -- ---------------------------- 21 | -- Table structure for comment 22 | -- ---------------------------- 23 | DROP TABLE IF EXISTS `comment`; 24 | CREATE TABLE `comment` ( 25 | `id` bigint(64) NOT NULL, 26 | `user_id` bigint(64) NOT NULL COMMENT '评论用户的id', 27 | `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '评论内容', 28 | `video_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '评论的视频id', 29 | `create_date` timestamp NULL DEFAULT CURRENT_TIMESTAMP, 30 | `is_deleted` int(1) NULL DEFAULT NULL, 31 | PRIMARY KEY (`id`) USING BTREE 32 | ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; 33 | 34 | -- ---------------------------- 35 | -- Records of comment 36 | -- ---------------------------- 37 | INSERT INTO `comment` VALUES (7092133932308628480, 7090306410939941888, '好!', '7089783222816474111', '2023-08-01 13:28:15', 0); 38 | INSERT INTO `comment` VALUES (7092327335134757888, 7090306410939941888, '1', '7089783222816474111', '2023-08-02 02:16:46', 0); 39 | INSERT INTO `comment` VALUES (7092328826272744448, 7090306410939941888, '好', '7089783222816474112', '2023-08-02 02:22:41', 0); 40 | INSERT INTO `comment` VALUES (7092328901388534784, 7090306410939941888, '1', '7092317635416687616', '2023-08-02 02:22:59', 0); 41 | INSERT INTO `comment` VALUES (7092359091284083712, 7092343857802642432, '33333', '7092317635416687616', '2023-08-02 04:22:57', 0); 42 | INSERT INTO `comment` VALUES (7092359146736976896, 7092343857802642432, '胡狠狠', '7092164459275224064', '2023-08-02 04:23:10', 0); 43 | INSERT INTO `comment` VALUES (7092364172008096768, 7092343857802642432, '好啊', '7092363100510225408', '2023-08-02 04:43:08', 0); 44 | 45 | -- ---------------------------- 46 | -- Table structure for follow 47 | -- ---------------------------- 48 | DROP TABLE IF EXISTS `follow`; 49 | CREATE TABLE `follow` ( 50 | `id` bigint(64) NOT NULL AUTO_INCREMENT, 51 | `user_id` bigint(64) NULL DEFAULT NULL COMMENT '用户id', 52 | `follow_user_id` bigint(64) NULL DEFAULT NULL COMMENT '关注的用户id', 53 | `create_date` timestamp NULL DEFAULT CURRENT_TIMESTAMP, 54 | `is_deleted` int(1) NULL DEFAULT 0, 55 | PRIMARY KEY (`id`) USING BTREE 56 | ) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; 57 | 58 | -- ---------------------------- 59 | -- Records of follow 60 | -- ---------------------------- 61 | INSERT INTO `follow` VALUES (1, 7092343857802642432, 7090306410939941888, '2023-08-02 12:44:03', 0); 62 | 63 | -- ---------------------------- 64 | -- Table structure for like 65 | -- ---------------------------- 66 | DROP TABLE IF EXISTS `like`; 67 | CREATE TABLE `like` ( 68 | `id` bigint(64) NOT NULL, 69 | `video_id` bigint(64) NULL DEFAULT NULL, 70 | `user_id` bigint(64) NULL DEFAULT NULL, 71 | `create_date` timestamp NULL DEFAULT CURRENT_TIMESTAMP, 72 | `is_deleted` int(1) NULL DEFAULT NULL, 73 | PRIMARY KEY (`id`) USING BTREE 74 | ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; 75 | 76 | -- ---------------------------- 77 | -- Records of like 78 | -- ---------------------------- 79 | INSERT INTO `like` VALUES (7092133780235748352, 7089783222816474112, 7089783222816474112, '2023-08-01 13:27:39', 1); 80 | INSERT INTO `like` VALUES (7092133804411716608, 7089783222816474111, 7089783222816474112, '2023-08-01 13:27:44', 1); 81 | INSERT INTO `like` VALUES (7092133897869198336, 7089783222816474111, 7089783222816474112, '2023-08-01 13:28:07', 0); 82 | INSERT INTO `like` VALUES (7092359070677468160, 7092317635416687616, 7092343857802642432, '2023-08-02 04:22:52', 1); 83 | INSERT INTO `like` VALUES (7092359113274819584, 7092314280921400320, 7092343857802642432, '2023-08-02 04:23:02', 1); 84 | INSERT INTO `like` VALUES (7092359121692787712, 7092164459275224064, 7092343857802642432, '2023-08-02 04:23:04', 1); 85 | INSERT INTO `like` VALUES (7092361109281178624, 7092317635416687616, 7092343857802642432, '2023-08-02 04:30:58', 1); 86 | INSERT INTO `like` VALUES (7092364328921203712, 7092363100510225408, 7092343857802642432, '2023-08-02 04:43:46', 1); 87 | INSERT INTO `like` VALUES (7092364335476900864, 7092363100510225408, 7092343857802642432, '2023-08-02 04:43:47', 1); 88 | INSERT INTO `like` VALUES (7092364599332176896, 7092317635416687616, 7092343857802642432, '2023-08-02 04:44:50', 0); 89 | INSERT INTO `like` VALUES (7092400079155233792, 7092317635416687616, 7090306410939941888, '2023-08-02 07:05:49', 0); 90 | 91 | -- ---------------------------- 92 | -- Table structure for message 93 | -- ---------------------------- 94 | DROP TABLE IF EXISTS `message`; 95 | CREATE TABLE `message` ( 96 | `id` bigint(64) NOT NULL, 97 | `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '消息内容', 98 | `create_date` timestamp NULL DEFAULT CURRENT_TIMESTAMP, 99 | `is_deleted` int(1) NULL DEFAULT NULL, 100 | PRIMARY KEY (`id`) USING BTREE 101 | ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; 102 | 103 | -- ---------------------------- 104 | -- Records of message 105 | -- ---------------------------- 106 | 107 | -- ---------------------------- 108 | -- Table structure for message_push_event 109 | -- ---------------------------- 110 | DROP TABLE IF EXISTS `message_push_event`; 111 | CREATE TABLE `message_push_event` ( 112 | `id` bigint(64) NOT NULL, 113 | `from_user_id` bigint(64) NULL DEFAULT NULL COMMENT '发送者的id', 114 | `msg_content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '消息内容', 115 | `create_date` timestamp NULL DEFAULT CURRENT_TIMESTAMP, 116 | `is_deleted` int(1) NULL DEFAULT NULL, 117 | PRIMARY KEY (`id`) USING BTREE 118 | ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; 119 | 120 | -- ---------------------------- 121 | -- Records of message_push_event 122 | -- ---------------------------- 123 | 124 | -- ---------------------------- 125 | -- Table structure for message_send_event 126 | -- ---------------------------- 127 | DROP TABLE IF EXISTS `message_send_event`; 128 | CREATE TABLE `message_send_event` ( 129 | `id` bigint(64) NOT NULL, 130 | `user_id` bigint(64) NOT NULL, 131 | `to_user_id` bigint(64) NOT NULL, 132 | `msg_content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, 133 | `create_date` timestamp NULL DEFAULT CURRENT_TIMESTAMP, 134 | `is_deleted` int(1) NULL DEFAULT NULL, 135 | PRIMARY KEY (`id`) USING BTREE 136 | ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; 137 | 138 | -- ---------------------------- 139 | -- Records of message_send_event 140 | -- ---------------------------- 141 | 142 | -- ---------------------------- 143 | -- Table structure for user 144 | -- ---------------------------- 145 | DROP TABLE IF EXISTS `user`; 146 | CREATE TABLE `user` ( 147 | `id` bigint(64) NOT NULL COMMENT '用户id', 148 | `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '姓名', 149 | `follow_count` int(8) NULL DEFAULT NULL COMMENT '关注数', 150 | `follower_count` int(8) NULL DEFAULT NULL COMMENT '粉丝数', 151 | `phone` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '电话', 152 | `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '密码', 153 | `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '头像', 154 | `gender` int(2) NULL DEFAULT NULL COMMENT '性别', 155 | `age` int(2) NULL DEFAULT NULL COMMENT '年龄', 156 | `create_date` timestamp NULL DEFAULT CURRENT_TIMESTAMP, 157 | `is_deleted` int(1) NULL DEFAULT NULL, 158 | `nickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '昵称', 159 | `signature` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '个人简介', 160 | `total_favorited` int(22) NULL DEFAULT NULL COMMENT '获赞数量', 161 | `work_count` int(22) NULL DEFAULT NULL COMMENT '作品数', 162 | `favorite_count` int(22) NULL DEFAULT NULL COMMENT '喜欢数', 163 | `is_follow` int(11) NULL DEFAULT NULL COMMENT '是否关注', 164 | `background_image` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '个人背景图片', 165 | PRIMARY KEY (`id`) USING BTREE 166 | ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; 167 | 168 | -- ---------------------------- 169 | -- Records of user 170 | -- ---------------------------- 171 | INSERT INTO `user` VALUES (7089783222816474111, '张四', 1, 1, '1', '1', '1', 1, 1, '2023-07-27 20:41:55', 0, '1', '这是个人简介', 0, 0, 0, 1, NULL); 172 | INSERT INTO `user` VALUES (7090306410939941888, '20202231014@163.com', 0, 0, '', '$2a$10$t7RCzWVc1A/ReQPi8awWsu0MnnhAdwBTLzCsW1CWaHw1TU/64XIkG', 'http://39.101.74.182:80/photos/dog1.jpg', 0, 0, '2023-07-27 20:26:20', 0, '', '这是个人简介', 0, 2, 0, 0, 'http://39.101.74.182:80/photos/background1.jpg'); 173 | INSERT INTO `user` VALUES (7092343857802642432, '20202231014@162.com', 0, 0, '', '$2a$10$kVTGM2HXbc4LJjIi.JfxQ.FfiJnqnshFCySP884XxxCr716098EP.', 'http://39.101.74.182:80/photos/people1.jpg', 0, 0, '2023-08-02 03:22:25', 0, '', '这是个人简介', 0, 0, 0, 0, 'http://39.101.74.182:80/photos/background2.jpg'); 174 | 175 | -- ---------------------------- 176 | -- Table structure for video 177 | -- ---------------------------- 178 | DROP TABLE IF EXISTS `video`; 179 | CREATE TABLE `video` ( 180 | `id` bigint(64) NOT NULL, 181 | `author_id` bigint(64) NOT NULL COMMENT '视频作者', 182 | `play_url` varchar(2048) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '播放路径', 183 | `cover_url` varchar(2048) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, 184 | `favorite_count` int(8) NULL DEFAULT NULL COMMENT '喜欢数量', 185 | `comment_count` int(8) NULL DEFAULT NULL COMMENT '评论数量', 186 | `is_favorite` int(2) NULL DEFAULT NULL, 187 | `create_date` timestamp NULL DEFAULT CURRENT_TIMESTAMP, 188 | `is_deleted` int(1) NULL DEFAULT NULL, 189 | `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '视频标题', 190 | PRIMARY KEY (`id`) USING BTREE 191 | ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; 192 | 193 | -- ---------------------------- 194 | -- Records of video 195 | -- ---------------------------- 196 | INSERT INTO `video` VALUES (7089783222816474111, 7089783222816474111, 'https://www.w3schools.com/html/movie.mp4', 'https://cdn.pixabay.com/photo/2016/03/27/18/10/bear-1283347_1280.jpg', 1, 2, 0, '2023-07-28 12:33:04', 0, '熊'); 197 | INSERT INTO `video` VALUES (7092317635416687616, 7090306410939941888, 'http://39.101.74.182:80/videos/taifeng.mp4', 'http://39.101.74.182:80/photos/cover7092317632895910912.jpg', 2, 2, 0, '2023-08-02 01:38:13', 0, '台风'); 198 | INSERT INTO `video` VALUES (7092363100510225408, 7090306410939941888, 'http://39.101.74.182:80/videos/cc187a2f2856d056e80c4a78874b26e5.mp4', 'http://39.101.74.182:80/photos/cover7092363097855230976.jpg', 0, 1, 0, '2023-08-02 04:38:53', 0, '合成高温超导体'); 199 | 200 | -- ---------------------------- 201 | -- Procedure structure for addFollowRelation 202 | -- ---------------------------- 203 | DROP PROCEDURE IF EXISTS `addFollowRelation`; 204 | delimiter ;; 205 | CREATE PROCEDURE `addFollowRelation`(IN user_id bigint, IN follower_id bigint) 206 | BEGIN 207 | #Routine body goes here... 208 | # 声明记录个数变量。 209 | DECLARE cnt INT DEFAULT 0; 210 | # 获取记录个数变量。 211 | SELECT COUNT(1) FROM follow f where f.user_id = user_id AND f.follow_user_id = follower_id INTO cnt; 212 | # 判断是否已经存在该记录,并做出相应的插入关系、更新关系动作。 213 | # 插入操作。 214 | IF cnt = 0 THEN 215 | INSERT INTO follow(`user_id`, `follow_user_id`) VALUES (user_id, follower_id); 216 | END IF; 217 | # 更新操作 218 | IF cnt != 0 THEN 219 | UPDATE follow f SET f.is_deleted = 0 WHERE f.user_id = user_id AND f.follow_user_id = follower_id; 220 | END IF; 221 | END 222 | ;; 223 | delimiter ; 224 | 225 | -- ---------------------------- 226 | -- Procedure structure for delFollowRelation 227 | -- ---------------------------- 228 | DROP PROCEDURE IF EXISTS `delFollowRelation`; 229 | delimiter ;; 230 | CREATE PROCEDURE `delFollowRelation`(IN `user_id` bigint, IN `follower_id` bigint) 231 | BEGIN 232 | #Routine body goes here... 233 | # 定义记录个数变量,记录是否存在此关系,默认没有关系。 234 | DECLARE cnt INT DEFAULT 0; 235 | # 查看是否之前有关系。 236 | SELECT COUNT(1) FROM follow f WHERE f.user_id = user_id AND f.follow_user_id = follower_id INTO cnt; 237 | # 有关系,则需要update cancel = 1,使其关系无效。 238 | IF cnt = 1 THEN 239 | UPDATE follow f SET f.is_deleted = 1 WHERE f.user_id = user_id AND f.follow_user_id = follower_id; 240 | END IF; 241 | END 242 | ;; 243 | delimiter ; 244 | 245 | SET FOREIGN_KEY_CHECKS = 1; 246 | -------------------------------------------------------------------------------- /service/impl/RelationServiceImpl.go: -------------------------------------------------------------------------------- 1 | package impl 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "strconv" 8 | "sync" 9 | "time" 10 | 11 | "github.com/RaymondCode/simple-demo/config" 12 | "github.com/RaymondCode/simple-demo/models" 13 | "github.com/RaymondCode/simple-demo/mq" 14 | "github.com/RaymondCode/simple-demo/utils" 15 | "github.com/sirupsen/logrus" 16 | 17 | "golang.org/x/net/context" 18 | "gopkg.in/errgo.v2/errors" 19 | ) 20 | 21 | type RelationServiceImpl struct { 22 | Logger *logrus.Logger 23 | } 24 | 25 | // FollowUser 关注用户 26 | func (relationServiceImpl RelationServiceImpl) FollowUser(userId int64, toUserId int64, actionType int) error { 27 | relationServiceImpl.Logger.Info("FollowUser\n") 28 | 29 | if userId == toUserId { 30 | return fmt.Errorf("你不能关注(或者取消关注)自己") 31 | } 32 | //分布式锁 不能让用户连续两次关注或者取消关注同一个用户的请求进入 33 | userIdStr := strconv.FormatInt(userId, 10) 34 | toUserIdStr := strconv.FormatInt(toUserId, 10) 35 | 36 | lockKey := config.FollowLock + userIdStr + toUserIdStr 37 | unFollowLockKey := config.UnFollowLock + userIdStr + toUserIdStr 38 | 39 | if actionType == 1 { 40 | isSuccess, _ := utils.GetRedisDB().SetNX(context.Background(), lockKey, "0", time.Duration(config.FollowLockTTL)*time.Second).Result() 41 | if isSuccess == false { 42 | log.Println("已关注") 43 | return errors.New("已关注") 44 | } else { 45 | utils.GetRedisDB().Del(context.Background(), unFollowLockKey) 46 | } 47 | } else { 48 | isSuccess, _ := utils.GetRedisDB().SetNX(context.Background(), unFollowLockKey, "0", time.Duration(config.FollowLockTTL)*time.Second).Result() 49 | if isSuccess == false { 50 | log.Println("已取消关注") 51 | return errors.New("已取消关注") 52 | } else { 53 | utils.GetRedisDB().Del(context.Background(), lockKey) 54 | } 55 | } 56 | 57 | var isExists bool = false 58 | var err error 59 | var follow *models.Follow 60 | 61 | userFollowKey := config.FollowKey + userIdStr 62 | //看看缓存中有没有这个集合 63 | exits, _ := utils.GetRedisDB().Exists(context.Background(), userFollowKey).Result() 64 | if exits != 0 { 65 | // 看看这个集合中有没有这个ID 66 | result, _ := utils.GetRedisDB().SIsMember(context.Background(), userFollowKey, userIdStr).Result() 67 | isExists = result 68 | // 如果缓存里面的 Set 里面没有就要从数据库里面查 69 | if !isExists { 70 | follow, err = getFollowByUserIdAndToUserId(userId, toUserId) 71 | if err != nil { 72 | log.Printf("查询关注记录发生异常 = %v", err) 73 | return err 74 | } 75 | if follow.Id != 0 { 76 | isExists = true 77 | } 78 | } 79 | } else { 80 | // 缓存中没有则从数据库找 81 | follow, err = getFollowByUserIdAndToUserId(userId, toUserId) 82 | if err != nil { 83 | log.Printf("查询关注记录发生异常 = %v", err) 84 | return err 85 | } 86 | 87 | if follow.Id != 0 { 88 | isExists = true 89 | } 90 | 91 | } 92 | 93 | if actionType == 1 { 94 | 95 | if isExists { 96 | log.Printf("该用户已关注") 97 | //tx.Rollback() 98 | err = errors.New("已关注") 99 | return err 100 | } 101 | 102 | //mqData := models.LikeMQToVideo{UserId: userId, VideoId: videoId, ActionType: actionType} 103 | mqData := models.FollowMQToUser{UserId: userId, FollowUserId: toUserId, ActionType: actionType} 104 | // 加入 channel 105 | mq.FollowChannel <- mqData 106 | jsonData, err := json.Marshal(mqData) 107 | if err != nil { 108 | log.Println("json序列化失败 = #{err}") 109 | //TODO 处理失败导致的数据不一致 110 | } 111 | //加入消息队列 112 | mq.FollowRMQ.Publish(string(jsonData)) 113 | 114 | return nil 115 | 116 | } else if actionType == 2 { 117 | 118 | if !isExists && (follow == nil || follow.Id == 0) { 119 | log.Printf("未找到要取消的点赞记录") 120 | err = errors.New("-2") 121 | //tx.Rollback() 122 | return err 123 | } 124 | 125 | //mqData := models.LikeMQToVideo{UserId: userId, VideoId: videoId, ActionType: actionType} 126 | mqData := models.FollowMQToUser{UserId: userId, FollowUserId: toUserId, ActionType: actionType} 127 | // 加入 channel 128 | mq.FollowChannel <- mqData 129 | jsonData, err := json.Marshal(mqData) 130 | if err != nil { 131 | log.Printf("json序列化失败 = #{err}") 132 | //TODO 处理失败导致的数据不一致 133 | } 134 | //加入消息队列 135 | mq.FollowRMQ.Publish(string(jsonData)) 136 | // TODO 消息队列处理失败会导致数据不一致 137 | 138 | return nil 139 | 140 | } 141 | return nil 142 | } 143 | 144 | func FollowConsumer(ch <-chan models.FollowMQToUser) { 145 | for { 146 | select { 147 | case msg := <-ch: 148 | // 在这里处理接收到的消息 149 | tx := utils.GetMysqlDB().Begin() 150 | if msg.ActionType == 1 { 151 | follow := models.Follow{ 152 | CommonEntity: utils.NewCommonEntity(), 153 | UserId: msg.UserId, 154 | FollowUserId: msg.FollowUserId, 155 | } 156 | err1 := follow.Insert(tx) 157 | if err1 != nil { 158 | log.Printf(err1.Error()) 159 | tx.Rollback() 160 | } 161 | tx.Commit() 162 | 163 | userIdStr := strconv.FormatInt(msg.UserId, 10) 164 | toUserIdStr := strconv.FormatInt(msg.FollowUserId, 10) 165 | followSetKey := config.FollowKey + userIdStr 166 | followerSetKey := config.FollowerKey + toUserIdStr 167 | exists, _ := utils.GetRedisDB().Exists(context.Background(), followSetKey).Result() 168 | if exists == 0 { //缓存里面没有 169 | errBuildRedis := BuildFollowRedis(msg.UserId) 170 | if errBuildRedis != nil { 171 | log.Println("重建缓存失败", errBuildRedis) 172 | } 173 | } else { 174 | utils.GetRedisDB().SAdd(context.Background(), followSetKey, toUserIdStr) 175 | } 176 | 177 | exists1, _ := utils.GetRedisDB().Exists(context.Background(), followerSetKey).Result() 178 | if exists1 == 0 { //缓存里面没有 179 | errBuildRedis := BuildFollowerRedis(msg.FollowUserId) 180 | if errBuildRedis != nil { 181 | log.Println("重建缓存失败", errBuildRedis) 182 | } 183 | } else { 184 | utils.GetRedisDB().SAdd(context.Background(), followerSetKey, userIdStr) 185 | } 186 | } 187 | 188 | if msg.ActionType == 2 { 189 | follow, err := getFollowByUserIdAndToUserId(msg.UserId, msg.FollowUserId) 190 | if err != nil { 191 | tx.Rollback() 192 | } 193 | err1 := follow.Delete(tx) 194 | if err1 != nil { 195 | log.Printf(err1.Error()) 196 | tx.Rollback() 197 | } 198 | tx.Commit() 199 | 200 | userIdStr := strconv.FormatInt(msg.UserId, 10) 201 | toUserIdStr := strconv.FormatInt(msg.FollowUserId, 10) 202 | followSetKey := config.FollowKey + userIdStr 203 | followerSetKey := config.FollowerKey + toUserIdStr 204 | // 删除缓存 SET 中的ID , 避免脏数据产生 205 | utils.GetRedisDB().SRem(context.Background(), followSetKey, toUserIdStr) 206 | utils.GetRedisDB().SRem(context.Background(), followerSetKey, userIdStr) 207 | } 208 | default: 209 | // 如果channel为空,暂停一段时间后重新监听 210 | time.Sleep(time.Millisecond * 1) 211 | } 212 | } 213 | } 214 | 215 | // 重建缓存 216 | func BuildFollowRedis(userId int64) error { 217 | relationService := RelationServiceImpl{} 218 | relationService.Logger = logrus.New() 219 | idSet, err := relationService.GetFollows(userId) 220 | if err != nil { 221 | return err 222 | } 223 | userIdStr := strconv.FormatInt(userId, 10) 224 | followSetKey := config.FollowKey + userIdStr 225 | var strValues []string 226 | for i := range idSet { 227 | strValues = append(strValues, strconv.FormatInt(idSet[i].Id, 10)) 228 | } 229 | 230 | ctx := context.Background() 231 | err = utils.GetRedisDB().SAdd(ctx, followSetKey, strValues).Err() 232 | errSetTime := utils.GetRedisDB().Expire(ctx, followSetKey, time.Duration(config.FollowKeyTTL)*time.Second).Err() 233 | if errSetTime != nil { 234 | log.Println("redis 时间设置失败", errSetTime.Error()) 235 | } 236 | return err 237 | } 238 | 239 | func BuildFollowerRedis(toUserId int64) error { 240 | relationService := RelationServiceImpl{} 241 | relationService.Logger = logrus.New() 242 | idSet, err := relationService.GetFollowers(toUserId) 243 | if err != nil { 244 | return err 245 | } 246 | toUserIdStr := strconv.FormatInt(toUserId, 10) 247 | followerSetKey := config.FollowerKey + toUserIdStr 248 | var strValues []string 249 | for i := range idSet { 250 | strValues = append(strValues, strconv.FormatInt(idSet[i].Id, 10)) 251 | } 252 | 253 | ctx := context.Background() 254 | err = utils.GetRedisDB().SAdd(ctx, followerSetKey, strValues).Err() 255 | errSetTime := utils.GetRedisDB().Expire(ctx, followerSetKey, time.Duration(config.FollowKeyTTL)*time.Second).Err() 256 | if errSetTime != nil { 257 | log.Println("redis 时间设置失败", errSetTime.Error()) 258 | } 259 | return err 260 | } 261 | 262 | // 创建消费者协程 263 | func MakeFollowGroutine() { 264 | numConsumers := 20 265 | for i := 0; i < numConsumers; i++ { 266 | go FollowConsumer(mq.FollowChannel) 267 | } 268 | } 269 | 270 | // GetFollows 查询关注列表 271 | func (relationServiceImpl RelationServiceImpl) GetFollows(userId int64) ([]models.User, error) { 272 | relationServiceImpl.Logger.Info("GetFollows\n") 273 | var users []models.User 274 | 275 | // 查询Redis中是否存在该用户ID,如果存在则,则使用协程获取数据并查询放到users中,否则原逻辑 276 | follows := utils.GetRedisDB().SMembers(context.Background(), fmt.Sprintf("follow:%d", userId)).Val() 277 | 278 | fmt.Println("follows = ", follows) 279 | 280 | if len(follows) > 0 { 281 | users = make([]models.User, len(follows)) 282 | var wg sync.WaitGroup 283 | tx := utils.GetMysqlDB().Begin() 284 | for i, v := range follows { 285 | wg.Add(1) 286 | go func(index int, value string) { 287 | defer wg.Done() 288 | tx.Table("user").Where("id = ? and is_deleted = ?", value, 0).Find(users[index]) 289 | }(i, v) 290 | } 291 | wg.Wait() 292 | } else { 293 | err := utils.GetMysqlDB().Table("follow").Where("user_id = ? AND is_deleted != ?", userId, 1).Find(&users).Error 294 | if err != nil { 295 | return nil, err 296 | } 297 | } 298 | 299 | //协程并发更新,isFollow 为 True 前端才能显示已关注 300 | var wg sync.WaitGroup 301 | for i := 0; i < len(users); i++ { 302 | wg.Add(1) 303 | go func(i int) { 304 | defer wg.Done() 305 | users[i].IsFollow = true 306 | }(i) 307 | } 308 | 309 | wg.Wait() 310 | 311 | return users, nil 312 | } 313 | 314 | // GetFollowers 查询粉丝列表 315 | func (relationServiceImpl RelationServiceImpl) GetFollowers(userId int64) ([]models.User, error) { 316 | relationServiceImpl.Logger.Info("GetFollowers") 317 | var users []models.User 318 | 319 | // 查询redis中有没有粉丝列表集合,如果有则启动多协程查询,如果没有则原逻辑 320 | followers := utils.GetRedisDB().SMembers(context.Background(), fmt.Sprintf("follower:%d", userId)).Val() 321 | fmt.Println("followers = ", followers) 322 | 323 | if len(followers) > 0 { 324 | users = make([]models.User, len(followers)) 325 | var wg sync.WaitGroup 326 | tx := utils.GetMysqlDB().Begin() 327 | for i, v := range followers { 328 | wg.Add(1) 329 | go func(index int, value string) { 330 | defer wg.Done() 331 | tx.Table("user").Where("id = ? and is_deleted = ?", value, 0).Find(users[index]) 332 | }(i, v) 333 | } 334 | wg.Wait() 335 | 336 | return users, nil 337 | } 338 | 339 | err := utils.GetMysqlDB().Table("follow").Where("follow_user_id = ? AND is_deleted != ?", userId, 1).Find(&users).Error 340 | if err != nil { 341 | return nil, err 342 | } 343 | return users, nil 344 | } 345 | 346 | // GetFriends 查询好友列表 347 | func (relationServiceImpl RelationServiceImpl) GetFriends(userId int64) ([]models.User, error) { 348 | 349 | jugeExist(userId, "follow", fromMysqlToRedis) 350 | 351 | jugeExist(userId, "follower", fromMysqlToRedis) 352 | 353 | key1 := fmt.Sprintf("%s:%d", "follow", userId) 354 | key2 := fmt.Sprintf("%s:%d", "follower", userId) 355 | vals := utils.GetRedisDB().SInter(context.Background(), key1, key2).Val() 356 | 357 | users := make([]models.User, 0) 358 | err := utils.GetMysqlDB().Table("user").Find(&users, vals).Error 359 | 360 | return users, err 361 | 362 | // follows, err := relationServiceImpl.GetFollows(userId) 363 | // if err != nil { 364 | // return nil, err 365 | // } 366 | // followers, err := relationServiceImpl.GetFollowers(userId) 367 | // if err != nil { 368 | // return nil, err 369 | // } 370 | // var friends []models.User 371 | // for _, user := range followers { 372 | // if containsID(follows, user.Id) { 373 | // friends = append(friends, user) 374 | // } 375 | // } 376 | // return friends, nil 377 | } 378 | 379 | func fromMysqlToRedis(typeStr string, userId int64) (err error) { 380 | 381 | var wh string 382 | var s string 383 | 384 | if typeStr == "follow" { 385 | wh = "user_id = ? and is_deleted = ?" 386 | s = "follow_user_id" 387 | } else { 388 | wh = "follow_user_id = ? and is_deleted = ?" 389 | s = "user_id" 390 | } 391 | 392 | userIds := make([]int64, 0) 393 | if err = utils.GetMysqlDB().Table("follow").Select(s).Where(wh, userId, 0).Find(&userIds).Error; err != nil { 394 | return 395 | } 396 | 397 | key := fmt.Sprintf("%s:%d", typeStr, userId) 398 | if len(userIds) > 0 { 399 | redisDB := utils.GetRedisDB() 400 | redisDB.SAdd(context.Background(), key, userIds) 401 | } 402 | 403 | return 404 | } 405 | 406 | // 判断redis中是否存在key,并且不存在时,调用回调函数 407 | func jugeExist(userId int64, typeStr string, callback func(t string, u int64) error) (err error) { 408 | followerExists := utils.GetRedisDB().Exists(context.Background(), fmt.Sprintf("%s:%d", typeStr, userId)).Val() 409 | 410 | if followerExists == 0 { 411 | // 不存在 412 | err = callback(typeStr, userId) 413 | return 414 | } 415 | return 416 | } 417 | 418 | // containsID 辅助函数,用于检查指定的 id 是否在数组中存在 419 | func containsID(arr []models.User, id int64) bool { 420 | for _, u := range arr { 421 | if u.Id == id { 422 | return true 423 | } 424 | } 425 | return false 426 | } 427 | 428 | func getFollowByUserIdAndToUserId(userId int64, toUserId int64) (*models.Follow, error) { 429 | res := &models.Follow{} 430 | err := utils.GetMysqlDB().Model(models.Follow{}).Where("user_id = ? AND follow_user_id = ? AND is_deleted = ?", userId, toUserId, 0).Find(res).Error 431 | return res, err 432 | } 433 | --------------------------------------------------------------------------------