package service import ( "fmt" "sort" "strings" "sync" "time" ) var backendStore = newDemoStore() type demoStore struct { mu sync.RWMutex Users []UserProfile Content []ContentItem Speakers []Speaker Categories []string Tags []string Notifications []NotificationItem Comments []CommentItem Audit []AuditItem Files map[string]StoredFile } func newDemoStore() *demoStore { return &demoStore{ Users: []UserProfile{ {ID: "demo-user-1", Name: "Демо-администратор", Login: "demo_admin", Roles: []RoleCode{RoleAdministrator, RoleEditor}, Subscriptions: []string{"Новости", "Демо-спикер 01", "медиапроизводство"}}, {ID: "demo-user-2", Name: "Демо-редактор", Login: "demo_editor", Roles: []RoleCode{RoleEditor}, Subscriptions: []string{"Видео"}}, {ID: "demo-user-3", Name: "Демо-пользователь", Login: "demo_user", Roles: []RoleCode{RoleUser}, Subscriptions: []string{"Аудио"}}, }, Content: []ContentItem{ {ID: "demo-news-1", Title: "Демо-новость о запуске медиаплатформы", Lead: "Публичная карточка показывает, как новости и статьи будут выглядеть в едином каталоге.", Body: "Демонстрационный материал без реальных персональных данных, подразделений и брендовых материалов.", Type: ContentTypeNews, Category: "Новости", Tags: []string{"медиапроизводство", "анонс"}, Author: "Демо-редакция", PublishedAt: "2026-06-04", Visibility: VisibilityPublic, Status: ContentStatusPublished, Views: 1240, ImageTone: "from-university-700 via-university-500 to-sky-300"}, {ID: "demo-video-1", Title: "Демо-видео: открытая лекция", Lead: "Видеоматериал с метаданными, статусом проверки, категорией и тегами.", Body: "В реальной системе здесь будет предпросмотр видео, CDN-ссылка, история модерации и аналитика просмотров.", Type: ContentTypeVideo, Category: "Видео", Tags: []string{"образование", "архив"}, Author: "Демо-медиагруппа", PublishedAt: "2026-06-09", Duration: "18:40", Visibility: VisibilityAuthenticated, Status: ContentStatusReview, Views: 382, ImageTone: "from-indigo-700 via-university-800 to-cyan-500"}, {ID: "demo-audio-1", Title: "Демо-аудио: выпуск университетского радио", Lead: "Аудиоконтент хранится в медиатеке и связывается с публикациями, авторами и тегами.", Body: "Этот пример показывает карточку аудио без использования реального названия передачи или записи.", Type: ContentTypeAudio, Category: "Аудио", Tags: []string{"интервью", "редакция"}, Author: "Демо-редактор", PublishedAt: "2026-06-11", Duration: "32:10", Visibility: VisibilityPublic, Status: ContentStatusPublished, Views: 715, ImageTone: "from-blue-950 via-blue-700 to-emerald-300"}, {ID: "demo-graphic-1", Title: "Демо-графика: афиша редакционного события", Lead: "Графические материалы можно фильтровать по типу, дате, категории и тегам.", Body: "Заглушка демонстрирует графический материал без копирования фотографий, логотипов или брендовых элементов.", Type: ContentTypeGraphic, Category: "Графика", Tags: []string{"анонс", "редакция"}, Author: "Демо-дизайнер", PublishedAt: "2026-06-13", Visibility: VisibilityRole, Status: ContentStatusModeration, Views: 96, ImageTone: "from-sky-400 via-blue-600 to-slate-900"}, {ID: "demo-event-1", Title: "Демо-анонс медиавстречи со спикером", Lead: "Мероприятия показаны как тип медиаконтента до подтверждения отдельной сущности Event.", Body: "События представлены как анонсы контента, потому что отдельная сущность Event требует подтверждения заказчиком.", Type: ContentTypeEvent, Category: "Мероприятия", Tags: []string{"анонс", "интервью"}, Author: "Демо-менеджер", PublishedAt: "2026-06-17", Visibility: VisibilityPublic, Status: ContentStatusDraft, Views: 45, ImageTone: "from-university-900 via-violet-700 to-orange-300"}, }, Speakers: []Speaker{ {ID: "demo-speaker-1", Name: "Демо-спикер 01", Role: "Приглашенный эксперт", Topics: []string{"медиапроизводство", "образование"}, Materials: 8, Subscribers: 132}, {ID: "demo-speaker-2", Name: "Демо-спикер 02", Role: "Участник редакционного события", Topics: []string{"интервью", "анонс"}, Materials: 5, Subscribers: 74}, {ID: "demo-speaker-3", Name: "Демо-спикер 03", Role: "Автор образовательных материалов", Topics: []string{"архив", "редакция"}, Materials: 12, Subscribers: 205}, }, Categories: []string{"Новости", "Статьи", "Видео", "Аудио", "Графика", "Мероприятия"}, Tags: []string{"медиапроизводство", "интервью", "анонс", "образование", "редакция", "архив"}, Notifications: []NotificationItem{ {ID: "demo-notification-1", Title: "Новый материал по подписке", Description: "В категории «Новости» появился демонстрационный материал.", Read: false, CreatedAt: "2026-06-13 10:20"}, {ID: "demo-notification-2", Title: "Материал ожидает проверки", Description: "Демо-видео находится на этапе проверки перед публикацией.", Read: true, CreatedAt: "2026-06-12 16:45"}, }, Comments: []CommentItem{ {ID: "demo-comment-1", ContentID: "demo-news-1", Author: "Демо-пользователь", Text: "Комментарий доступен авторизованным пользователям и может проходить модерацию.", CreatedAt: "2026-06-13 12:00"}, }, Audit: []AuditItem{ {ID: "demo-audit-1", Actor: "Демо-администратор", Action: "изменил статус", Target: "Демо-видео: открытая лекция", CreatedAt: "2026-06-13 11:10"}, {ID: "demo-audit-2", Actor: "Демо-редактор", Action: "создал черновик", Target: "Демо-анонс медиавстречи со спикером", CreatedAt: "2026-06-12 15:35"}, }, Files: map[string]StoredFile{}, } } func (s *demoStore) userByLogin(login string) (UserProfile, bool) { s.mu.RLock() defer s.mu.RUnlock() for _, user := range s.Users { if user.Login == login { return user, true } } return UserProfile{}, false } func (s *demoStore) upsertSubscription(userID, target string) []string { s.mu.Lock() defer s.mu.Unlock() for index := range s.Users { if s.Users[index].ID == userID { for _, subscription := range s.Users[index].Subscriptions { if subscription == target { return append([]string(nil), s.Users[index].Subscriptions...) } } s.Users[index].Subscriptions = append(s.Users[index].Subscriptions, target) return append([]string(nil), s.Users[index].Subscriptions...) } } return nil } func (s *demoStore) addUser(login, name string) UserProfile { s.mu.Lock() defer s.mu.Unlock() user := UserProfile{ID: fmt.Sprintf("demo-user-%d", time.Now().UnixNano()), Name: name, Login: login, Roles: []RoleCode{RoleUser}, Subscriptions: []string{}} s.Users = append(s.Users, user) return user } func (s *demoStore) addContent(item ContentItem) ContentItem { s.mu.Lock() defer s.mu.Unlock() s.Content = append([]ContentItem{item}, s.Content...) s.Audit = append([]AuditItem{{ID: fmt.Sprintf("demo-audit-%d", time.Now().UnixNano()), Actor: item.Author, Action: "создал черновик", Target: item.Title, CreatedAt: time.Now().Format("2006-01-02 15:04")}}, s.Audit...) return item } func (s *demoStore) addFile(file StoredFile) StoredFile { s.mu.Lock() defer s.mu.Unlock() s.Files[file.ID] = file return file } func (s *demoStore) fileByID(id string) (StoredFile, bool) { s.mu.RLock() defer s.mu.RUnlock() file, ok := s.Files[id] return file, ok } func (s *demoStore) patchContent(id string, patch ContentItem) (ContentItem, bool) { s.mu.Lock() defer s.mu.Unlock() for index, item := range s.Content { if item.ID != id { continue } if patch.Title != "" { item.Title = patch.Title } if patch.Lead != "" { item.Lead = patch.Lead } if patch.Body != "" { item.Body = patch.Body } if patch.Type != "" { item.Type = patch.Type } if patch.Category != "" { item.Category = patch.Category } if patch.Tags != nil { item.Tags = patch.Tags } if patch.Visibility != "" { item.Visibility = patch.Visibility } if patch.Status != "" { item.Status = patch.Status } if patch.MediaURL != "" { item.MediaURL = patch.MediaURL } if patch.MediaKind != "" { item.MediaKind = patch.MediaKind } if patch.MimeType != "" { item.MimeType = patch.MimeType } if patch.FileName != "" { item.FileName = patch.FileName } if patch.FileSize != 0 { item.FileSize = patch.FileSize } s.Content[index] = item return item, true } return ContentItem{}, false } func (s *demoStore) deleteContent(id string) bool { s.mu.Lock() defer s.mu.Unlock() for index, item := range s.Content { if item.ID == id { s.Content = append(s.Content[:index], s.Content[index+1:]...) return true } } return false } func (s *demoStore) contentByID(id string) (ContentItem, bool) { s.mu.Lock() defer s.mu.Unlock() for index, item := range s.Content { if item.ID == id { s.Content[index].Views++ return s.Content[index], true } } return ContentItem{}, false } func (s *demoStore) searchContent(term, category, contentType, sortMode string) []ContentItem { s.mu.RLock() defer s.mu.RUnlock() term = strings.ToLower(strings.TrimSpace(term)) items := make([]ContentItem, 0, len(s.Content)) for _, item := range s.Content { joined := strings.ToLower(strings.Join(append([]string{item.Title, item.Lead, item.Body, item.Author, item.Category, string(item.Status), string(item.Type)}, item.Tags...), " ")) if term != "" && !strings.Contains(joined, term) { continue } if category != "" && category != "Все" && item.Category != category { continue } if contentType != "" && string(item.Type) != contentType { continue } items = append(items, item) } sort.SliceStable(items, func(i, j int) bool { if sortMode == "newest" { return items[i].PublishedAt > items[j].PublishedAt } return items[i].Views > items[j].Views }) return items } func (s *demoStore) addComment(contentID string, user UserProfile, text string) CommentItem { s.mu.Lock() defer s.mu.Unlock() comment := CommentItem{ID: fmt.Sprintf("demo-comment-%d", time.Now().UnixNano()), ContentID: contentID, Author: user.Name, Text: text, CreatedAt: time.Now().Format("2006-01-02 15:04")} s.Comments = append([]CommentItem{comment}, s.Comments...) return comment }