diff --git a/internal/common/errors.go b/internal/common/errors.go
index 571c778..c9793e0 100644
--- a/internal/common/errors.go
+++ b/internal/common/errors.go
@@ -35,4 +35,12 @@ var (
ErrNeedsConfirmRestore = errors.New("需要二次确认恢复账号")
ErrOwnerCannotDelete = errors.New("站长不可自主注销,请先转让站长权限")
ErrRegistrationDisabled = errors.New("注册功能已关闭")
+
+ // 帖子相关
+ ErrPostNotFound = errors.New("帖子不存在")
+ ErrPostCannotEdit = errors.New("当前状态不允许编辑")
+ ErrPostCannotSubmit = errors.New("仅草稿和已退回帖子可提交审核")
+ ErrPostCannotApprove = errors.New("仅待审核帖子可通过")
+ ErrPostCannotReject = errors.New("仅待审核和已发布帖子可退回")
+ ErrPostCannotUnlock = errors.New("仅锁定状态可解锁")
)
diff --git a/internal/controller/admin/admin_post_controller.go b/internal/controller/admin/admin_post_controller.go
index 64ae5f9..919ac14 100644
--- a/internal/controller/admin/admin_post_controller.go
+++ b/internal/controller/admin/admin_post_controller.go
@@ -1,6 +1,7 @@
package admin
import (
+ "errors"
"net/http"
"strconv"
@@ -72,7 +73,11 @@ func (ctrl *AdminPostController) Approve(c *gin.Context) {
}
if err := ctrl.postService.Approve(uint(id)); err != nil {
- common.Error(c, http.StatusBadRequest, err.Error())
+ if errors.Is(err, common.ErrPostCannotApprove) || errors.Is(err, common.ErrPostNotFound) {
+ common.Error(c, http.StatusBadRequest, err.Error())
+ } else {
+ common.Error(c, http.StatusInternalServerError, "审核失败")
+ }
return
}
@@ -94,7 +99,11 @@ func (ctrl *AdminPostController) Reject(c *gin.Context) {
}
if err := ctrl.postService.Reject(uint(id), req.Reason); err != nil {
- common.Error(c, http.StatusBadRequest, err.Error())
+ if errors.Is(err, common.ErrPostCannotReject) || errors.Is(err, common.ErrPostNotFound) {
+ common.Error(c, http.StatusBadRequest, err.Error())
+ } else {
+ common.Error(c, http.StatusInternalServerError, "退回失败")
+ }
return
}
@@ -110,7 +119,11 @@ func (ctrl *AdminPostController) Lock(c *gin.Context) {
}
if err := ctrl.postService.Lock(uint(id)); err != nil {
- common.Error(c, http.StatusBadRequest, err.Error())
+ if errors.Is(err, common.ErrPostNotFound) {
+ common.Error(c, http.StatusBadRequest, err.Error())
+ } else {
+ common.Error(c, http.StatusInternalServerError, "锁定失败")
+ }
return
}
@@ -126,7 +139,11 @@ func (ctrl *AdminPostController) Unlock(c *gin.Context) {
}
if err := ctrl.postService.Unlock(uint(id)); err != nil {
- common.Error(c, http.StatusBadRequest, err.Error())
+ if errors.Is(err, common.ErrPostCannotUnlock) || errors.Is(err, common.ErrPostNotFound) {
+ common.Error(c, http.StatusBadRequest, err.Error())
+ } else {
+ common.Error(c, http.StatusInternalServerError, "解锁失败")
+ }
return
}
@@ -142,7 +159,7 @@ func (ctrl *AdminPostController) Restore(c *gin.Context) {
}
if err := ctrl.postService.Restore(uint(id)); err != nil {
- common.Error(c, http.StatusBadRequest, err.Error())
+ common.Error(c, http.StatusInternalServerError, "恢复失败")
return
}
diff --git a/internal/controller/post_controller.go b/internal/controller/post_controller.go
index d42e131..df14921 100644
--- a/internal/controller/post_controller.go
+++ b/internal/controller/post_controller.go
@@ -1,6 +1,7 @@
package controller
import (
+ "errors"
"fmt"
"html/template"
"mime/multipart"
@@ -144,6 +145,22 @@ func (ctrl *PostController) EditPage(c *gin.Context) {
return
}
+ // 权限检查:仅作者和 moderator+ 可编辑
+ var uid uint
+ var role string
+ if uidObj, exists := c.Get("uid"); exists {
+ uid, _ = uidObj.(uint)
+ }
+ if roleObj, exists := c.Get("role"); exists {
+ role, _ = roleObj.(string)
+ }
+ if post.UserID != uid && !model.HasMinRole(role, model.RoleModerator) {
+ c.HTML(http.StatusNotFound, "posts/404.html", common.BuildPageData(c, gin.H{
+ "Title": "未找到",
+ }))
+ return
+ }
+
c.HTML(http.StatusOK, "posts/new.html", common.BuildPageData(c, gin.H{
"Title": "编辑帖子",
"Post": post,
@@ -178,7 +195,11 @@ func (ctrl *PostController) Create(c *gin.Context) {
// Update API 编辑帖子
func (ctrl *PostController) Update(c *gin.Context) {
uidObj, _ := c.Get("uid")
- uid, _ := uidObj.(uint)
+ uid, ok := uidObj.(uint)
+ if !ok {
+ common.Error(c, http.StatusUnauthorized, "请先登录")
+ return
+ }
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
@@ -206,7 +227,11 @@ func (ctrl *PostController) Update(c *gin.Context) {
}
if err := ctrl.postService.Update(uint(id), req.Title, req.Body); err != nil {
- common.Error(c, http.StatusBadRequest, err.Error())
+ if errors.Is(err, common.ErrPostCannotEdit) {
+ common.Error(c, http.StatusBadRequest, err.Error())
+ } else {
+ common.Error(c, http.StatusInternalServerError, "编辑失败")
+ }
return
}
@@ -216,7 +241,11 @@ func (ctrl *PostController) Update(c *gin.Context) {
// Delete API 删除帖子
func (ctrl *PostController) Delete(c *gin.Context) {
uidObj, _ := c.Get("uid")
- uid, _ := uidObj.(uint)
+ uid, ok := uidObj.(uint)
+ if !ok {
+ common.Error(c, http.StatusUnauthorized, "请先登录")
+ return
+ }
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
@@ -254,7 +283,11 @@ func (ctrl *PostController) Submit(c *gin.Context) {
}
if err := ctrl.postService.SubmitForAudit(uint(id)); err != nil {
- common.Error(c, http.StatusBadRequest, err.Error())
+ if errors.Is(err, common.ErrPostCannotSubmit) || errors.Is(err, common.ErrPostNotFound) {
+ common.Error(c, http.StatusBadRequest, err.Error())
+ } else {
+ common.Error(c, http.StatusInternalServerError, "提交审核失败")
+ }
return
}
@@ -303,6 +336,24 @@ func (ctrl *PostController) ShowAPI(c *gin.Context) {
return
}
+ // 权限控制:非 approved 帖子仅作者和 moderator+ 可见
+ if post.Status != model.PostStatusApproved {
+ var uid uint
+ var role string
+ if uidObj, exists := c.Get("uid"); exists {
+ uid, _ = uidObj.(uint)
+ }
+ if roleObj, exists := c.Get("role"); exists {
+ role, _ = roleObj.(string)
+ }
+ isAuthor := uid == post.UserID
+ isModerator := model.HasMinRole(role, model.RoleModerator)
+ if !isAuthor && !isModerator {
+ common.Error(c, http.StatusNotFound, "帖子不存在")
+ return
+ }
+ }
+
common.Ok(c, post)
}
diff --git a/internal/service/post_service.go b/internal/service/post_service.go
index 9edf841..f396eda 100644
--- a/internal/service/post_service.go
+++ b/internal/service/post_service.go
@@ -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)
diff --git a/internal/service/repository.go b/internal/service/repository.go
index e1a0c8b..6c4c2a5 100644
--- a/internal/service/repository.go
+++ b/internal/service/repository.go
@@ -31,5 +31,14 @@ type userAdminStore interface {
IncrementTokenVersion(userID uint) error
}
-
-
+// postStore PostService 所需的最小仓储接口(ISP:8 个方法)
+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
+}
\ No newline at end of file
diff --git a/templates/MetaLab-2026/html/posts/new.html b/templates/MetaLab-2026/html/posts/new.html
index c32cba0..4664828 100644
--- a/templates/MetaLab-2026/html/posts/new.html
+++ b/templates/MetaLab-2026/html/posts/new.html
@@ -282,4 +282,3 @@
-