package service import ( "errors" "regexp" "strings" "time" "unicode/utf8" "github.com/freefire/jiang13-bbs/model" "gorm.io/gorm" ) var ( sitePageSlugRe = regexp.MustCompile(`^[a-z0-9]+(?:-[a-z0-9]+)*$`) ErrSitePageNotDeleted = errors.New("仅已删除的单页可彻底删除") ) // 写入时显式列出字段(须为独立 Select 参数;逗号拼成一串会被 GORM 当成单个非法字段名) var sitePageWriteFields = []string{ "Slug", "Title", "Content", "Excerpt", "Published", "ShowInSidebar", "SortOrder", "AllowComments", } // SitePageService 站点单页服务 type SitePageService struct { db *gorm.DB } func NewSitePageService(db *gorm.DB) *SitePageService { return &SitePageService{db: db} } // SitePageInput 创建/更新单页入参 type SitePageInput struct { Slug string `json:"slug"` Title string `json:"title"` Content string `json:"content"` Excerpt string `json:"excerpt"` Published *bool `json:"published"` ShowInSidebar *bool `json:"show_in_sidebar"` SortOrder *int `json:"sort_order"` AllowComments *bool `json:"allow_comments"` } // SidebarPageItem 首页右栏「关于本站」入口 type SidebarPageItem struct { ID uint `json:"id"` Slug string `json:"slug"` Title string `json:"title"` Excerpt string `json:"excerpt"` } // PublishedPageItem 公开单页目录(全部页,含未进侧栏的已发布页) type PublishedPageItem struct { ID uint `json:"id"` Slug string `json:"slug"` Title string `json:"title"` Excerpt string `json:"excerpt"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } func (in *SitePageInput) normalize(existing *model.SitePage) error { in.Slug = strings.ToLower(strings.TrimSpace(in.Slug)) if in.Slug == "" { return errors.New("slug 不能为空") } if len(in.Slug) > 64 { return errors.New("slug 不能超过 64 字符") } if !sitePageSlugRe.MatchString(in.Slug) { return errors.New("slug 仅允许小写字母、数字与连字符") } in.Title = strings.TrimSpace(in.Title) if in.Title == "" { return errors.New("标题不能为空") } if utf8.RuneCountInString(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 := rejectHideBlocks(in.Content, ErrSitePageHideNotAllowed); err != nil { return err } in.Excerpt = strings.TrimSpace(in.Excerpt) if utf8.RuneCountInString(in.Excerpt) > 300 { return errors.New("摘要不能超过 300 字") } _ = existing return nil } func (in *SitePageInput) applyTo(p *model.SitePage) { p.Slug = in.Slug p.Title = in.Title p.Content = in.Content p.Excerpt = in.Excerpt if in.Published != nil { p.Published = *in.Published } if in.ShowInSidebar != nil { p.ShowInSidebar = *in.ShowInSidebar } if in.SortOrder != nil { p.SortOrder = *in.SortOrder } if in.AllowComments != nil { p.AllowComments = *in.AllowComments } } // slugTaken 查询 slug 占用情况(Unscoped 含软删行:软删行仍占用数据库唯一索引) // active=在用单页占用;deleted=已删除单页占用 func (s *SitePageService) slugTaken(slug string, excludeID uint) (active, deleted bool, err error) { q := s.db.Unscoped().Model(&model.SitePage{}).Where("slug = ?", slug).Select("deleted_at") if excludeID > 0 { q = q.Where("id <> ?", excludeID) } var rows []struct { DeletedAt gorm.DeletedAt } if err := q.Find(&rows).Error; err != nil { return false, false, err } for _, r := range rows { if r.DeletedAt.Valid { deleted = true } else { active = true } } return active, deleted, nil } // ListSidebarPublished 首页右栏:已发布且勾选侧栏展示 func (s *SitePageService) ListSidebarPublished() ([]SidebarPageItem, error) { var pages []model.SitePage err := s.db.Where("published = ? AND show_in_sidebar = ?", true, true). Order("sort_order ASC, id ASC"). Select("id", "slug", "title", "excerpt"). Find(&pages).Error if err != nil { return nil, err } out := make([]SidebarPageItem, 0, len(pages)) for _, p := range pages { out = append(out, SidebarPageItem{ ID: p.ID, Slug: p.Slug, Title: p.Title, Excerpt: p.Excerpt, }) } return out, nil } // ListPublished 已发布单页目录(全部页;含未勾选侧栏的) func (s *SitePageService) ListPublished() ([]PublishedPageItem, error) { var pages []model.SitePage err := s.db.Where("published = ?", true). Order("sort_order ASC, id ASC"). Select("id", "slug", "title", "excerpt", "created_at", "updated_at"). Limit(100). Find(&pages).Error if err != nil { return nil, err } out := make([]PublishedPageItem, 0, len(pages)) for _, p := range pages { out = append(out, PublishedPageItem{ ID: p.ID, Slug: p.Slug, Title: p.Title, Excerpt: p.Excerpt, CreatedAt: p.CreatedAt, UpdatedAt: p.UpdatedAt, }) } return out, nil } // GetPublishedBySlug 公开详情(仅已发布) func (s *SitePageService) GetPublishedBySlug(slug string) (*model.SitePage, error) { slug = strings.ToLower(strings.TrimSpace(slug)) var p model.SitePage err := s.db.Where("slug = ? AND published = ?", slug, true).First(&p).Error if err != nil { return nil, err } return &p, nil } // SitePageAdminItem 管理端单页(附 deleted 标记;DeletedAt json:"-" 不出模型) type SitePageAdminItem struct { model.SitePage Deleted bool `json:"deleted"` } // ListAll 管理后台全部单页(含已软删,仅供查看与彻底删除) func (s *SitePageService) ListAll() ([]SitePageAdminItem, error) { var pages []model.SitePage if err := s.db.Unscoped().Order("sort_order ASC, id ASC").Limit(100).Find(&pages).Error; err != nil { return nil, err } out := make([]SitePageAdminItem, 0, len(pages)) for _, p := range pages { out = append(out, SitePageAdminItem{SitePage: p, Deleted: p.DeletedAt.Valid}) } return out, nil } // Get 管理后台按 ID 取单页 func (s *SitePageService) Get(id uint) (*model.SitePage, error) { var p model.SitePage if err := s.db.First(&p, id).Error; err != nil { return nil, err } return &p, nil } // Create 新建单页 func (s *SitePageService) Create(in *SitePageInput) (*model.SitePage, error) { if err := in.normalize(nil); err != nil { return nil, err } active, deleted, err := s.slugTaken(in.Slug, 0) if err != nil { return nil, err } if active { return nil, errors.New("slug 已被占用") } if deleted { return nil, errors.New("该地址已被已删除单页占用,可在单页管理列表中彻底删除后重新使用") } p := &model.SitePage{ Published: false, ShowInSidebar: false, AllowComments: false, SortOrder: 0, } in.applyTo(p) if err := s.db.Select(sitePageWriteFields).Create(p).Error; err != nil { return nil, err } return p, nil } // Update 更新单页 func (s *SitePageService) Update(id uint, in *SitePageInput) (*model.SitePage, error) { var p model.SitePage if err := s.db.First(&p, id).Error; err != nil { return nil, err } if err := in.normalize(&p); err != nil { return nil, err } active, deleted, err := s.slugTaken(in.Slug, id) if err != nil { return nil, err } if active { return nil, errors.New("slug 已被占用") } if deleted { return nil, errors.New("该地址已被已删除单页占用,可在单页管理列表中彻底删除后重新使用") } in.applyTo(&p) // 用 map 更新,确保 false / 空串也会写入;勿用逗号拼接的 Select+Save(只会改 updated_at) if err := s.db.Model(&model.SitePage{}).Where("id = ?", p.ID).Updates(map[string]interface{}{ "slug": p.Slug, "title": p.Title, "content": p.Content, "excerpt": p.Excerpt, "published": p.Published, "show_in_sidebar": p.ShowInSidebar, "sort_order": p.SortOrder, "allow_comments": p.AllowComments, }).Error; err != nil { return nil, err } return &p, nil } // Delete 软删除单页 func (s *SitePageService) Delete(id uint) error { result := s.db.Delete(&model.SitePage{}, id) if result.Error != nil { return result.Error } if result.RowsAffected == 0 { return gorm.ErrRecordNotFound } return nil } // Purge 彻底删除已软删的单页(slug 释放可复用) func (s *SitePageService) Purge(id uint) error { var p model.SitePage if err := s.db.Unscoped().First(&p, id).Error; err != nil { return gorm.ErrRecordNotFound } if !p.DeletedAt.Valid { return ErrSitePageNotDeleted } return s.db.Unscoped().Delete(&p).Error }