package service import ( "errors" "strings" "time" "unicode/utf8" "infinite-canvas/backend/internal/model" "gorm.io/gorm" ) type CreateAnnouncementRequest struct { Title string `json:"title"` Content string `json:"content"` Level model.AnnouncementLevel `json:"level"` } type UpdateAnnouncementRequest = CreateAnnouncementRequest type AnnouncementPage struct { Announcements []model.Announcement `json:"announcements"` Total int64 `json:"total"` Page int `json:"page"` Limit int `json:"limit"` } type UserAnnouncementFeed struct { Announcements []model.Announcement `json:"announcements"` UnreadCount int64 `json:"unreadCount"` } func (s *Service) AdminAnnouncementPage(actor *model.User, query AdminListQuery) (*AnnouncementPage, error) { if err := s.RequireAdmin(actor); err != nil { return nil, err } page, limit := normalizeAdminPage(query.Page, query.Limit) announcements, total, err := s.repo.AdminAnnouncements(query.Keyword, model.AnnouncementStatus(query.Status), limit, (page-1)*limit) if err != nil { return nil, err } return &AnnouncementPage{Announcements: announcements, Total: total, Page: page, Limit: limit}, nil } func (s *Service) CreateAnnouncement(actor *model.User, req CreateAnnouncementRequest) (*model.Announcement, error) { if err := s.RequireAdmin(actor); err != nil { return nil, err } title, content, level, err := normalizeAnnouncementInput(req) if err != nil { return nil, err } now := time.Now() announcement := &model.Announcement{ ID: newID(), Title: title, Content: content, Level: level, Status: model.AnnouncementStatusActive, CreatedBy: actor.ID, PublishedAt: now, CreatedAt: now, UpdatedAt: now, } if err := s.repo.Create(announcement); err != nil { return nil, err } return announcement, nil } func (s *Service) UpdateAnnouncement(actor *model.User, id string, req UpdateAnnouncementRequest) (*model.Announcement, error) { if err := s.RequireAdmin(actor); err != nil { return nil, err } announcement, err := s.repo.Announcement(strings.TrimSpace(id)) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, BadAuthRequest("公告不存在") } return nil, err } title, content, level, err := normalizeAnnouncementInput(req) if err != nil { return nil, err } now := time.Now() announcement.Title = title announcement.Content = content announcement.Level = level announcement.Status = model.AnnouncementStatusActive announcement.ClosedAt = nil announcement.PublishedAt = now announcement.UpdatedAt = now if err := s.repo.UpdateAnnouncement(announcement); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, BadAuthRequest("公告状态已变化,请刷新后重试") } return nil, err } return s.repo.Announcement(announcement.ID) } func (s *Service) CloseAnnouncement(actor *model.User, id string) (*model.Announcement, error) { if err := s.RequireAdmin(actor); err != nil { return nil, err } announcement, err := s.repo.Announcement(strings.TrimSpace(id)) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, BadAuthRequest("公告不存在") } return nil, err } if announcement.Status == model.AnnouncementStatusClosed { return nil, BadAuthRequest("公告已经关闭") } updated, err := s.repo.CloseAnnouncement(announcement.ID, time.Now()) if err != nil { return nil, err } if !updated { return nil, BadAuthRequest("公告状态已变化,请刷新后重试") } return s.repo.Announcement(announcement.ID) } func (s *Service) UserAnnouncements(user *model.User) (*UserAnnouncementFeed, error) { if user == nil { return nil, Unauthorized("请先登录") } announcements, unreadCount, err := s.repo.AnnouncementFeed(user.ID) if err != nil { return nil, err } return &UserAnnouncementFeed{Announcements: announcements, UnreadCount: unreadCount}, nil } func (s *Service) MarkAnnouncementsRead(user *model.User, announcementIDs []string) (int64, error) { if user == nil { return 0, Unauthorized("请先登录") } if len(announcementIDs) > 5000 { return 0, BadAuthRequest("单次已读公告数量过多") } ids := uniqueNonEmpty(announcementIDs) for _, id := range ids { if len(id) > 64 { return 0, BadAuthRequest("公告 ID 无效") } } if err := s.repo.MarkAnnouncementsRead(user.ID, ids, time.Now()); err != nil { return 0, err } _, unreadCount, err := s.repo.AnnouncementFeed(user.ID) return unreadCount, err } func validAnnouncementLevel(level model.AnnouncementLevel) bool { return level == model.AnnouncementLevelInfo || level == model.AnnouncementLevelSuccess || level == model.AnnouncementLevelWarning || level == model.AnnouncementLevelCritical } func normalizeAnnouncementInput(req CreateAnnouncementRequest) (string, string, model.AnnouncementLevel, error) { title := strings.TrimSpace(req.Title) content := strings.TrimSpace(req.Content) if title == "" || content == "" { return "", "", "", BadAuthRequest("请填写公告标题和正文") } if utf8.RuneCountInString(title) > 120 { return "", "", "", BadAuthRequest("公告标题不能超过 120 个字符") } if utf8.RuneCountInString(content) > 4000 { return "", "", "", BadAuthRequest("公告正文不能超过 4000 个字符") } if !validAnnouncementLevel(req.Level) { return "", "", "", BadAuthRequest("公告级别无效") } return title, content, req.Level, nil }