spark-server/internal/module/square/model/memory.go
DESKTOP-L8OUL9F\gfl0034 715ace7ac6 feat(square): 广场 Bot/RSS 发布与媒体校验
支持广场机器人发帖与媒体类型检查,并补充相关迁移与 i18n。
2026-09-24 23:07:06 +08:00

236 lines
5.6 KiB
Go

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
}