feat: 友链/书库/移动端导航/排行榜等多模块功能
新增模块: - 友链(friendlink)后端处理 + 前端管理页与友链板 - 书库(library):导入、章节、封面、阅读页、横竖版自适应 AdaptiveCoverSlot - 顶栏导航(header_nav)配置与 MobileTabBar/MobileRailDrawers 移动端抽屉 - 排行榜 service 测试、站点页面测试、时间线发布(timeline_release) 其它改动: - 后端 handlers/services 全量小幅调整 - 前端组件、库函数、URL/品牌/站点 URL 工具更新 - Lightbox 图片、MdEntries 条目卡、coverColor 派生色相等前端能力
This commit is contained in:
@@ -11,7 +11,10 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var sitePageSlugRe = regexp.MustCompile(`^[a-z0-9]+(?:-[a-z0-9]+)*$`)
|
||||
var (
|
||||
sitePageSlugRe = regexp.MustCompile(`^[a-z0-9]+(?:-[a-z0-9]+)*$`)
|
||||
ErrSitePageNotDeleted = errors.New("仅已删除的单页可彻底删除")
|
||||
)
|
||||
|
||||
// 写入时显式列出字段(须为独立 Select 参数;逗号拼成一串会被 GORM 当成单个非法字段名)
|
||||
var sitePageWriteFields = []string{
|
||||
@@ -116,16 +119,27 @@ func (in *SitePageInput) applyTo(p *model.SitePage) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SitePageService) slugTaken(slug string, excludeID uint) (bool, error) {
|
||||
q := s.db.Model(&model.SitePage{}).Where("slug = ?", slug)
|
||||
// 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 n int64
|
||||
if err := q.Count(&n).Error; err != nil {
|
||||
return false, err
|
||||
var rows []struct {
|
||||
DeletedAt gorm.DeletedAt
|
||||
}
|
||||
return n > 0, nil
|
||||
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 首页右栏:已发布且勾选侧栏展示
|
||||
@@ -179,11 +193,23 @@ func (s *SitePageService) GetPublishedBySlug(slug string) (*model.SitePage, erro
|
||||
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
|
||||
// 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 取单页
|
||||
@@ -200,13 +226,16 @@ 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)
|
||||
active, deleted, err := s.slugTaken(in.Slug, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if taken {
|
||||
if active {
|
||||
return nil, errors.New("slug 已被占用")
|
||||
}
|
||||
if deleted {
|
||||
return nil, errors.New("该地址已被已删除单页占用,可在单页管理列表中彻底删除后重新使用")
|
||||
}
|
||||
p := &model.SitePage{
|
||||
Published: false,
|
||||
ShowInSidebar: false,
|
||||
@@ -229,13 +258,16 @@ func (s *SitePageService) Update(id uint, in *SitePageInput) (*model.SitePage, e
|
||||
if err := in.normalize(&p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
taken, err := s.slugTaken(in.Slug, id)
|
||||
active, deleted, err := s.slugTaken(in.Slug, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if taken {
|
||||
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{}{
|
||||
@@ -264,3 +296,15 @@ func (s *SitePageService) Delete(id uint) error {
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user