feat(hot-rank): add per-channel hot ranking functionality
Add channel-specific hot post filtering and ranking across the posts listing flow. Introduce refreshChannelRanks to compute per-channel hot scores, new Redis cache keys for channel hot ranks, and update repository/service/handler layers to accept channelID filtering parameters. Update HotRankWorker dependency injection to include ChannelRepository.
This commit is contained in:
@@ -33,10 +33,12 @@ type PostRepository interface {
|
||||
IncrementViews(postID string) error
|
||||
IncrementShares(postID string) (int, error)
|
||||
Search(keyword string, page, pageSize int) ([]*model.Post, int64, error)
|
||||
GetFollowingPosts(userID string, page, pageSize int) ([]*model.Post, int64, error)
|
||||
GetHotPosts(page, pageSize int) ([]*model.Post, int64, error)
|
||||
GetFollowingPosts(userID string, page, pageSize int, channelID *string) ([]*model.Post, int64, error)
|
||||
GetHotPosts(page, pageSize int, channelID *string) ([]*model.Post, int64, error)
|
||||
ListPublishedPostIDsSince(since time.Time, limit int) ([]string, error)
|
||||
ListPublishedPostIDsByChannel(channelID string, recentHours int, limit int) ([]string, error)
|
||||
ListPinnedPublishedPostIDs() ([]string, error)
|
||||
ListPinnedPublishedPostIDsByChannel(channelID string) ([]string, error)
|
||||
GetPostHotSnapshotsByIDs(ids []string) ([]PostHotSnapshot, error)
|
||||
GetByIDs(ids []string) ([]*model.Post, error)
|
||||
CreateWithContext(ctx context.Context, post *model.Post, images []string) error
|
||||
@@ -52,8 +54,8 @@ type PostRepository interface {
|
||||
GetPostsByCursor(ctx context.Context, userID string, includePending bool, channelID *string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Post], error)
|
||||
SearchPostsByCursor(ctx context.Context, keyword string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Post], error)
|
||||
GetUserPostsByCursor(ctx context.Context, userID string, includePending bool, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Post], error)
|
||||
GetFollowingPostsByCursor(ctx context.Context, userID string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Post], error)
|
||||
GetHotPostsByCursor(ctx context.Context, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Post], error)
|
||||
GetFollowingPostsByCursor(ctx context.Context, userID string, channelID *string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Post], error)
|
||||
GetHotPostsByCursor(ctx context.Context, channelID *string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Post], error)
|
||||
}
|
||||
|
||||
// postRepository 帖子仓储实现
|
||||
@@ -528,18 +530,21 @@ func (r *postRepository) Search(keyword string, page, pageSize int) ([]*model.Po
|
||||
}
|
||||
|
||||
// GetFollowingPosts 获取关注用户的帖子
|
||||
func (r *postRepository) GetFollowingPosts(userID string, page, pageSize int) ([]*model.Post, int64, error) {
|
||||
func (r *postRepository) GetFollowingPosts(userID string, page, pageSize int, channelID *string) ([]*model.Post, int64, error) {
|
||||
var posts []*model.Post
|
||||
var total int64
|
||||
|
||||
// 子查询:获取当前用户关注的所有用户ID
|
||||
subQuery := r.db.Model(&model.Follow{}).Where("follower_id = ?", userID).Select("following_id")
|
||||
|
||||
// 统计总数
|
||||
r.db.Model(&model.Post{}).Where("user_id IN (?) AND status = ?", subQuery, model.PostStatusPublished).Count(&total)
|
||||
baseQuery := r.db.Model(&model.Post{}).Where("user_id IN (?) AND status = ?", subQuery, model.PostStatusPublished)
|
||||
if channelID != nil && *channelID != "" {
|
||||
baseQuery = baseQuery.Where("channel_id = ?", *channelID)
|
||||
}
|
||||
|
||||
baseQuery.Count(&total)
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
err := r.db.Where("user_id IN (?) AND status = ?", subQuery, model.PostStatusPublished).
|
||||
err := baseQuery.
|
||||
Preload("User").Preload("Images").
|
||||
Offset(offset).Limit(pageSize).
|
||||
Order("created_at DESC").
|
||||
@@ -549,14 +554,18 @@ func (r *postRepository) GetFollowingPosts(userID string, page, pageSize int) ([
|
||||
}
|
||||
|
||||
// GetHotPosts 热门榜降级:Redis 未就绪时按最新发布排序
|
||||
func (r *postRepository) GetHotPosts(page, pageSize int) ([]*model.Post, int64, error) {
|
||||
func (r *postRepository) GetHotPosts(page, pageSize int, channelID *string) ([]*model.Post, int64, error) {
|
||||
var posts []*model.Post
|
||||
var total int64
|
||||
|
||||
r.db.Model(&model.Post{}).Where("status = ?", model.PostStatusPublished).Count(&total)
|
||||
baseQuery := r.db.Model(&model.Post{}).Where("status = ?", model.PostStatusPublished)
|
||||
if channelID != nil && *channelID != "" {
|
||||
baseQuery = baseQuery.Where("channel_id = ?", *channelID)
|
||||
}
|
||||
baseQuery.Count(&total)
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
err := r.db.Where("status = ?", model.PostStatusPublished).Preload("User").Preload("Images").
|
||||
err := baseQuery.Preload("User").Preload("Images").
|
||||
Offset(offset).Limit(pageSize).
|
||||
Order("created_at DESC").
|
||||
Find(&posts).Error
|
||||
@@ -625,6 +634,58 @@ func (r *postRepository) ListPinnedPublishedPostIDs() ([]string, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (r *postRepository) ListPublishedPostIDsByChannel(channelID string, recentHours int, limit int) ([]string, error) {
|
||||
if limit <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
type row struct {
|
||||
ID string `gorm:"column:id"`
|
||||
}
|
||||
var rows []row
|
||||
q := r.db.Model(&model.Post{}).
|
||||
Select("id").
|
||||
Where("status = ? AND channel_id = ?", model.PostStatusPublished, channelID)
|
||||
if recentHours > 0 {
|
||||
since := time.Now().Add(-time.Duration(recentHours) * time.Hour)
|
||||
q = q.Where("created_at >= ?", since)
|
||||
}
|
||||
err := q.Order("created_at DESC").
|
||||
Limit(limit).
|
||||
Find(&rows).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]string, 0, len(rows))
|
||||
for _, x := range rows {
|
||||
if x.ID != "" {
|
||||
out = append(out, x.ID)
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (r *postRepository) ListPinnedPublishedPostIDsByChannel(channelID string) ([]string, error) {
|
||||
type row struct {
|
||||
ID string `gorm:"column:id"`
|
||||
}
|
||||
var rows []row
|
||||
err := r.db.Model(&model.Post{}).
|
||||
Select("id").
|
||||
Where("status = ? AND is_pinned = ? AND channel_id = ?", model.PostStatusPublished, true, channelID).
|
||||
Order("created_at DESC").
|
||||
Find(&rows).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]string, 0, len(rows))
|
||||
for _, x := range rows {
|
||||
if x.ID != "" {
|
||||
out = append(out, x.ID)
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// GetPostHotSnapshotsByIDs 仅拉取候选 ID 对应的热度字段(分批 IN 查询)
|
||||
func (r *postRepository) GetPostHotSnapshotsByIDs(ids []string) ([]PostHotSnapshot, error) {
|
||||
if len(ids) == 0 {
|
||||
@@ -1187,14 +1248,15 @@ func (r *postRepository) GetUserPostsByCursor(ctx context.Context, userID string
|
||||
}
|
||||
|
||||
// GetFollowingPostsByCursor 游标分页获取关注用户的帖子
|
||||
func (r *postRepository) GetFollowingPostsByCursor(ctx context.Context, userID string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Post], error) {
|
||||
func (r *postRepository) GetFollowingPostsByCursor(ctx context.Context, userID string, channelID *string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Post], error) {
|
||||
db := r.getDB(ctx).WithContext(ctx)
|
||||
|
||||
// 子查询:获取当前用户关注的所有用户ID
|
||||
subQuery := db.Model(&model.Follow{}).Where("follower_id = ?", userID).Select("following_id")
|
||||
|
||||
// 构建基础查询
|
||||
query := db.Model(&model.Post{}).Where("user_id IN (?) AND status = ?", subQuery, model.PostStatusPublished)
|
||||
if channelID != nil && *channelID != "" {
|
||||
query = query.Where("channel_id = ?", *channelID)
|
||||
}
|
||||
|
||||
// 使用游标构建器
|
||||
builder := cursor.NewBuilder(query, cursor.SortByCreatedAtDesc).
|
||||
@@ -1243,12 +1305,14 @@ func (r *postRepository) GetFollowingPostsByCursor(ctx context.Context, userID s
|
||||
}
|
||||
|
||||
// GetHotPostsByCursor 游标分页获取热门帖子
|
||||
func (r *postRepository) GetHotPostsByCursor(ctx context.Context, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Post], error) {
|
||||
func (r *postRepository) GetHotPostsByCursor(ctx context.Context, channelID *string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Post], error) {
|
||||
db := r.getDB(ctx).WithContext(ctx)
|
||||
|
||||
// 热门游标降级:与 GetHotPosts 一致,按最新发布排序
|
||||
query := db.Model(&model.Post{}).Where("status = ?", model.PostStatusPublished).
|
||||
Order("created_at DESC")
|
||||
query := db.Model(&model.Post{}).Where("status = ?", model.PostStatusPublished)
|
||||
if channelID != nil && *channelID != "" {
|
||||
query = query.Where("channel_id = ?", *channelID)
|
||||
}
|
||||
query = query.Order("created_at DESC")
|
||||
|
||||
// 使用游标构建器
|
||||
builder := cursor.NewBuilder(query, cursor.SortByCreatedAtDesc).
|
||||
|
||||
Reference in New Issue
Block a user