package service import ( "errors" "strings" "github.com/freefire/jiang13-bbs/markdown" "github.com/freefire/jiang13-bbs/model" "gorm.io/gorm" ) // 公告标签允许使用的预设色(服务端白名单,禁止透传任意颜色值) var allowedTagColors = map[string]bool{ "blue": true, "green": true, "orange": true, "red": true, "purple": true, "gray": true, "teal": true, "cyan": true, "pink": true, "amber": true, "indigo": true, "rose": true, } // AnnouncementService 站点公告服务 type AnnouncementService struct { db *gorm.DB } func NewAnnouncementService(db *gorm.DB) *AnnouncementService { return &AnnouncementService{db: db} } // AnnouncementInput 创建/更新公告的入参 type AnnouncementInput struct { Title string `json:"title"` Content string `json:"content"` Tag string `json:"tag"` TagColor string `json:"tag_color"` Published *bool `json:"published"` Pinned *bool `json:"pinned"` } // normalize 清洗并校验入参,返回可直接落库的字段 func (in *AnnouncementInput) normalize(existing *model.Announcement) error { in.Title = strings.TrimSpace(in.Title) if in.Title == "" { return errors.New("标题不能为空") } if len([]rune(in.Title)) > 200 { return errors.New("标题不能超过 200 字") } in.Content = strings.TrimSpace(in.Content) if in.Content == "" { return errors.New("内容不能为空") } if err := markdown.ValidateHideContent(in.Content); err != nil { return err } if err := rejectReplyHideBlocks(in.Content, ErrAnnouncementReplyHideNotAllowed); err != nil { return err } in.Tag = strings.TrimSpace(in.Tag) if in.Tag == "" { in.Tag = "公告" } if len([]rune(in.Tag)) > 32 { return errors.New("标签不能超过 32 字") } in.TagColor = strings.ToLower(strings.TrimSpace(in.TagColor)) if in.TagColor == "" { if existing != nil && existing.TagColor != "" { in.TagColor = existing.TagColor } else { in.TagColor = "blue" } } if !allowedTagColors[in.TagColor] { return errors.New("不支持的标签颜色") } return nil } // applyTo 把入参写入模型(Published/Pinned 为指针以区分"未传"与"显式 false") func (in *AnnouncementInput) applyTo(a *model.Announcement) { a.Title = in.Title a.Content = in.Content a.Tag = in.Tag a.TagColor = in.TagColor if in.Published != nil { a.Published = *in.Published } if in.Pinned != nil { a.Pinned = *in.Pinned } } // 写入字段须为独立 Select 参数;逗号拼成一串会被 GORM 当成单个非法字段名 var announcementWriteFields = []string{ "Title", "Content", "Tag", "TagColor", "Published", "Pinned", } // ListPublished 已发布公告(首页右栏),置顶优先再按发布时间倒序 func (s *AnnouncementService) ListPublished(limit int) ([]model.Announcement, error) { if limit <= 0 { limit = 5 } var list []model.Announcement err := s.db.Where("published = ?", true). Order("pinned DESC, created_at DESC, id DESC").Limit(limit).Find(&list).Error return list, err } // ListPublishedPage 已发布公告分页(全部公告页) func (s *AnnouncementService) ListPublishedPage(page, size int) ([]model.Announcement, int64, error) { if page < 1 { page = 1 } if size < 1 || size > 50 { size = 20 } q := s.db.Model(&model.Announcement{}).Where("published = ?", true) var total int64 if err := q.Count(&total).Error; err != nil { return nil, 0, err } var list []model.Announcement err := q.Order("pinned DESC, created_at DESC, id DESC"). Offset((page - 1) * size).Limit(size).Find(&list).Error return list, total, err } // GetPublished 取单条已发布公告(详情页);草稿/不存在返回 gorm.ErrRecordNotFound func (s *AnnouncementService) GetPublished(id uint) (*model.Announcement, error) { var a model.Announcement err := s.db.Where("published = ?", true).First(&a, id).Error if err != nil { return nil, err } return &a, nil } // ListAll 管理后台:含草稿,置顶优先 func (s *AnnouncementService) ListAll() ([]model.Announcement, error) { var list []model.Announcement err := s.db.Order("pinned DESC, created_at DESC, id DESC").Limit(100).Find(&list).Error return list, err } // Create 创建公告 func (s *AnnouncementService) Create(in *AnnouncementInput) (*model.Announcement, error) { if err := in.normalize(nil); err != nil { return nil, err } a := &model.Announcement{Published: true, Pinned: false} in.applyTo(a) if err := s.db.Select(announcementWriteFields).Create(a).Error; err != nil { return nil, err } return a, nil } // Update 更新公告 func (s *AnnouncementService) Update(id uint, in *AnnouncementInput) (*model.Announcement, error) { var a model.Announcement if err := s.db.First(&a, id).Error; err != nil { return nil, err } if err := in.normalize(&a); err != nil { return nil, err } in.applyTo(&a) if err := s.db.Model(&model.Announcement{}).Where("id = ?", a.ID).Updates(map[string]interface{}{ "title": a.Title, "content": a.Content, "tag": a.Tag, "tag_color": a.TagColor, "published": a.Published, "pinned": a.Pinned, }).Error; err != nil { return nil, err } return &a, nil } // TogglePin 切换置顶状态 func (s *AnnouncementService) TogglePin(id uint) (*model.Announcement, error) { var a model.Announcement if err := s.db.First(&a, id).Error; err != nil { return nil, err } a.Pinned = !a.Pinned if err := s.db.Model(&a).Select("Pinned").Update("pinned", a.Pinned).Error; err != nil { return nil, err } return &a, nil } // Delete 删除公告(软删除) func (s *AnnouncementService) Delete(id uint) error { result := s.db.Delete(&model.Announcement{}, id) if result.Error != nil { return result.Error } if result.RowsAffected == 0 { return gorm.ErrRecordNotFound } return nil }