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]) } // FindByOwnerAndName returns the active group owned by ownerID with the given name. func (m *MemoryRepo) FindByOwnerAndName(_ context.Context, ownerID int64, name string) (*entity.Group, error) { if ownerID <= 0 || name == "" { return nil, errs.ErrInvalidArgument } m.mu.Lock() defer m.mu.Unlock() var best *entity.Group for _, g := range m.groups { if g == nil || g.OwnerID != ownerID || g.Name != name || g.Status != entity.StatusActive { continue } if best == nil || g.ID < best.ID { cp := *g best = &cp } } if best == nil { return nil, errs.ErrNotFound } return best, nil } 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) ListAllActiveByUser(_ context.Context, userID int64) ([]*entity.Group, error) { m.mu.Lock() defer m.mu.Unlock() 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 }) 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 } if out[i].JoinedAtMS != out[j].JoinedAtMS { return out[i].JoinedAtMS < out[j].JoinedAtMS } return out[i].UserID < out[j].UserID }) 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.Announcement = g.Announcement 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.Remark = mem.Remark 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 }