owpengram-server/internal/store/memory/themes.go

210 lines
4.7 KiB
Go

package memory
import (
"context"
"sort"
"sync"
"telesrv/internal/domain"
)
// ThemeStore 是 store.ThemeStore 的内存实现(测试/单实例)。
type ThemeStore struct {
mu sync.RWMutex
byID map[int64]domain.Theme
bySlug map[string]int64
installs map[int64]map[int64]themeInstall // userID -> themeID -> 安装项
seq int64 // 单调序,模拟 postgres installed_at 排序
}
type themeInstall struct {
dark bool
order int64
}
// NewThemeStore 创建内存主题 store。
func NewThemeStore() *ThemeStore {
return &ThemeStore{
byID: make(map[int64]domain.Theme),
bySlug: make(map[string]int64),
installs: make(map[int64]map[int64]themeInstall),
}
}
func (s *ThemeStore) CreateTheme(_ context.Context, t domain.Theme) error {
if t.ID == 0 || t.CreatorUserID == 0 {
return domain.ErrThemeInvalid
}
s.mu.Lock()
defer s.mu.Unlock()
if _, ok := s.byID[t.ID]; ok {
return domain.ErrThemeInvalid
}
if t.Slug != "" {
if _, ok := s.bySlug[t.Slug]; ok {
return domain.ErrThemeSlugTaken
}
}
s.byID[t.ID] = t.Clone()
if t.Slug != "" {
s.bySlug[t.Slug] = t.ID
}
return nil
}
func (s *ThemeStore) GetThemeByID(_ context.Context, id int64) (domain.Theme, bool, error) {
s.mu.RLock()
defer s.mu.RUnlock()
t, ok := s.byID[id]
if !ok {
return domain.Theme{}, false, nil
}
return t.Clone(), true, nil
}
func (s *ThemeStore) GetThemeBySlug(_ context.Context, slug string) (domain.Theme, bool, error) {
if slug == "" {
return domain.Theme{}, false, nil
}
s.mu.RLock()
defer s.mu.RUnlock()
id, ok := s.bySlug[slug]
if !ok {
return domain.Theme{}, false, nil
}
t, ok := s.byID[id]
if !ok {
return domain.Theme{}, false, nil
}
return t.Clone(), true, nil
}
func (s *ThemeStore) SlugExists(_ context.Context, slug string) (bool, error) {
if slug == "" {
return false, nil
}
s.mu.RLock()
defer s.mu.RUnlock()
_, ok := s.bySlug[slug]
return ok, nil
}
func (s *ThemeStore) UpdateTheme(_ context.Context, t domain.Theme) error {
s.mu.Lock()
defer s.mu.Unlock()
prev, ok := s.byID[t.ID]
if !ok {
return domain.ErrThemeNotFound
}
if t.Slug != "" && t.Slug != prev.Slug {
if _, taken := s.bySlug[t.Slug]; taken {
return domain.ErrThemeSlugTaken
}
}
if prev.Slug != "" && prev.Slug != t.Slug {
delete(s.bySlug, prev.Slug)
}
s.byID[t.ID] = t.Clone()
if t.Slug != "" {
s.bySlug[t.Slug] = t.ID
}
return nil
}
func (s *ThemeStore) IncrementInstalls(_ context.Context, id int64) error {
s.mu.Lock()
defer s.mu.Unlock()
t, ok := s.byID[id]
if !ok {
return domain.ErrThemeNotFound
}
t.InstallsCount++
s.byID[id] = t
return nil
}
func (s *ThemeStore) SetInstalled(_ context.Context, userID, themeID int64, dark bool) error {
s.mu.Lock()
defer s.mu.Unlock()
if _, ok := s.byID[themeID]; !ok {
return domain.ErrThemeNotFound
}
byUser := s.installs[userID]
if byUser == nil {
byUser = make(map[int64]themeInstall)
s.installs[userID] = byUser
}
if prev, ok := byUser[themeID]; ok {
byUser[themeID] = themeInstall{dark: dark, order: prev.order}
return nil
}
s.seq++
byUser[themeID] = themeInstall{dark: dark, order: s.seq}
return nil
}
func (s *ThemeStore) RemoveInstalled(_ context.Context, userID, themeID int64) error {
s.mu.Lock()
defer s.mu.Unlock()
if byUser := s.installs[userID]; byUser != nil {
delete(byUser, themeID)
}
return nil
}
func (s *ThemeStore) ListInstalledByUser(_ context.Context, userID int64) ([]domain.Theme, error) {
s.mu.RLock()
defer s.mu.RUnlock()
byUser := s.installs[userID]
if len(byUser) == 0 {
return nil, nil
}
type row struct {
t domain.Theme
order int64
}
rows := make([]row, 0, len(byUser))
for themeID, inst := range byUser {
t, ok := s.byID[themeID]
if !ok {
continue
}
rows = append(rows, row{t: t.Clone(), order: inst.order})
}
sort.Slice(rows, func(i, j int) bool { return rows[i].order < rows[j].order })
out := make([]domain.Theme, 0, len(rows))
for _, r := range rows {
out = append(out, r.t)
}
return out, nil
}
func (s *ThemeStore) ListThemesForUser(_ context.Context, userID int64) ([]domain.Theme, error) {
s.mu.RLock()
defer s.mu.RUnlock()
seen := make(map[int64]bool)
out := make([]domain.Theme, 0)
for _, t := range s.byID {
if t.CreatorUserID == userID {
out = append(out, t.Clone())
seen[t.ID] = true
}
}
if byUser := s.installs[userID]; byUser != nil {
for themeID := range byUser {
if seen[themeID] {
continue
}
if t, ok := s.byID[themeID]; ok {
out = append(out, t.Clone())
}
}
}
sort.Slice(out, func(i, j int) bool {
if out[i].CreatedAt != out[j].CreatedAt {
return out[i].CreatedAt < out[j].CreatedAt
}
return out[i].ID < out[j].ID
})
return out, nil
}