支持 square/squarenovel/squaresteps/squarehealth 实例与 category 筛选;音乐增加免登录 grouped/track;订阅套餐按 package_name 区分;补 migration 0030/0031 与 OpenAPI。 Co-authored-by: Cursor <cursoragent@cursor.com>
328 lines
7.9 KiB
Go
328 lines
7.9 KiB
Go
package model
|
|
|
|
import (
|
|
"context"
|
|
"math"
|
|
"sort"
|
|
"sync"
|
|
"time"
|
|
|
|
"spark-server/internal/module/square/entity"
|
|
"spark-server/internal/pkg/errs"
|
|
)
|
|
|
|
type likeKey struct {
|
|
PostID int64
|
|
UserID int64
|
|
}
|
|
|
|
type noticeStateKey struct {
|
|
UserID int64
|
|
Channel entity.Channel
|
|
}
|
|
|
|
// MemoryRepo is an in-process square store for tests and empty-DSN bring-up.
|
|
type MemoryRepo struct {
|
|
mu sync.Mutex
|
|
posts map[int64]*entity.Post
|
|
audience map[int64][]int64
|
|
likes map[likeKey]*entity.Like
|
|
comments map[int64]*entity.Comment
|
|
notices map[int64]*entity.Notice
|
|
noticeState map[noticeStateKey]*entity.NoticeState
|
|
botLinks map[string]int64 // url -> post_id
|
|
}
|
|
|
|
func NewMemoryRepo() *MemoryRepo {
|
|
return &MemoryRepo{
|
|
posts: make(map[int64]*entity.Post),
|
|
audience: make(map[int64][]int64),
|
|
likes: make(map[likeKey]*entity.Like),
|
|
comments: make(map[int64]*entity.Comment),
|
|
notices: make(map[int64]*entity.Notice),
|
|
noticeState: make(map[noticeStateKey]*entity.NoticeState),
|
|
botLinks: make(map[string]int64),
|
|
}
|
|
}
|
|
|
|
func memChannel(ch entity.Channel) entity.Channel {
|
|
if ch == "" {
|
|
return entity.ChannelSquare
|
|
}
|
|
return ch
|
|
}
|
|
|
|
func (m *MemoryRepo) GetPost(_ context.Context, id int64) (*entity.Post, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
row, ok := m.posts[id]
|
|
if !ok {
|
|
return nil, errs.ErrNotFound
|
|
}
|
|
return clonePost(row), nil
|
|
}
|
|
|
|
func (m *MemoryRepo) ListByAuthors(_ context.Context, channel entity.Channel, authorIDs []int64, cursor int64, limit int) ([]*entity.Post, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
ch := memChannel(channel)
|
|
allow := toSet(authorIDs)
|
|
nowMS := time.Now().UnixMilli()
|
|
out := make([]*entity.Post, 0)
|
|
for _, row := range m.posts {
|
|
if row.Status != entity.StatusNormal || !allow[row.UserID] || memChannel(row.Channel) != ch {
|
|
continue
|
|
}
|
|
if row.Expired(nowMS) {
|
|
continue
|
|
}
|
|
if cursor > 0 && row.ID >= cursor {
|
|
continue
|
|
}
|
|
out = append(out, clonePost(row))
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].ID > out[j].ID })
|
|
if limit > 0 && len(out) > limit {
|
|
out = out[:limit]
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (m *MemoryRepo) ListByAuthor(ctx context.Context, channel entity.Channel, authorID, cursor int64, limit int) ([]*entity.Post, error) {
|
|
return m.ListByAuthors(ctx, channel, []int64{authorID}, cursor, limit)
|
|
}
|
|
|
|
func (m *MemoryRepo) ListAudience(_ context.Context, postIDs []int64) (map[int64][]int64, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
allow := toSet(postIDs)
|
|
out := make(map[int64][]int64)
|
|
for id, peers := range m.audience {
|
|
if !allow[id] {
|
|
continue
|
|
}
|
|
out[id] = append([]int64(nil), peers...)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (m *MemoryRepo) ListLikes(_ context.Context, postIDs []int64) (map[int64][]*entity.Like, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
allow := toSet(postIDs)
|
|
out := make(map[int64][]*entity.Like)
|
|
for _, like := range m.likes {
|
|
if !allow[like.PostID] {
|
|
continue
|
|
}
|
|
cp := *like
|
|
out[like.PostID] = append(out[like.PostID], &cp)
|
|
}
|
|
for id := range out {
|
|
sort.Slice(out[id], func(i, j int) bool { return out[id][i].CreatedAtMS < out[id][j].CreatedAtMS })
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (m *MemoryRepo) ListComments(_ context.Context, postIDs []int64) (map[int64][]*entity.Comment, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
allow := toSet(postIDs)
|
|
out := make(map[int64][]*entity.Comment)
|
|
for _, c := range m.comments {
|
|
if !allow[c.PostID] {
|
|
continue
|
|
}
|
|
out[c.PostID] = append(out[c.PostID], cloneComment(c))
|
|
}
|
|
for id := range out {
|
|
sort.Slice(out[id], func(i, j int) bool { return out[id][i].ID < out[id][j].ID })
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (m *MemoryRepo) GetComment(_ context.Context, id int64) (*entity.Comment, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
row, ok := m.comments[id]
|
|
if !ok {
|
|
return nil, errs.ErrNotFound
|
|
}
|
|
return cloneComment(row), nil
|
|
}
|
|
|
|
func (m *MemoryRepo) ListPublicFeed(_ context.Context, channel entity.Channel, q entity.FeedListQuery) ([]*entity.Post, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
nowMS := time.Now().UnixMilli()
|
|
ch := memChannel(channel)
|
|
out := make([]*entity.Post, 0)
|
|
for _, row := range m.posts {
|
|
if row.Status != entity.StatusNormal || row.AuditStatus != entity.AuditApproved {
|
|
continue
|
|
}
|
|
if memChannel(row.Channel) != ch {
|
|
continue
|
|
}
|
|
if row.Visibility != entity.VisibilityPublic && row.Visibility != entity.VisibilityExclude {
|
|
continue
|
|
}
|
|
if row.Expired(nowMS) {
|
|
continue
|
|
}
|
|
if q.Category.IsTag() && row.Category != q.Category {
|
|
continue
|
|
}
|
|
if q.Category == entity.CategoryNearby && row.POI.Lat == 0 && row.POI.Lng == 0 {
|
|
continue
|
|
}
|
|
out = append(out, clonePost(row))
|
|
}
|
|
switch q.Category {
|
|
case entity.CategoryHot:
|
|
sort.Slice(out, func(i, j int) bool {
|
|
if out[i].LikeCount == out[j].LikeCount {
|
|
return out[i].ID > out[j].ID
|
|
}
|
|
return out[i].LikeCount > out[j].LikeCount
|
|
})
|
|
if q.Cursor > 0 {
|
|
var lc int
|
|
var found bool
|
|
for _, p := range out {
|
|
if p.ID == q.Cursor {
|
|
lc = p.LikeCount
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
filtered := make([]*entity.Post, 0, len(out))
|
|
for _, p := range out {
|
|
if !found {
|
|
if p.ID < q.Cursor {
|
|
filtered = append(filtered, p)
|
|
}
|
|
continue
|
|
}
|
|
if p.LikeCount < lc || (p.LikeCount == lc && p.ID < q.Cursor) {
|
|
filtered = append(filtered, p)
|
|
}
|
|
}
|
|
out = filtered
|
|
}
|
|
case entity.CategoryNearby:
|
|
if q.Lat != 0 || q.Lng != 0 {
|
|
sort.SliceStable(out, func(i, j int) bool {
|
|
di := math.Abs(q.Lat-out[i].POI.Lat) + math.Abs(q.Lng-out[i].POI.Lng)
|
|
dj := math.Abs(q.Lat-out[j].POI.Lat) + math.Abs(q.Lng-out[j].POI.Lng)
|
|
if di == dj {
|
|
return out[i].ID > out[j].ID
|
|
}
|
|
return di < dj
|
|
})
|
|
} else {
|
|
sort.Slice(out, func(i, j int) bool { return out[i].ID > out[j].ID })
|
|
}
|
|
if q.Cursor > 0 {
|
|
filtered := make([]*entity.Post, 0, len(out))
|
|
for _, p := range out {
|
|
if p.ID < q.Cursor {
|
|
filtered = append(filtered, p)
|
|
}
|
|
}
|
|
out = filtered
|
|
}
|
|
default:
|
|
sort.Slice(out, func(i, j int) bool { return out[i].ID > out[j].ID })
|
|
if q.Cursor > 0 {
|
|
filtered := make([]*entity.Post, 0, len(out))
|
|
for _, p := range out {
|
|
if p.ID < q.Cursor {
|
|
filtered = append(filtered, p)
|
|
}
|
|
}
|
|
out = filtered
|
|
}
|
|
}
|
|
if q.Limit > 0 && len(out) > q.Limit {
|
|
out = out[:q.Limit]
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (m *MemoryRepo) GetNoticeState(_ context.Context, userID int64, channel entity.Channel) (*entity.NoticeState, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
ch := memChannel(channel)
|
|
if row, ok := m.noticeState[noticeStateKey{UserID: userID, Channel: ch}]; ok {
|
|
cp := *row
|
|
return &cp, nil
|
|
}
|
|
return entity.DefaultNoticeState(userID, ch), nil
|
|
}
|
|
|
|
func (m *MemoryRepo) LatestNoticeID(_ context.Context, userID int64, channel entity.Channel) (int64, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
ch := memChannel(channel)
|
|
var latest int64
|
|
for _, n := range m.notices {
|
|
if n.ToUserID == userID && memChannel(n.Channel) == ch && n.ID > latest {
|
|
latest = n.ID
|
|
}
|
|
}
|
|
return latest, nil
|
|
}
|
|
|
|
func (m *MemoryRepo) ListNotices(_ context.Context, userID int64, channel entity.Channel, cursor int64, limit int) ([]*entity.Notice, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
ch := memChannel(channel)
|
|
out := make([]*entity.Notice, 0)
|
|
for _, n := range m.notices {
|
|
if n.ToUserID != userID || memChannel(n.Channel) != ch {
|
|
continue
|
|
}
|
|
if cursor > 0 && n.ID >= cursor {
|
|
continue
|
|
}
|
|
cp := *n
|
|
out = append(out, &cp)
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].ID > out[j].ID })
|
|
if limit > 0 && len(out) > limit {
|
|
out = out[:limit]
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func toSet(ids []int64) map[int64]bool {
|
|
out := make(map[int64]bool, len(ids))
|
|
for _, id := range ids {
|
|
out[id] = true
|
|
}
|
|
return out
|
|
}
|
|
|
|
func clonePost(p *entity.Post) *entity.Post {
|
|
if p == nil {
|
|
return nil
|
|
}
|
|
cp := *p
|
|
if p.Content != nil {
|
|
cp.Content = append([]byte(nil), p.Content...)
|
|
}
|
|
if cp.Channel == "" {
|
|
cp.Channel = entity.ChannelSquare
|
|
}
|
|
return &cp
|
|
}
|
|
|
|
func cloneComment(c *entity.Comment) *entity.Comment {
|
|
if c == nil {
|
|
return nil
|
|
}
|
|
cp := *c
|
|
return &cp
|
|
}
|