Files
jiang13-bbs/backend/service/announcement.go
freefire 5270fee2b7 feat: 评论/帖子编辑历史与编辑标记,内容锁定
- 新增 smartDiff 差异计算与 EditHistoryModal 编辑历史弹窗,替换原 CommentEditHistoryModal
- 新增 PostEditedMark 编辑标记展示
- 新增内容锁定机制(content_lock)防止并发编辑冲突
- 审核与通知服务适配
2026-09-25 23:41:45 +08:00

211 lines
5.8 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import (
"errors"
"strings"
"unicode/utf8"
"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 utf8.RuneCountInString(in.Content) > 20000 {
return errors.New("内容不能超过 20000 字")
}
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
}