package model import ( "context" "sort" "sync" "time" "spark-server/internal/module/square/entity" "spark-server/internal/pkg/errs" ) type likeKey struct { PostID int64 UserID int64 } // 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[int64]*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[int64]*entity.NoticeState), botLinks: make(map[string]int64), } } 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, authorIDs []int64, cursor int64, limit int) ([]*entity.Post, error) { m.mu.Lock() defer m.mu.Unlock() 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] { 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, authorID, cursor int64, limit int) ([]*entity.Post, error) { return m.ListByAuthors(ctx, []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, cursor int64, limit int) ([]*entity.Post, error) { m.mu.Lock() defer m.mu.Unlock() nowMS := time.Now().UnixMilli() out := make([]*entity.Post, 0) for _, row := range m.posts { if row.Status != entity.StatusNormal || row.AuditStatus != entity.AuditApproved { continue } if row.Visibility != entity.VisibilityPublic && row.Visibility != entity.VisibilityExclude { 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) GetNoticeState(_ context.Context, userID int64) (*entity.NoticeState, error) { m.mu.Lock() defer m.mu.Unlock() if row, ok := m.noticeState[userID]; ok { cp := *row return &cp, nil } return entity.DefaultNoticeState(userID), nil } func (m *MemoryRepo) LatestNoticeID(_ context.Context, userID int64) (int64, error) { m.mu.Lock() defer m.mu.Unlock() var latest int64 for _, n := range m.notices { if n.ToUserID == userID && n.ID > latest { latest = n.ID } } return latest, nil } func (m *MemoryRepo) ListNotices(_ context.Context, userID, cursor int64, limit int) ([]*entity.Notice, error) { m.mu.Lock() defer m.mu.Unlock() out := make([]*entity.Notice, 0) for _, n := range m.notices { if n.ToUserID != userID { 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...) } return &cp } func cloneComment(c *entity.Comment) *entity.Comment { if c == nil { return nil } cp := *c return &cp }