From d68a4f3f65e13034a3e36317f9193f00cea4fe52 Mon Sep 17 00:00:00 2001 From: Sisyphus Date: Sat, 25 Apr 2026 15:29:47 +0800 Subject: [PATCH] =?UTF-8?q?fix(P1):=20Router=20Bug=20=E4=BF=AE=E5=A4=8D=20?= =?UTF-8?q?+=20Video=20=E5=A4=8D=E5=90=88=E7=B4=A2=E5=BC=95=20+=20ListByAu?= =?UTF-8?q?thorID=20=E5=8A=A0=20LIMIT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/http/router.go | 304 ++++++++++++------------- backend/internal/video/video_entity.go | 97 ++++---- backend/internal/video/video_repo.go | 208 ++++++++--------- 3 files changed, 305 insertions(+), 304 deletions(-) diff --git a/backend/internal/http/router.go b/backend/internal/http/router.go index 73b73d3..483752c 100644 --- a/backend/internal/http/router.go +++ b/backend/internal/http/router.go @@ -1,152 +1,152 @@ -package http - -import ( - "feedsystem_video_go/internal/account" - "feedsystem_video_go/internal/feed" - "feedsystem_video_go/internal/middleware/jwt" - "feedsystem_video_go/internal/middleware/ratelimit" - "feedsystem_video_go/internal/middleware/rabbitmq" - rediscache "feedsystem_video_go/internal/middleware/redis" - "feedsystem_video_go/internal/social" - "feedsystem_video_go/internal/video" - "feedsystem_video_go/internal/worker" - "log" - "time" - "github.com/gin-gonic/gin" - "gorm.io/gorm" -) - -func SetRouter(db *gorm.DB, cache *rediscache.Client, rmq *rabbitmq.RabbitMQ) *gin.Engine { - r := gin.Default() - if err := r.SetTrustedProxies(nil); err != nil { - log.Printf("SetTrustedProxies failed: %v", err) - } - r.Static("/static", "./.run/uploads") - // rate_limit - loginLimiter := ratelimit.Limit(cache, "account_login", 10, time.Minute, ratelimit.KeyByIP) - registerLimiter := ratelimit.Limit(cache, "account_register", 5, time.Hour, ratelimit.KeyByIP) - - likeLimiter := ratelimit.Limit(cache, "like_write", 30, time.Minute, ratelimit.KeyByAccount) - commentLimiter := ratelimit.Limit(cache, "comment_write", 10, time.Minute, ratelimit.KeyByAccount) - socialLimiter := ratelimit.Limit(cache, "social_write", 20, time.Minute, ratelimit.KeyByAccount) - - // account - accountRepository := account.NewAccountRepository(db) - accountService := account.NewAccountService(accountRepository, cache) - accountHandler := account.NewAccountHandler(accountService) - accountGroup := r.Group("/account") - { - accountGroup.POST("/register", registerLimiter, accountHandler.CreateAccount) - accountGroup.POST("/login", loginLimiter, accountHandler.Login) - accountGroup.POST("/changePassword", accountHandler.ChangePassword) - accountGroup.POST("/findByID", accountHandler.FindByID) - accountGroup.POST("/findByUsername", accountHandler.FindByUsername) - } - protectedAccountGroup := accountGroup.Group("") - protectedAccountGroup.Use(jwt.JWTAuth(accountRepository, cache)) - { - protectedAccountGroup.POST("/logout", accountHandler.Logout) - protectedAccountGroup.POST("/rename", accountHandler.Rename) - } - // video - videoRepository := video.NewVideoRepository(db) - popularityMQ, err := rabbitmq.NewPopularityMQ(rmq) - if err != nil { - log.Printf("PopularityMQ init failed (mq disabled): %v", err) - popularityMQ = nil - } - videoService := video.NewVideoService(videoRepository, cache, popularityMQ) - videoHandler := video.NewVideoHandler(videoService, accountService) - videoGroup := r.Group("/video") - { - videoGroup.POST("/listByAuthorID", videoHandler.ListByAuthorID) - videoGroup.POST("/getDetail", videoHandler.GetDetail) - } - protectedVideoGroup := videoGroup.Group("") - protectedVideoGroup.Use(jwt.JWTAuth(accountRepository, cache)) - { - protectedVideoGroup.POST("/uploadVideo", videoHandler.UploadVideo) - protectedVideoGroup.POST("/uploadCover", videoHandler.UploadCover) - protectedVideoGroup.POST("/publish", videoHandler.PublishVideo) - } - // like - likeMQ, err := rabbitmq.NewLikeMQ(rmq) - if err != nil { - log.Printf("LikeMQ init failed (mq disabled): %v", err) - likeMQ = nil - } - likeRepository := video.NewLikeRepository(db) - likeService := video.NewLikeService(likeRepository, videoRepository, cache, likeMQ, popularityMQ) - likeHandler := video.NewLikeHandler(likeService) - likeGroup := r.Group("/like") - protectedLikeGroup := likeGroup.Group("") - protectedLikeGroup.Use(jwt.JWTAuth(accountRepository, cache)) - { - protectedLikeGroup.POST("/like", likeLimiter, likeHandler.Like) - protectedLikeGroup.POST("/unlike", likeLimiter, likeHandler.Unlike) - protectedLikeGroup.POST("/isLiked", likeHandler.IsLiked) - protectedLikeGroup.POST("/listMyLikedVideos", likeHandler.ListMyLikedVideos) - } - // comment - commentRepository := video.NewCommentRepository(db) - commentMQ, err := rabbitmq.NewCommentMQ(rmq) - if err != nil { - log.Printf("CommentMQ init failed (mq disabled): %v", err) - commentMQ = nil - } - commentService := video.NewCommentService(commentRepository, videoRepository, cache, commentMQ, popularityMQ) - commentHandler := video.NewCommentHandler(commentService, accountService) - commentGroup := r.Group("/comment") - { - commentGroup.POST("/listAll", commentHandler.GetAllComments) - } - protectedCommentGroup := commentGroup.Group("") - protectedCommentGroup.Use(jwt.JWTAuth(accountRepository, cache)) - { - protectedCommentGroup.POST("/publish", commentLimiter, commentHandler.PublishComment) - protectedCommentGroup.POST("/delete", commentLimiter, commentHandler.DeleteComment) - } - // social - socialMQ, err := rabbitmq.NewSocialMQ(rmq) - if err != nil { - log.Printf("SocialMQ init failed (mq disabled): %v", err) - socialMQ = nil - } - socialRepository := social.NewSocialRepository(db) - socialService := social.NewSocialService(socialRepository, accountRepository, socialMQ) - socialHandler := social.NewSocialHandler(socialService) - socialGroup := r.Group("/social") - protectedSocialGroup := socialGroup.Group("") - protectedSocialGroup.Use(jwt.JWTAuth(accountRepository, cache)) - { - protectedSocialGroup.POST("/follow", socialLimiter, socialHandler.Follow) - protectedSocialGroup.POST("/unfollow", socialLimiter, socialHandler.Unfollow) - protectedSocialGroup.POST("/getAllFollowers", socialHandler.GetAllFollowers) - protectedSocialGroup.POST("/getAllVloggers", socialHandler.GetAllVloggers) - } - // feed - feedRepository := feed.NewFeedRepository(db) - feedService := feed.NewFeedService(feedRepository, likeRepository, cache) - feedHandler := feed.NewFeedHandler(feedService) - feedGroup := r.Group("/feed") - feedGroup.Use(jwt.SoftJWTAuth(accountRepository, cache)) - { - feedGroup.POST("/listLatest", feedHandler.ListLatest) - feedGroup.POST("/listLikesCount", feedHandler.ListLikesCount) - feedGroup.POST("/listByPopularity", feedHandler.ListByPopularity) - } - protectedFeedGroup := feedGroup.Group("") - protectedFeedGroup.Use(jwt.JWTAuth(accountRepository, cache)) - { - protectedFeedGroup.POST("/listByFollowing", feedHandler.ListByFollowing) - } - //worker - timelineMQ, err := rabbitmq.NewTimelineMQ(rmq) - if err != nil { - log.Printf("timelineMQ init failed (mq disabled): %v", err) - socialMQ = nil - } - worker.StartOutboxPoller(db, timelineMQ) - worker.StartConsumer(timelineMQ, "video.timeline.update.queue", cache) - return r -} +package http + +import ( + "feedsystem_video_go/internal/account" + "feedsystem_video_go/internal/feed" + "feedsystem_video_go/internal/middleware/jwt" + "feedsystem_video_go/internal/middleware/ratelimit" + "feedsystem_video_go/internal/middleware/rabbitmq" + rediscache "feedsystem_video_go/internal/middleware/redis" + "feedsystem_video_go/internal/social" + "feedsystem_video_go/internal/video" + "feedsystem_video_go/internal/worker" + "log" + "time" + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +func SetRouter(db *gorm.DB, cache *rediscache.Client, rmq *rabbitmq.RabbitMQ) *gin.Engine { + r := gin.Default() + if err := r.SetTrustedProxies(nil); err != nil { + log.Printf("SetTrustedProxies failed: %v", err) + } + r.Static("/static", "./.run/uploads") + // rate_limit + loginLimiter := ratelimit.Limit(cache, "account_login", 10, time.Minute, ratelimit.KeyByIP) + registerLimiter := ratelimit.Limit(cache, "account_register", 5, time.Hour, ratelimit.KeyByIP) + + likeLimiter := ratelimit.Limit(cache, "like_write", 30, time.Minute, ratelimit.KeyByAccount) + commentLimiter := ratelimit.Limit(cache, "comment_write", 10, time.Minute, ratelimit.KeyByAccount) + socialLimiter := ratelimit.Limit(cache, "social_write", 20, time.Minute, ratelimit.KeyByAccount) + + // account + accountRepository := account.NewAccountRepository(db) + accountService := account.NewAccountService(accountRepository, cache) + accountHandler := account.NewAccountHandler(accountService) + accountGroup := r.Group("/account") + { + accountGroup.POST("/register", registerLimiter, accountHandler.CreateAccount) + accountGroup.POST("/login", loginLimiter, accountHandler.Login) + accountGroup.POST("/changePassword", accountHandler.ChangePassword) + accountGroup.POST("/findByID", accountHandler.FindByID) + accountGroup.POST("/findByUsername", accountHandler.FindByUsername) + } + protectedAccountGroup := accountGroup.Group("") + protectedAccountGroup.Use(jwt.JWTAuth(accountRepository, cache)) + { + protectedAccountGroup.POST("/logout", accountHandler.Logout) + protectedAccountGroup.POST("/rename", accountHandler.Rename) + } + // video + videoRepository := video.NewVideoRepository(db) + popularityMQ, err := rabbitmq.NewPopularityMQ(rmq) + if err != nil { + log.Printf("PopularityMQ init failed (mq disabled): %v", err) + popularityMQ = nil + } + videoService := video.NewVideoService(videoRepository, cache, popularityMQ) + videoHandler := video.NewVideoHandler(videoService, accountService) + videoGroup := r.Group("/video") + { + videoGroup.POST("/listByAuthorID", videoHandler.ListByAuthorID) + videoGroup.POST("/getDetail", videoHandler.GetDetail) + } + protectedVideoGroup := videoGroup.Group("") + protectedVideoGroup.Use(jwt.JWTAuth(accountRepository, cache)) + { + protectedVideoGroup.POST("/uploadVideo", videoHandler.UploadVideo) + protectedVideoGroup.POST("/uploadCover", videoHandler.UploadCover) + protectedVideoGroup.POST("/publish", videoHandler.PublishVideo) + } + // like + likeMQ, err := rabbitmq.NewLikeMQ(rmq) + if err != nil { + log.Printf("LikeMQ init failed (mq disabled): %v", err) + likeMQ = nil + } + likeRepository := video.NewLikeRepository(db) + likeService := video.NewLikeService(likeRepository, videoRepository, cache, likeMQ, popularityMQ) + likeHandler := video.NewLikeHandler(likeService) + likeGroup := r.Group("/like") + protectedLikeGroup := likeGroup.Group("") + protectedLikeGroup.Use(jwt.JWTAuth(accountRepository, cache)) + { + protectedLikeGroup.POST("/like", likeLimiter, likeHandler.Like) + protectedLikeGroup.POST("/unlike", likeLimiter, likeHandler.Unlike) + protectedLikeGroup.POST("/isLiked", likeHandler.IsLiked) + protectedLikeGroup.POST("/listMyLikedVideos", likeHandler.ListMyLikedVideos) + } + // comment + commentRepository := video.NewCommentRepository(db) + commentMQ, err := rabbitmq.NewCommentMQ(rmq) + if err != nil { + log.Printf("CommentMQ init failed (mq disabled): %v", err) + commentMQ = nil + } + commentService := video.NewCommentService(commentRepository, videoRepository, cache, commentMQ, popularityMQ) + commentHandler := video.NewCommentHandler(commentService, accountService) + commentGroup := r.Group("/comment") + { + commentGroup.POST("/listAll", commentHandler.GetAllComments) + } + protectedCommentGroup := commentGroup.Group("") + protectedCommentGroup.Use(jwt.JWTAuth(accountRepository, cache)) + { + protectedCommentGroup.POST("/publish", commentLimiter, commentHandler.PublishComment) + protectedCommentGroup.POST("/delete", commentLimiter, commentHandler.DeleteComment) + } + // social + socialMQ, err := rabbitmq.NewSocialMQ(rmq) + if err != nil { + log.Printf("SocialMQ init failed (mq disabled): %v", err) + socialMQ = nil + } + socialRepository := social.NewSocialRepository(db) + socialService := social.NewSocialService(socialRepository, accountRepository, socialMQ) + socialHandler := social.NewSocialHandler(socialService) + socialGroup := r.Group("/social") + protectedSocialGroup := socialGroup.Group("") + protectedSocialGroup.Use(jwt.JWTAuth(accountRepository, cache)) + { + protectedSocialGroup.POST("/follow", socialLimiter, socialHandler.Follow) + protectedSocialGroup.POST("/unfollow", socialLimiter, socialHandler.Unfollow) + protectedSocialGroup.POST("/getAllFollowers", socialHandler.GetAllFollowers) + protectedSocialGroup.POST("/getAllVloggers", socialHandler.GetAllVloggers) + } + // feed + feedRepository := feed.NewFeedRepository(db) + feedService := feed.NewFeedService(feedRepository, likeRepository, cache) + feedHandler := feed.NewFeedHandler(feedService) + feedGroup := r.Group("/feed") + feedGroup.Use(jwt.SoftJWTAuth(accountRepository, cache)) + { + feedGroup.POST("/listLatest", feedHandler.ListLatest) + feedGroup.POST("/listLikesCount", feedHandler.ListLikesCount) + feedGroup.POST("/listByPopularity", feedHandler.ListByPopularity) + } + protectedFeedGroup := feedGroup.Group("") + protectedFeedGroup.Use(jwt.JWTAuth(accountRepository, cache)) + { + protectedFeedGroup.POST("/listByFollowing", feedHandler.ListByFollowing) + } + //worker + timelineMQ, err := rabbitmq.NewTimelineMQ(rmq) + if err != nil { + log.Printf("timelineMQ init failed (mq disabled): %v", err) + timelineMQ = nil + } + worker.StartOutboxPoller(db, timelineMQ) + worker.StartConsumer(timelineMQ, "video.timeline.update.queue", cache) + return r +} diff --git a/backend/internal/video/video_entity.go b/backend/internal/video/video_entity.go index 9487427..67bf022 100644 --- a/backend/internal/video/video_entity.go +++ b/backend/internal/video/video_entity.go @@ -1,48 +1,49 @@ -package video - -import "time" - -type Video struct { - ID uint `gorm:"primaryKey" json:"id"` - AuthorID uint `gorm:"index;not null" json:"author_id"` - Username string `gorm:"type:varchar(255);not null" json:"username"` - Title string `gorm:"type:varchar(255);not null" json:"title"` - Description string `gorm:"type:varchar(255);" json:"description,omitempty"` - PlayURL string `gorm:"type:varchar(255);not null" json:"play_url"` - CoverURL string `gorm:"type:varchar(255);not null" json:"cover_url"` - CreateTime time.Time `gorm:"autoCreateTime" json:"create_time"` - LikesCount int64 `gorm:"column:likes_count;not null;default:0" json:"likes_count"` - Popularity int64 `gorm:"column:popularity;not null;default:0" json:"popularity"` -} - -type PublishVideoRequest struct { - Title string `json:"title"` - Description string `json:"description"` - PlayURL string `json:"play_url"` - CoverURL string `json:"cover_url"` -} - -type DeleteVideoRequest struct { - ID uint `json:"id"` -} - -type ListByAuthorIDRequest struct { - AuthorID uint `json:"author_id"` -} - -type GetDetailRequest struct { - ID uint `json:"id"` -} - -type UpdateLikesCountRequest struct { - ID uint `json:"id"` - LikesCount int64 `json:"likes_count"` -} - -type OutboxMsg struct { - ID uint `gorm:"primaryKey"` - VideoID uint `gorm:"index"` - EventType string `gorm:"type:varchar(50)"` - CreateTime time.Time `gorm:"autoCreateTime"` - Status string `gorm:"type:varchar(50);index"` -} +package video + +import "time" + +type Video struct { + ID uint `gorm:"primaryKey" json:"id"` + AuthorID uint `gorm:"index;not null" json:"author_id"` + Username string `gorm:"type:varchar(255);not null" json:"username"` + Title string `gorm:"type:varchar(255);not null" json:"title"` + Description string `gorm:"type:varchar(255);" json:"description,omitempty"` + PlayURL string `gorm:"type:varchar(255);not null" json:"play_url"` + CoverURL string `gorm:"type:varchar(255);not null" json:"cover_url"` + CreateTime time.Time `gorm:"autoCreateTime;index:idx_videos_create_time,sort:desc;index:idx_videos_popularity_time_id,priority:2,sort:desc" json:"create_time"` + LikesCount int64 `gorm:"column:likes_count;not null;default:0;index:idx_videos_likes_count_id,priority:1,sort:desc" json:"likes_count"` + Popularity int64 `gorm:"column:popularity;not null;default:0;index:idx_videos_popularity_time_id,priority:1,sort:desc" json:"popularity"` +} +} + +type PublishVideoRequest struct { + Title string `json:"title"` + Description string `json:"description"` + PlayURL string `json:"play_url"` + CoverURL string `json:"cover_url"` +} + +type DeleteVideoRequest struct { + ID uint `json:"id"` +} + +type ListByAuthorIDRequest struct { + AuthorID uint `json:"author_id"` +} + +type GetDetailRequest struct { + ID uint `json:"id"` +} + +type UpdateLikesCountRequest struct { + ID uint `json:"id"` + LikesCount int64 `json:"likes_count"` +} + +type OutboxMsg struct { + ID uint `gorm:"primaryKey"` + VideoID uint `gorm:"index"` + EventType string `gorm:"type:varchar(50)"` + CreateTime time.Time `gorm:"autoCreateTime"` + Status string `gorm:"type:varchar(50);index"` +} diff --git a/backend/internal/video/video_repo.go b/backend/internal/video/video_repo.go index d8ee81a..2cc5597 100644 --- a/backend/internal/video/video_repo.go +++ b/backend/internal/video/video_repo.go @@ -1,104 +1,104 @@ -package video - -import ( - "context" - "errors" - - "gorm.io/gorm" -) - -type VideoRepository struct { - db *gorm.DB -} - -func NewVideoRepository(db *gorm.DB) *VideoRepository { - return &VideoRepository{db: db} -} - -func (vr *VideoRepository) CreateVideo(ctx context.Context, video *Video) error { - if err := vr.db.WithContext(ctx).Create(video).Error; err != nil { - return err - } - return nil -} - -func (vr *VideoRepository) CreateMsg(ctx context.Context, Msg *OutboxMsg) error { - if err := vr.db.WithContext(ctx).Create(Msg).Error; err != nil { - return err - } - return nil -} - -func (vr *VideoRepository) DeleteVideo(ctx context.Context, id uint) error { - if err := vr.db.WithContext(ctx).Delete(&Video{}, id).Error; err != nil { - return err - } - return nil -} - -func (vr *VideoRepository) ListByAuthorID(ctx context.Context, authorID int64) ([]Video, error) { - var videos []Video - if err := vr.db.WithContext(ctx). - Where("author_id = ?", authorID). - Order("create_time desc"). - Offset(0). - Find(&videos).Error; err != nil { - return nil, err - } - return videos, nil -} - -func (vr *VideoRepository) GetByID(ctx context.Context, id uint) (*Video, error) { - var video Video - if err := vr.db.WithContext(ctx).First(&video, id).Error; err != nil { - return (*Video)(nil), err - } - return &video, nil -} - -func (vr *VideoRepository) UpdateLikesCount(ctx context.Context, id uint, likesCount int64) error { - if err := vr.db.WithContext(ctx).Model(&Video{}). - Where("id = ?", id). - Update("likes_count", likesCount).Error; err != nil { - return err - } - return nil -} - -func (vr *VideoRepository) IsExist(ctx context.Context, id uint) (bool, error) { - var video Video - if err := vr.db.WithContext(ctx).First(&video, id).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return false, nil - } - return false, err - } - return true, nil -} - -func (vr *VideoRepository) UpdatePopularity(ctx context.Context, id uint, change int64) error { - if err := vr.db.WithContext(ctx).Model(&Video{}). - Where("id = ?", id). - Update("popularity", gorm.Expr("popularity + ?", change)).Error; err != nil { - return err - } - return nil -} - -func (vr *VideoRepository) ChangeLikesCount(ctx context.Context, id uint, change int64) error { - if err := vr.db.WithContext(ctx).Model(&Video{}). - Where("id = ?", id). - UpdateColumn("likes_count", gorm.Expr("GREATEST(likes_count + ?, 0)", change)).Error; err != nil { - return err - } - return nil -} - -func (vr *VideoRepository) ChangePopularity(ctx context.Context, id uint, change int64) error { - if err := vr.db.WithContext(ctx).Model(&Video{}). - Where("id = ?", id). - UpdateColumn("popularity", gorm.Expr("GREATEST(popularity + ?, 0)", change)).Error; err != nil { - return err - } - return nil -} +package video + +import ( + "context" + "errors" + + "gorm.io/gorm" +) + +type VideoRepository struct { + db *gorm.DB +} + +func NewVideoRepository(db *gorm.DB) *VideoRepository { + return &VideoRepository{db: db} +} + +func (vr *VideoRepository) CreateVideo(ctx context.Context, video *Video) error { + if err := vr.db.WithContext(ctx).Create(video).Error; err != nil { + return err + } + return nil +} + +func (vr *VideoRepository) CreateMsg(ctx context.Context, Msg *OutboxMsg) error { + if err := vr.db.WithContext(ctx).Create(Msg).Error; err != nil { + return err + } + return nil +} + +func (vr *VideoRepository) DeleteVideo(ctx context.Context, id uint) error { + if err := vr.db.WithContext(ctx).Delete(&Video{}, id).Error; err != nil { + return err + } + return nil +} + +func (vr *VideoRepository) ListByAuthorID(ctx context.Context, authorID int64) ([]Video, error) { + var videos []Video + if err := vr.db.WithContext(ctx). + Where("author_id = ?", authorID). + Order("create_time desc"). + Limit(200). + Find(&videos).Error; err != nil { + return nil, err + } + return videos, nil +} + +func (vr *VideoRepository) GetByID(ctx context.Context, id uint) (*Video, error) { + var video Video + if err := vr.db.WithContext(ctx).First(&video, id).Error; err != nil { + return (*Video)(nil), err + } + return &video, nil +} + +func (vr *VideoRepository) UpdateLikesCount(ctx context.Context, id uint, likesCount int64) error { + if err := vr.db.WithContext(ctx).Model(&Video{}). + Where("id = ?", id). + Update("likes_count", likesCount).Error; err != nil { + return err + } + return nil +} + +func (vr *VideoRepository) IsExist(ctx context.Context, id uint) (bool, error) { + var video Video + if err := vr.db.WithContext(ctx).First(&video, id).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return false, nil + } + return false, err + } + return true, nil +} + +func (vr *VideoRepository) UpdatePopularity(ctx context.Context, id uint, change int64) error { + if err := vr.db.WithContext(ctx).Model(&Video{}). + Where("id = ?", id). + Update("popularity", gorm.Expr("popularity + ?", change)).Error; err != nil { + return err + } + return nil +} + +func (vr *VideoRepository) ChangeLikesCount(ctx context.Context, id uint, change int64) error { + if err := vr.db.WithContext(ctx).Model(&Video{}). + Where("id = ?", id). + UpdateColumn("likes_count", gorm.Expr("GREATEST(likes_count + ?, 0)", change)).Error; err != nil { + return err + } + return nil +} + +func (vr *VideoRepository) ChangePopularity(ctx context.Context, id uint, change int64) error { + if err := vr.db.WithContext(ctx).Model(&Video{}). + Where("id = ?", id). + UpdateColumn("popularity", gorm.Expr("GREATEST(popularity + ?, 0)", change)).Error; err != nil { + return err + } + return nil +}