feat: 帖子分级置顶(板块/全站)与板块管理员置顶权限、消息管理权限调整

This commit is contained in:
2026-10-15 23:35:30 +08:00
parent e7b0686947
commit 47f87a011c
40 changed files with 822 additions and 207 deletions

View File

@@ -13,6 +13,7 @@ import (
var (
ErrContentNotDeleted = errors.New("仅已删除内容可彻底清除")
ErrContentNotFound = errors.New("内容不存在")
ErrPinOrderForbidden = errors.New("无权配置该层级置顶顺序")
)
func (s *ModerationService) authorRole(userID uint) (model.Role, error) {
@@ -511,3 +512,105 @@ func (s *ModerationService) PurgeComment(actor *Actor, id uint) error {
return tx.Unscoped().Delete(&model.Comment{}, id).Error
})
}
// ===== 置顶顺序配置(后台可拖拽排序,同层级 pin_order 越小越靠前) =====
// PinnedPostItem 置顶帖排序列表项
type PinnedPostItem struct {
ID uint `json:"id"`
Title string `json:"title"`
Pinned int `json:"pinned"`
PinOrder int `json:"pin_order"`
BoardID uint `json:"board_id"`
BoardName string `json:"board_name"`
CreatedAt time.Time `json:"created_at"`
}
// assertPinScopePermission 校验 actor 对指定置顶层级/板块的排序权限:
// 全局置顶仅管理员及以上;板块置顶允许管理员或管辖该板块的板管(board_id 必填)。
func (s *ModerationService) assertPinScopePermission(actor *Actor, level int, boardID uint) error {
if actor == nil {
return ErrPinOrderForbidden
}
if model.RoleLevel(actor.Role) >= model.RoleLevel(model.RoleAdmin) {
return nil
}
if level == model.PinGlobal {
return ErrPinOrderForbidden
}
if level == model.PinBoard {
if boardID == 0 {
return ErrPinOrderForbidden // 板管必须指定具体板块
}
if actor.CanModerateBoard(boardID) {
return nil
}
}
return ErrPinOrderForbidden
}
// ListPinnedPosts 列出指定层级的置顶帖(当前顺序:pin_order 升序,未配置按发帖时间倒序兜底)。
// level=2 全站置顶;level=1 需带 board_id(管理员可省=全部板块的板块置顶)。
func (s *ModerationService) ListPinnedPosts(actor *Actor, level int, boardID uint) ([]PinnedPostItem, error) {
if err := s.assertPinScopePermission(actor, level, boardID); err != nil {
return nil, err
}
q := s.db.Model(&model.Post{}).
Where("pinned = ? AND deleted_at IS NULL AND status = ?", level, model.ContentStatusPublished)
if boardID > 0 {
q = q.Where("board_id = ?", boardID)
}
var posts []model.Post
if err := q.Order("(pin_order = 0), pin_order ASC, created_at DESC").
Preload("Board").Find(&posts).Error; err != nil {
return nil, err
}
items := make([]PinnedPostItem, 0, len(posts))
for _, p := range posts {
items = append(items, PinnedPostItem{
ID: p.ID,
Title: p.Title,
Pinned: p.Pinned,
PinOrder: p.PinOrder,
BoardID: p.BoardID,
BoardName: p.Board.Name,
CreatedAt: p.CreatedAt,
})
}
return items, nil
}
// SavePinOrder 按传入 ID 顺序重写 pin_order(从 1 递增)。
// 安全约束:ids 必须全部属于该层级且在权限范围内,否则整体拒绝,避免半截写入。
func (s *ModerationService) SavePinOrder(actor *Actor, level int, boardID uint, ids []uint) error {
if err := s.assertPinScopePermission(actor, level, boardID); err != nil {
return err
}
if len(ids) == 0 {
return nil
}
if len(ids) > 200 {
return errors.New("单次最多排序 200 条")
}
var count int64
q := s.db.Model(&model.Post{}).
Where("id IN ? AND pinned = ? AND deleted_at IS NULL", ids, level)
if boardID > 0 {
q = q.Where("board_id = ?", boardID)
}
if err := q.Count(&count).Error; err != nil {
return err
}
if count != int64(len(ids)) {
return ErrPinOrderForbidden // 含越权/已取消置顶/已删除的 ID
}
return s.db.Transaction(func(tx *gorm.DB) error {
for i, id := range ids {
if err := tx.Model(&model.Post{}).Where("id = ?", id).
UpdateColumn("pin_order", i+1).Error; err != nil {
return err
}
}
return nil
})
}