fix: 修复帖子相关逻辑的严重安全与错误处理问题

- 修复 Service 层所有 FindByID 错误均返回 ErrUserNotFound 的严重 bug,新增 findPost/findPostWithAuthor 正确区分 gorm.ErrRecordNotFound 和数据库错误
- 新增帖子相关错误哨兵:ErrPostNotFound/ErrPostCannotEdit/ErrPostCannotSubmit/ErrPostCannotApprove/ErrPostCannotReject/ErrPostCannotUnlock
- 修复 ShowAPI 缺少权限控制,非 approved 帖子可被任意用户通过 API 获取
- 修复 EditPage 缺少权限检查,非作者可访问编辑页面
- 修复 Update/Delete API 中 uid 断言未做 ok 检查
- Controller 层区分业务错误和内部错误,避免透传 Service 层错误信息给客户端
- 将 postStore 接口从 post_service.go 移至 repository.go,与其他仓储接口保持一致
- 移除 new.html 中孤立的 </template> 标签
This commit is contained in:
2026-05-28 14:44:22 +08:00
parent 36b18c0ffe
commit e45d66fb77
6 changed files with 141 additions and 47 deletions

View File

@ -1,6 +1,7 @@
package service
import (
"errors"
"fmt"
"regexp"
@ -9,6 +10,7 @@ import (
"metazone.cc/metalab/internal/model"
"github.com/microcosm-cc/bluemonday"
"gorm.io/gorm"
)
// sanitizePolicy Tiptap 输出 HTML 的消毒策略
@ -24,18 +26,6 @@ var sanitizePolicy = func() *bluemonday.Policy {
return p
}()
// postStore 帖子服务所需的最小仓储接口ISP
type postStore interface {
Create(post *model.Post) error
FindByID(id uint) (*model.Post, error)
FindByIDWithAuthor(id uint) (*model.Post, error)
FindPageable(keyword string, offset, limit int) ([]model.Post, int64, error)
FindAdminPageable(keyword, status string, offset, limit int) ([]model.Post, int64, error)
Update(post *model.Post) error
SoftDelete(id uint) error
Restore(id uint) error
}
// PostService 帖子业务逻辑
type PostService struct {
repo postStore
@ -55,6 +45,30 @@ func (s *PostService) auditEnabled() bool {
return s.ss.IsAuditEnabled()
}
// findPost 按 ID 查找帖子,区分"未找到"和"数据库错误"
func (s *PostService) findPost(id uint) (*model.Post, error) {
post, err := s.repo.FindByID(id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, common.ErrPostNotFound
}
return nil, fmt.Errorf("查询帖子失败: %w", err)
}
return post, nil
}
// findPostWithAuthor 按 ID 查找帖子(含作者名),区分"未找到"和"数据库错误"
func (s *PostService) findPostWithAuthor(id uint) (*model.Post, error) {
post, err := s.repo.FindByIDWithAuthor(id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, common.ErrPostNotFound
}
return nil, fmt.Errorf("查询帖子失败: %w", err)
}
return post, nil
}
// sanitizeHTML 对 Tiptap 输出的 HTML 进行消毒
// 前端 WYSIWYG 编辑器直接输出 HTML后端仅做安全消毒
func (s *PostService) sanitizeHTML(body string) string {
@ -86,11 +100,7 @@ func (s *PostService) Create(userID uint, title, body string) (*model.Post, erro
// GetByID 按 ID 获取帖子(含作者名,仅 approved 公开可见)
func (s *PostService) GetByID(id uint) (*model.Post, error) {
post, err := s.repo.FindByIDWithAuthor(id)
if err != nil {
return nil, common.ErrUserNotFound // 复用哨兵表示资源不存在
}
return post, nil
return s.findPostWithAuthor(id)
}
// List 公开帖子列表(仅 approved
@ -109,13 +119,13 @@ func (s *PostService) ListAdmin(keyword, status string, page, pageSize int) ([]m
// Update 编辑帖子(权限在 controller 层检查)
func (s *PostService) Update(postID uint, title, body string) error {
post, err := s.repo.FindByID(postID)
post, err := s.findPost(postID)
if err != nil {
return common.ErrUserNotFound
return err
}
if post.Status == model.PostStatusPending || post.Status == model.PostStatusLocked {
return fmt.Errorf("当前状态不允许编辑")
return common.ErrPostCannotEdit
}
post.Title = title
@ -138,12 +148,12 @@ func (s *PostService) Delete(postID uint) error {
// SubmitForAudit 提交审核draft → pending
func (s *PostService) SubmitForAudit(postID uint) error {
post, err := s.repo.FindByID(postID)
post, err := s.findPost(postID)
if err != nil {
return common.ErrUserNotFound
return err
}
if post.Status != model.PostStatusDraft && post.Status != model.PostStatusRejected {
return fmt.Errorf("仅草稿和已退回帖子可提交审核")
return common.ErrPostCannotSubmit
}
post.Status = model.PostStatusPending
return s.repo.Update(post)
@ -151,12 +161,12 @@ func (s *PostService) SubmitForAudit(postID uint) error {
// Approve 审核通过pending → approved
func (s *PostService) Approve(postID uint) error {
post, err := s.repo.FindByID(postID)
post, err := s.findPost(postID)
if err != nil {
return common.ErrUserNotFound
return err
}
if post.Status != model.PostStatusPending {
return fmt.Errorf("仅待审核帖子可通过")
return common.ErrPostCannotApprove
}
post.Status = model.PostStatusApproved
post.RejectReason = ""
@ -165,23 +175,23 @@ func (s *PostService) Approve(postID uint) error {
// Reject 退回pending/approved → rejected
func (s *PostService) Reject(postID uint, reason string) error {
post, err := s.repo.FindByID(postID)
post, err := s.findPost(postID)
if err != nil {
return common.ErrUserNotFound
return err
}
if post.Status != model.PostStatusPending && post.Status != model.PostStatusApproved {
return fmt.Errorf("仅待审核和已发布帖子可退回")
return common.ErrPostCannotReject
}
post.Status = model.PostStatusRejected
post.RejectReason = reason
return s.repo.Update(post)
}
// Lock 锁定:任意状态 → locked
// Lock 锁定:任意状态 → locked(管理员可随时锁定)
func (s *PostService) Lock(postID uint) error {
post, err := s.repo.FindByID(postID)
post, err := s.findPost(postID)
if err != nil {
return common.ErrUserNotFound
return err
}
post.Status = model.PostStatusLocked
return s.repo.Update(post)
@ -189,12 +199,12 @@ func (s *PostService) Lock(postID uint) error {
// Unlock 解锁locked → 草稿
func (s *PostService) Unlock(postID uint) error {
post, err := s.repo.FindByID(postID)
post, err := s.findPost(postID)
if err != nil {
return common.ErrUserNotFound
return err
}
if post.Status != model.PostStatusLocked {
return fmt.Errorf("仅锁定状态可解锁")
return common.ErrPostCannotUnlock
}
post.Status = model.PostStatusDraft
return s.repo.Update(post)