feat(hot-rank): add per-channel hot ranking functionality
Some checks failed
Build Backend / build (push) Failing after 1m52s
Build Backend / build-docker (push) Has been skipped

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:
lafay
2026-04-26 11:39:41 +08:00
parent 02466603f9
commit 27ea8689f9
7 changed files with 329 additions and 70 deletions

View File

@@ -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).