合并 origin/main 设备注册后,纳入本地 music 模块、seed 命令与 popadmin 相关路由/文档;music migration 编号为 0020 以避免与 0019_devices 冲突。 Co-authored-by: Cursor <cursoragent@cursor.com>
238 lines
5.4 KiB
Go
238 lines
5.4 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])
|
|
}
|
|
|
|
// 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
|
|
}
|