feat:添加热榜功能并添加了增加热度的方法。
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
package feed
|
package feed
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
type FeedAuthor struct {
|
type FeedAuthor struct {
|
||||||
ID uint `json:"id"`
|
ID uint `json:"id"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
@@ -56,3 +58,25 @@ type ListByFollowingResponse struct {
|
|||||||
NextTime int64 `json:"next_time"`
|
NextTime int64 `json:"next_time"`
|
||||||
HasMore bool `json:"has_more"`
|
HasMore bool `json:"has_more"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ListByPopularityRequest struct {
|
||||||
|
Limit int `json:"limit"`
|
||||||
|
AsOf int64 `json:"as_of"` // 服务器返回的分钟时间戳;第一页传0
|
||||||
|
Offset int `json:"offset"` // 下一页从这里开始;第一页传0
|
||||||
|
LatestIDBefore *uint `json:"latest_id_before,omitempty"`
|
||||||
|
|
||||||
|
// DB fallback 用(可选)
|
||||||
|
LatestPopularity int64 `json:"latest_popularity"`
|
||||||
|
LatestBefore time.Time `json:"latest_before"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListByPopularityResponse struct {
|
||||||
|
VideoList []FeedVideoItem `json:"video_list"`
|
||||||
|
AsOf int64 `json:"as_of"`
|
||||||
|
NextOffset int `json:"next_offset"`
|
||||||
|
HasMore bool `json:"has_more"`
|
||||||
|
|
||||||
|
NextLatestPopularity *int64 `json:"next_latest_popularity,omitempty"`
|
||||||
|
NextLatestBefore *time.Time `json:"next_latest_before,omitempty"`
|
||||||
|
NextLatestIDBefore *uint `json:"next_latest_id_before,omitempty"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -112,3 +112,53 @@ func (f *FeedHandler) ListByFollowing(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
c.JSON(200, feedItems)
|
c.JSON(200, feedItems)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *FeedHandler) ListByPopularity(c *gin.Context) {
|
||||||
|
var req ListByPopularityRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(400, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.Limit <= 0 || req.Limit > 50 {
|
||||||
|
req.Limit = 10
|
||||||
|
}
|
||||||
|
viewerAccountID, err := middleware.GetAccountID(c)
|
||||||
|
if err != nil {
|
||||||
|
viewerAccountID = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var latestPopularity int64
|
||||||
|
var latestBefore time.Time
|
||||||
|
var latestIDBefore uint
|
||||||
|
|
||||||
|
if req.LatestPopularity < 0 {
|
||||||
|
c.JSON(400, gin.H{"error": "latest_popularity must be >= 0"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
anyCursor := !req.LatestBefore.IsZero() || req.LatestIDBefore != nil
|
||||||
|
if anyCursor {
|
||||||
|
if req.LatestBefore.IsZero() || req.LatestIDBefore == nil || *req.LatestIDBefore == 0 {
|
||||||
|
c.JSON(400, gin.H{"error": "latest_before and latest_id_before must be provided together"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
latestPopularity = req.LatestPopularity
|
||||||
|
latestBefore = req.LatestBefore
|
||||||
|
latestIDBefore = *req.LatestIDBefore
|
||||||
|
}
|
||||||
|
resp, err := f.service.ListByPopularity(
|
||||||
|
c.Request.Context(),
|
||||||
|
req.Limit,
|
||||||
|
req.AsOf,
|
||||||
|
req.Offset,
|
||||||
|
viewerAccountID,
|
||||||
|
latestPopularity,
|
||||||
|
latestBefore,
|
||||||
|
latestIDBefore,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(200, resp)
|
||||||
|
}
|
||||||
|
|||||||
@@ -68,3 +68,36 @@ func (repo *FeedRepository) ListByFollowing(ctx context.Context, limit int, view
|
|||||||
}
|
}
|
||||||
return videos, nil
|
return videos, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (repo *FeedRepository) ListByPopularity(ctx context.Context, limit int, popularityBefore int64, timeBefore time.Time, idBefore uint) ([]*video.Video, error) {
|
||||||
|
var videos []*video.Video
|
||||||
|
query := repo.db.WithContext(ctx).Model(&video.Video{}).
|
||||||
|
Order("popularity DESC, create_time DESC, id DESC")
|
||||||
|
|
||||||
|
// 只有当游标完整提供时才加过滤(popularity 允许为 0)
|
||||||
|
if !timeBefore.IsZero() && idBefore > 0 {
|
||||||
|
query = query.Where(
|
||||||
|
"(popularity < ?) OR (popularity = ? AND create_time < ?) OR (popularity = ? AND create_time = ? AND id < ?)",
|
||||||
|
popularityBefore,
|
||||||
|
popularityBefore, timeBefore,
|
||||||
|
popularityBefore, timeBefore, idBefore,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := query.Limit(limit).Find(&videos).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return videos, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *FeedRepository) GetByIDs(ctx context.Context, ids []uint) ([]*video.Video, error) {
|
||||||
|
var videos []*video.Video
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return videos, nil
|
||||||
|
}
|
||||||
|
if err := repo.db.WithContext(ctx).Model(&video.Video{}).
|
||||||
|
Where("id IN ?", ids).Find(&videos).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return videos, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
rediscache "feedsystem_video_go/internal/redis"
|
rediscache "feedsystem_video_go/internal/redis"
|
||||||
"feedsystem_video_go/internal/video"
|
"feedsystem_video_go/internal/video"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -229,6 +230,114 @@ func (f *FeedService) ListByFollowing(ctx context.Context, limit int, latestBefo
|
|||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *FeedService) ListByPopularity(ctx context.Context, limit int, reqAsOf int64, offset int, viewerAccountID uint, latestPopularity int64, latestBefore time.Time, latestIDBefore uint) (ListByPopularityResponse, error) {
|
||||||
|
// Redis 热榜(稳定分页:as_of + offset)
|
||||||
|
if f.cache != nil {
|
||||||
|
asOf := time.Now().UTC().Truncate(time.Minute)
|
||||||
|
if reqAsOf > 0 {
|
||||||
|
asOf = time.Unix(reqAsOf, 0).UTC().Truncate(time.Minute)
|
||||||
|
}
|
||||||
|
|
||||||
|
const win = 60
|
||||||
|
keys := make([]string, 0, win)
|
||||||
|
for i := 0; i < win; i++ {
|
||||||
|
keys = append(keys, "hot:video:1m:"+asOf.Add(-time.Duration(i)*time.Minute).Format("200601021504"))
|
||||||
|
}
|
||||||
|
|
||||||
|
dest := "hot:video:merge:1m:" + asOf.Format("200601021504") // 快照key:同一个as_of页内复用
|
||||||
|
opCtx, cancel := context.WithTimeout(ctx, 80*time.Millisecond)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
exists, _ := f.cache.Exists(opCtx, dest)
|
||||||
|
if !exists {
|
||||||
|
_ = f.cache.ZUnionStore(opCtx, dest, keys, "SUM")
|
||||||
|
_ = f.cache.Expire(opCtx, dest, 2*time.Minute) // 给翻页留时间
|
||||||
|
}
|
||||||
|
|
||||||
|
start := int64(offset)
|
||||||
|
stop := start + int64(limit) - 1
|
||||||
|
members, err := f.cache.ZRevRange(opCtx, dest, start, stop)
|
||||||
|
if err == nil && len(members) == 0 {
|
||||||
|
if offset > 0 {
|
||||||
|
return ListByPopularityResponse{
|
||||||
|
VideoList: []FeedVideoItem{},
|
||||||
|
AsOf: asOf.Unix(),
|
||||||
|
NextOffset: offset,
|
||||||
|
HasMore: false,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err == nil && len(members) > 0 {
|
||||||
|
ids := make([]uint, 0, len(members))
|
||||||
|
for _, m := range members {
|
||||||
|
u, err := strconv.ParseUint(m, 10, 64)
|
||||||
|
if err == nil && u > 0 {
|
||||||
|
ids = append(ids, uint(u))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
videos, err := f.repo.GetByIDs(ctx, ids)
|
||||||
|
if err == nil {
|
||||||
|
byID := make(map[uint]*video.Video, len(videos))
|
||||||
|
for _, v := range videos {
|
||||||
|
byID[v.ID] = v
|
||||||
|
}
|
||||||
|
ordered := make([]*video.Video, 0, len(ids))
|
||||||
|
for _, id := range ids {
|
||||||
|
if v := byID[id]; v != nil {
|
||||||
|
ordered = append(ordered, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
items, err := f.buildFeedVideos(ctx, ordered, viewerAccountID)
|
||||||
|
if err != nil {
|
||||||
|
return ListByPopularityResponse{}, err
|
||||||
|
}
|
||||||
|
resp := ListByPopularityResponse{
|
||||||
|
VideoList: items,
|
||||||
|
AsOf: asOf.Unix(),
|
||||||
|
NextOffset: offset + len(items),
|
||||||
|
HasMore: len(items) == limit,
|
||||||
|
}
|
||||||
|
if len(ordered) > 0 {
|
||||||
|
last := ordered[len(ordered)-1]
|
||||||
|
nextPopularity := last.Popularity
|
||||||
|
nextBefore := last.CreateTime
|
||||||
|
nextID := last.ID
|
||||||
|
resp.NextLatestPopularity = &nextPopularity
|
||||||
|
resp.NextLatestBefore = &nextBefore
|
||||||
|
resp.NextLatestIDBefore = &nextID
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
videos, err := f.repo.ListByPopularity(ctx, limit, latestPopularity, latestBefore, latestIDBefore)
|
||||||
|
if err != nil {
|
||||||
|
return ListByPopularityResponse{}, err
|
||||||
|
}
|
||||||
|
items, err := f.buildFeedVideos(ctx, videos, viewerAccountID)
|
||||||
|
if err != nil {
|
||||||
|
return ListByPopularityResponse{}, err
|
||||||
|
}
|
||||||
|
resp := ListByPopularityResponse{
|
||||||
|
VideoList: items,
|
||||||
|
AsOf: 0,
|
||||||
|
NextOffset: 0,
|
||||||
|
HasMore: len(items) == limit,
|
||||||
|
}
|
||||||
|
if len(videos) > 0 {
|
||||||
|
last := videos[len(videos)-1]
|
||||||
|
nextPopularity := last.Popularity
|
||||||
|
nextBefore := last.CreateTime
|
||||||
|
nextID := last.ID
|
||||||
|
resp.NextLatestPopularity = &nextPopularity
|
||||||
|
resp.NextLatestBefore = &nextBefore
|
||||||
|
resp.NextLatestIDBefore = &nextID
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (f *FeedService) buildFeedVideos(ctx context.Context, videos []*video.Video, viewerAccountID uint) ([]FeedVideoItem, error) {
|
func (f *FeedService) buildFeedVideos(ctx context.Context, videos []*video.Video, viewerAccountID uint) ([]FeedVideoItem, error) {
|
||||||
feedVideos := make([]FeedVideoItem, 0, len(videos))
|
feedVideos := make([]FeedVideoItem, 0, len(videos))
|
||||||
videoIDs := make([]uint, len(videos))
|
videoIDs := make([]uint, len(videos))
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ func SetRouter(db *gorm.DB, cache *rediscache.Client) *gin.Engine {
|
|||||||
// like
|
// like
|
||||||
likeRepository := video.NewLikeRepository(db)
|
likeRepository := video.NewLikeRepository(db)
|
||||||
likeService := video.NewLikeService(likeRepository, videoRepository)
|
likeService := video.NewLikeService(likeRepository, videoRepository)
|
||||||
likeHandler := video.NewLikeHandler(likeService)
|
likeHandler := video.NewLikeHandler(likeService, videoService)
|
||||||
likeGroup := r.Group("/like")
|
likeGroup := r.Group("/like")
|
||||||
protectedLikeGroup := likeGroup.Group("")
|
protectedLikeGroup := likeGroup.Group("")
|
||||||
protectedLikeGroup.Use(middleware.JWTAuth(accountRepository, cache))
|
protectedLikeGroup.Use(middleware.JWTAuth(accountRepository, cache))
|
||||||
@@ -65,7 +65,7 @@ func SetRouter(db *gorm.DB, cache *rediscache.Client) *gin.Engine {
|
|||||||
// comment
|
// comment
|
||||||
commentRepository := video.NewCommentRepository(db)
|
commentRepository := video.NewCommentRepository(db)
|
||||||
commentService := video.NewCommentService(commentRepository, videoRepository)
|
commentService := video.NewCommentService(commentRepository, videoRepository)
|
||||||
commentHandler := video.NewCommentHandler(commentService, accountService)
|
commentHandler := video.NewCommentHandler(commentService, accountService, videoService)
|
||||||
commentGroup := r.Group("/comment")
|
commentGroup := r.Group("/comment")
|
||||||
{
|
{
|
||||||
commentGroup.POST("/listAll", commentHandler.GetAllComments)
|
commentGroup.POST("/listAll", commentHandler.GetAllComments)
|
||||||
@@ -98,6 +98,7 @@ func SetRouter(db *gorm.DB, cache *rediscache.Client) *gin.Engine {
|
|||||||
{
|
{
|
||||||
feedGroup.POST("/listLatest", feedHandler.ListLatest)
|
feedGroup.POST("/listLatest", feedHandler.ListLatest)
|
||||||
feedGroup.POST("/listLikesCount", feedHandler.ListLikesCount)
|
feedGroup.POST("/listLikesCount", feedHandler.ListLikesCount)
|
||||||
|
feedGroup.POST("/listByPopularity", feedHandler.ListByPopularity)
|
||||||
}
|
}
|
||||||
protectedFeedGroup := feedGroup.Group("")
|
protectedFeedGroup := feedGroup.Group("")
|
||||||
protectedFeedGroup.Use(middleware.JWTAuth(accountRepository, cache))
|
protectedFeedGroup.Use(middleware.JWTAuth(accountRepository, cache))
|
||||||
|
|||||||
@@ -103,3 +103,54 @@ func (c *Client) Unlock(ctx context.Context, key string, token string) error {
|
|||||||
_, err := unlockScript.Run(ctx, c.rdb, []string{key}, token).Result()
|
_, err := unlockScript.Run(ctx, c.rdb, []string{key}, token).Result()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) ZincrBy(ctx context.Context, key string, member string, score float64) error {
|
||||||
|
if c == nil || c.rdb == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return c.rdb.ZIncrBy(ctx, key, score, member).Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Expire(ctx context.Context, key string, ttl time.Duration) error {
|
||||||
|
if c == nil || c.rdb == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return c.rdb.Expire(ctx, key, ttl).Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) ZUnionStore(ctx context.Context, dst string, keys []string, aggregate string) error {
|
||||||
|
if c == nil || c.rdb == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return c.rdb.ZUnionStore(ctx, dst, &redis.ZStore{
|
||||||
|
Keys: keys,
|
||||||
|
Aggregate: aggregate,
|
||||||
|
}).Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Exists(ctx context.Context, key string) (bool, error) {
|
||||||
|
if c == nil || c.rdb == nil {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
n, err := c.rdb.Exists(ctx, key).Result()
|
||||||
|
return n > 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) ZRevRange(ctx context.Context, key string, start, stop int64) ([]string, error) {
|
||||||
|
if c == nil || c.rdb == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return c.rdb.ZRevRange(ctx, key, start, stop).Result()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) ZRevRangeByScore(ctx context.Context, key string, max, min string, offset, count int64) ([]string, error) {
|
||||||
|
if c == nil || c.rdb == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return c.rdb.ZRevRangeByScore(ctx, key, &redis.ZRangeBy{
|
||||||
|
Max: max,
|
||||||
|
Min: min,
|
||||||
|
Offset: offset,
|
||||||
|
Count: count,
|
||||||
|
}).Result()
|
||||||
|
}
|
||||||
@@ -10,10 +10,11 @@ import (
|
|||||||
type CommentHandler struct {
|
type CommentHandler struct {
|
||||||
service *CommentService
|
service *CommentService
|
||||||
accountService *account.AccountService
|
accountService *account.AccountService
|
||||||
|
videoService *VideoService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCommentHandler(service *CommentService, accountService *account.AccountService) *CommentHandler {
|
func NewCommentHandler(service *CommentService, accountService *account.AccountService, videoService *VideoService) *CommentHandler {
|
||||||
return &CommentHandler{service: service, accountService: accountService}
|
return &CommentHandler{service: service, accountService: accountService, videoService: videoService}
|
||||||
}
|
}
|
||||||
func (h *CommentHandler) PublishComment(c *gin.Context) {
|
func (h *CommentHandler) PublishComment(c *gin.Context) {
|
||||||
var req PublishCommentRequest
|
var req PublishCommentRequest
|
||||||
@@ -49,6 +50,10 @@ func (h *CommentHandler) PublishComment(c *gin.Context) {
|
|||||||
c.JSON(400, gin.H{"error": err.Error()})
|
c.JSON(400, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if err := h.videoService.UpdatePopularity(c.Request.Context(), req.VideoID, 1); err != nil {
|
||||||
|
c.JSON(500, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
c.JSON(200, gin.H{"message": "comment published successfully"})
|
c.JSON(200, gin.H{"message": "comment published successfully"})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,6 +76,7 @@ func (h *CommentHandler) DeleteComment(c *gin.Context) {
|
|||||||
c.JSON(400, gin.H{"error": err.Error()})
|
c.JSON(400, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(200, gin.H{"message": "comment deleted successfully"})
|
c.JSON(200, gin.H{"message": "comment deleted successfully"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,3 @@ type Like struct {
|
|||||||
type LikeRequest struct {
|
type LikeRequest struct {
|
||||||
VideoID uint `json:"video_id"`
|
VideoID uint `json:"video_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type LikeHandler struct {
|
|
||||||
service *LikeService
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,8 +6,13 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewLikeHandler(service *LikeService) *LikeHandler {
|
type LikeHandler struct {
|
||||||
return &LikeHandler{service: service}
|
service *LikeService
|
||||||
|
videoService *VideoService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLikeHandler(service *LikeService, videoService *VideoService) *LikeHandler {
|
||||||
|
return &LikeHandler{service: service, videoService: videoService}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (lh *LikeHandler) Like(c *gin.Context) {
|
func (lh *LikeHandler) Like(c *gin.Context) {
|
||||||
@@ -35,6 +40,10 @@ func (lh *LikeHandler) Like(c *gin.Context) {
|
|||||||
c.JSON(500, gin.H{"error": err.Error()})
|
c.JSON(500, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if err := lh.videoService.UpdatePopularity(c.Request.Context(), req.VideoID, 1); err != nil {
|
||||||
|
c.JSON(500, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
c.JSON(200, gin.H{"message": "like success"})
|
c.JSON(200, gin.H{"message": "like success"})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,6 +72,10 @@ func (lh *LikeHandler) Unlike(c *gin.Context) {
|
|||||||
c.JSON(500, gin.H{"error": err.Error()})
|
c.JSON(500, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if err := lh.videoService.UpdatePopularity(c.Request.Context(), req.VideoID, -1); err != nil {
|
||||||
|
c.JSON(500, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
c.JSON(200, gin.H{"message": "unlike success"})
|
c.JSON(200, gin.H{"message": "unlike success"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ type Video struct {
|
|||||||
CoverURL string `gorm:"type:varchar(255);not null" json:"cover_url"`
|
CoverURL string `gorm:"type:varchar(255);not null" json:"cover_url"`
|
||||||
CreateTime time.Time `gorm:"autoCreateTime" json:"create_time"`
|
CreateTime time.Time `gorm:"autoCreateTime" json:"create_time"`
|
||||||
LikesCount int64 `gorm:"column:likes_count;not null;default:0" json:"likes_count"`
|
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 {
|
type PublishVideoRequest struct {
|
||||||
|
|||||||
@@ -68,3 +68,12 @@ func (vr *VideoRepository) IsExist(ctx context.Context, id uint) (bool, error) {
|
|||||||
}
|
}
|
||||||
return true, nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -166,3 +167,26 @@ func (vs *VideoService) UpdateLikesCount(ctx context.Context, id uint, likesCoun
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (vs *VideoService) UpdatePopularity(ctx context.Context, id uint, change int64) error {
|
||||||
|
if err := vs.repo.UpdatePopularity(ctx, id, change); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if vs.cache != nil {
|
||||||
|
// 1) 详情缓存:直接失效(最简单靠谱)
|
||||||
|
_ = vs.cache.Del(context.Background(), fmt.Sprintf("video:detail:id=%d", id))
|
||||||
|
|
||||||
|
// 2) 热榜:写到“时间窗ZSET”,不要用 detail key
|
||||||
|
now := time.Now().UTC().Truncate(time.Minute)
|
||||||
|
windowKey := "hot:video:1m:" + now.Format("200601021504")
|
||||||
|
member := strconv.FormatUint(uint64(id), 10)
|
||||||
|
|
||||||
|
opCtx, cancel := context.WithTimeout(ctx, 50*time.Millisecond)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
_ = vs.cache.ZincrBy(opCtx, windowKey, member, float64(change))
|
||||||
|
_ = vs.cache.Expire(opCtx, windowKey, 2*time.Hour)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user