spark-server/internal/module/square/logic/feed.go
DESKTOP-L8OUL9F\gfl0034 53426af122 feat: 多广场 channel、音乐公开 API 与订阅 package_name
支持 square/squarenovel/squaresteps/squarehealth 实例与 category 筛选;音乐增加免登录 grouped/track;订阅套餐按 package_name 区分;补 migration 0030/0031 与 OpenAPI。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-30 09:02:28 +08:00

96 lines
2.3 KiB
Go

package logic
import (
"context"
"spark-server/internal/module/square/entity"
"spark-server/internal/pkg/errs"
)
// FeedQuery is list filter/sort for a plaza channel.
type FeedQuery struct {
Cursor int64
Limit int
Category entity.Category
Lat float64
Lng float64
}
func (s *Service) Feed(ctx context.Context, viewerID int64, channel entity.Channel, q FeedQuery) (*ListResult, error) {
if !s.ready() || viewerID <= 0 {
return nil, errs.ErrUnauthorized
}
channel = normalizeChannel(channel)
if !channel.Valid() {
return nil, errs.ErrInvalidArgument
}
if !q.Category.Valid() {
return nil, errs.ErrInvalidArgument
}
limit := entity.ClampLimit(q.Limit)
picked := make([]*entity.Post, 0, limit)
scan := q.Cursor
for range entity.OverfetchRounds {
batch, err := s.repo.ListPublicFeed(ctx, channel, entity.FeedListQuery{
Cursor: scan, Limit: entity.Overfetch, Category: q.Category, Lat: q.Lat, Lng: q.Lng,
})
if err != nil {
return nil, err
}
if len(batch) == 0 {
break
}
vis, err := s.visFor(ctx, viewerID, batch)
if err != nil {
return nil, err
}
for _, p := range batch {
scan = p.ID
if !vis.canSee(p) {
continue
}
picked = append(picked, p)
if len(picked) >= limit {
break
}
}
if len(picked) >= limit || len(batch) < entity.Overfetch {
break
}
}
return s.listResult(ctx, viewerID, picked, limit, false)
}
func (s *Service) listResult(ctx context.Context, viewer int64, posts []*entity.Post, limit int, full bool) (*ListResult, error) {
views, err := s.attachViews(ctx, viewer, posts, full)
if err != nil {
return nil, err
}
next := int64(0)
if len(posts) > 0 {
next = posts[len(posts)-1].ID
}
hasMore := len(posts) >= limit && limit > 0
return &ListResult{Items: views, NextCursor: next, HasMore: hasMore}, nil
}
// CategoryItem is one filter tab for clients.
type CategoryItem struct {
Value entity.Category `json:"value"`
Kind string `json:"kind"` // feed | tag
LabelKey string `json:"-"`
}
// Categories returns filter tabs (feed modes + tags) shared by every channel.
func (s *Service) Categories() []CategoryItem {
out := make([]CategoryItem, 0, len(entity.Categories()))
for _, c := range entity.Categories() {
kind := "feed"
if c.IsTag() {
kind = "tag"
}
out = append(out, CategoryItem{Value: c, Kind: kind})
}
return out
}