spark-server/internal/module/group/model/memory.go
zhr ae9a71034d feat: implement Spark IM V3 modular monolith
Add HTTP /v1 MVC, TCP send path, groups with server-side member-avatar collage, object-storage upload, and friends/favorites around the architecture-v3 layout.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-17 19:52:11 +08:00

191 lines
4.2 KiB
Go

package model
import (
"context"
"sort"
"sync"
"time"
"spark-server/internal/module/group/entity"
"spark-server/internal/pkg/errs"
)
type lastMsg struct {
ID int64
At int64
}
// MemoryRepo is an in-process group store for tests and empty-DSN bring-up.
type MemoryRepo struct {
mu sync.Mutex
nextID int64
groups map[int64]*entity.Group
byConv map[int64]int64
members map[int64]map[int64]*entity.Member
last map[int64]lastMsg
}
func NewMemoryRepo() *MemoryRepo {
return &MemoryRepo{
groups: make(map[int64]*entity.Group),
byConv: make(map[int64]int64),
members: make(map[int64]map[int64]*entity.Member),
last: make(map[int64]lastMsg),
}
}
func (m *MemoryRepo) Get(_ context.Context, id int64) (*entity.Group, error) {
m.mu.Lock()
defer m.mu.Unlock()
return copyGroup(m.groups[id])
}
func (m *MemoryRepo) GetByConversation(_ context.Context, conversationID int64) (*entity.Group, error) {
m.mu.Lock()
defer m.mu.Unlock()
id, ok := m.byConv[conversationID]
if !ok {
return nil, errs.ErrNotFound
}
return copyGroup(m.groups[id])
}
func (m *MemoryRepo) ListByUser(_ context.Context, userID int64, limit int) ([]*entity.Group, error) {
m.mu.Lock()
defer m.mu.Unlock()
limit = entity.ClampLimit(limit)
out := make([]*entity.Group, 0)
for gid, byUser := range m.members {
if _, ok := byUser[userID]; !ok {
continue
}
g, ok := m.groups[gid]
if !ok || g.Status != entity.StatusActive {
continue
}
cp := *g
out = append(out, &cp)
}
sort.Slice(out, func(i, j int) bool { return out[i].UpdatedAtMS > out[j].UpdatedAtMS })
if len(out) > limit {
out = out[:limit]
}
return out, nil
}
func (m *MemoryRepo) GetMember(_ context.Context, groupID, userID int64) (*entity.Member, error) {
m.mu.Lock()
defer m.mu.Unlock()
byUser, ok := m.members[groupID]
if !ok {
return nil, errs.ErrNotFound
}
mem, ok := byUser[userID]
if !ok {
return nil, errs.ErrNotFound
}
cp := *mem
return &cp, nil
}
func (m *MemoryRepo) ListMembers(_ context.Context, groupID int64, limit int) ([]*entity.Member, error) {
m.mu.Lock()
defer m.mu.Unlock()
byUser, ok := m.members[groupID]
if !ok {
return nil, errs.ErrNotFound
}
limit = entity.ClampMemberLimit(limit)
out := make([]*entity.Member, 0, len(byUser))
for _, mem := range byUser {
cp := *mem
out = append(out, &cp)
}
sort.Slice(out, func(i, j int) bool {
if out[i].Role != out[j].Role {
return out[i].Role < out[j].Role
}
return out[i].JoinedAtMS < out[j].JoinedAtMS
})
if len(out) > limit {
out = out[:limit]
}
return out, nil
}
func (m *MemoryRepo) ListMemberIDs(_ context.Context, groupID int64) ([]int64, error) {
m.mu.Lock()
defer m.mu.Unlock()
byUser, ok := m.members[groupID]
if !ok {
return nil, errs.ErrNotFound
}
ids := make([]int64, 0, len(byUser))
for uid := range byUser {
ids = append(ids, uid)
}
sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
return ids, nil
}
func (m *MemoryRepo) UpdateProfile(_ context.Context, g *entity.Group) error {
if g == nil {
return errs.ErrInvalidArgument
}
m.mu.Lock()
defer m.mu.Unlock()
cur, ok := m.groups[g.ID]
if !ok || cur.Status != entity.StatusActive {
return errs.ErrNotFound
}
cur.Name = g.Name
cur.Avatar = g.Avatar
cur.MutedAll = g.MutedAll
cur.UpdatedAtMS = g.UpdatedAtMS
if cur.UpdatedAtMS <= 0 {
cur.UpdatedAtMS = time.Now().UnixMilli()
}
return nil
}
func (m *MemoryRepo) UpdateMember(_ context.Context, mem *entity.Member) error {
if mem == nil {
return errs.ErrInvalidArgument
}
m.mu.Lock()
defer m.mu.Unlock()
byUser, ok := m.members[mem.GroupID]
if !ok {
return errs.ErrNotFound
}
cur, ok := byUser[mem.UserID]
if !ok {
return errs.ErrNotFound
}
cur.Role = mem.Role
cur.Nickname = mem.Nickname
cur.SpeakMuted = mem.SpeakMuted
return nil
}
func (m *MemoryRepo) UpdateLastMsg(_ context.Context, conversationID, msgID, atMS int64) error {
m.mu.Lock()
defer m.mu.Unlock()
if _, ok := m.byConv[conversationID]; !ok {
return errs.ErrNotFound
}
if atMS <= 0 {
atMS = time.Now().UnixMilli()
}
m.last[conversationID] = lastMsg{ID: msgID, At: atMS}
return nil
}
func copyGroup(g *entity.Group) (*entity.Group, error) {
if g == nil {
return nil, errs.ErrNotFound
}
cp := *g
return &cp, nil
}