180 lines
5.5 KiB
Go
180 lines
5.5 KiB
Go
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
|
|
}
|