264 lines
6.9 KiB
Go
264 lines
6.9 KiB
Go
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]+)*$`)
|
||
|
||
// 写入时显式列出字段(须为独立 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 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
|
||
}
|
||
}
|
||
|
||
func (s *SitePageService) slugTaken(slug string, excludeID uint) (bool, error) {
|
||
q := s.db.Model(&model.SitePage{}).Where("slug = ?", slug)
|
||
if excludeID > 0 {
|
||
q = q.Where("id <> ?", excludeID)
|
||
}
|
||
var n int64
|
||
if err := q.Count(&n).Error; err != nil {
|
||
return false, err
|
||
}
|
||
return n > 0, 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
|
||
}
|
||
|
||
// ListAll 管理后台全部单页
|
||
func (s *SitePageService) ListAll() ([]model.SitePage, error) {
|
||
var list []model.SitePage
|
||
err := s.db.Order("sort_order ASC, id ASC").Limit(100).Find(&list).Error
|
||
return list, err
|
||
}
|
||
|
||
// 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
|
||
}
|
||
taken, err := s.slugTaken(in.Slug, 0)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if taken {
|
||
return nil, errors.New("slug 已被占用")
|
||
}
|
||
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
|
||
}
|
||
taken, err := s.slugTaken(in.Slug, id)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if taken {
|
||
return nil, errors.New("slug 已被占用")
|
||
}
|
||
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
|
||
}
|