230 lines
12 KiB
Go
230 lines
12 KiB
Go
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
|
||
}
|