diff --git a/internal/controller/admin/admin_post_controller.go b/internal/controller/admin/admin_post_controller.go index 8e488cd..24e0efd 100644 --- a/internal/controller/admin/admin_post_controller.go +++ b/internal/controller/admin/admin_post_controller.go @@ -47,6 +47,9 @@ func (ctrl *AdminPostController) PostsPage(c *gin.Context) { nextPage = totalPages } + // 查询待审修订(approved 帖子有 PendingBody 的) + revisions, _, _ := ctrl.postService.ListPendingRevisions(keyword, 1, 100) + c.HTML(http.StatusOK, "admin/posts/index.html", common.BuildAdminPageData(c, gin.H{ "Title": "内容管理", "Posts": posts, @@ -58,6 +61,7 @@ func (ctrl *AdminPostController) PostsPage(c *gin.Context) { "Keyword": keyword, "Status": status, "StatusNames": common.PostStatusDisplayNames, + "Revisions": revisions, "ExtraCSS": "/admin/static/css/posts.css", })) } diff --git a/internal/controller/admin/interfaces.go b/internal/controller/admin/interfaces.go index 3768887..68ad123 100644 --- a/internal/controller/admin/interfaces.go +++ b/internal/controller/admin/interfaces.go @@ -33,9 +33,10 @@ type auditStatusProvider interface { FindByID(id uint) (*model.User, error) } -// adminPostUseCase AdminPostController 对 PostService 的最小依赖(ISP:6 个方法) +// adminPostUseCase AdminPostController 对 PostService 的最小依赖(ISP:7 个方法) type adminPostUseCase interface { ListAdmin(keyword, status string, page, pageSize int) ([]model.Post, int64, error) + ListPendingRevisions(keyword string, page, pageSize int) ([]model.Post, int64, error) Approve(postID uint) error Reject(postID uint, reason string) error Lock(postID uint) error diff --git a/internal/controller/post_controller.go b/internal/controller/post_controller.go index 2cd9f2f..a7668bd 100644 --- a/internal/controller/post_controller.go +++ b/internal/controller/post_controller.go @@ -154,6 +154,12 @@ func (ctrl *PostController) EditPage(c *gin.Context) { return } + // 已通过帖子有待审修订时,编辑页展示待审内容 + if post.PendingBody != "" { + post.Title = post.PendingTitle + post.Body = post.PendingBody + } + c.HTML(http.StatusOK, "posts/new.html", common.BuildPageData(c, gin.H{ "Title": "编辑帖子", "Post": post, diff --git a/internal/model/post.go b/internal/model/post.go index 0d51946..77a85ce 100644 --- a/internal/model/post.go +++ b/internal/model/post.go @@ -15,6 +15,8 @@ type Post struct { UserID uint `gorm:"index;not null" json:"user_id"` Status string `gorm:"type:varchar(20);index;default:draft;not null" json:"status"` IsLocked bool `gorm:"default:false" json:"is_locked"` + PendingTitle string `gorm:"type:varchar(200);default:''" json:"pending_title,omitempty"` + PendingBody string `gorm:"type:text" json:"pending_body,omitempty"` RejectReason string `gorm:"type:varchar(500);default:''" json:"reject_reason,omitempty"` AllowComment bool `gorm:"default:true" json:"allow_comment"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` diff --git a/internal/repository/post_repo.go b/internal/repository/post_repo.go index 60ac148..d25203d 100644 --- a/internal/repository/post_repo.go +++ b/internal/repository/post_repo.go @@ -88,6 +88,13 @@ func (r *PostRepo) FindByUserIDAndStatus(userID uint, status string, offset, lim return r.pageResults(query, offset, limit) } +// FindPendingRevisions 查询有待审修订的帖子(approved 且 pending_body 不为空) +func (r *PostRepo) FindPendingRevisions(keyword string, offset, limit int) ([]model.Post, int64, error) { + query := r.buildQuery(keyword, model.PostStatusApproved). + Where("posts.pending_body != ''") + return r.pageResults(query, offset, limit) +} + // pageResults 执行 Count + Offset/Limit + ORDER BY func (r *PostRepo) pageResults(query *gorm.DB, offset, limit int) ([]model.Post, int64, error) { var total int64 diff --git a/internal/service/post_service.go b/internal/service/post_service.go index 218b43a..2a55835 100644 --- a/internal/service/post_service.go +++ b/internal/service/post_service.go @@ -217,11 +217,19 @@ func (s *PostService) Update(postID uint, title, body string) error { return common.ErrPostCannotEdit } + // 已通过帖子:编辑内容保存为待审修订,不影响当前展示 + if post.Status == model.PostStatusApproved { + post.PendingTitle = title + post.PendingBody = body + post.RejectReason = "" // 清空旧退回理由 + return s.repo.Update(post) + } + + // 草稿/已退回:直接修改原文(现有逻辑) post.Title = title post.Body = body post.Excerpt = generateExcerpt(body) - // rejected 状态编辑后自动重置为 draft if post.Status == model.PostStatusRejected { post.Status = model.PostStatusDraft post.RejectReason = "" @@ -248,12 +256,26 @@ func (s *PostService) SubmitForAudit(postID uint) error { return s.repo.Update(post) } -// Approve 审核通过:pending → approved +// Approve 审核通过:pending → approved;或修订审核通过复制 Pending 到正式字段 func (s *PostService) Approve(postID uint) error { post, err := s.findPost(postID) if err != nil { return err } + + // 修订审核:将待审内容覆盖到正式字段 + if post.PendingBody != "" { + post.Title = post.PendingTitle + post.Body = post.PendingBody + post.Excerpt = generateExcerpt(post.PendingBody) + post.PendingTitle = "" + post.PendingBody = "" + post.Status = model.PostStatusApproved + post.RejectReason = "" + return s.repo.Update(post) + } + + // 首次审核通过 if post.Status != model.PostStatusPending { return common.ErrPostCannotApprove } @@ -262,8 +284,7 @@ func (s *PostService) Approve(postID uint) error { return s.repo.Update(post) } -// Reject 退回:pending/approved → rejected -// 独立于锁定:可单独退回(允许作者修改后重审),也可锁定+退回(禁止再投稿) +// Reject 退回:pending/approved → rejected;修订退回则清空 Pending 保持 approved func (s *PostService) Reject(postID uint, reason string) error { post, err := s.findPost(postID) if err != nil { @@ -272,6 +293,16 @@ func (s *PostService) Reject(postID uint, reason string) error { if post.Status != model.PostStatusPending && post.Status != model.PostStatusApproved { return common.ErrPostCannotReject } + + // 修订退回:清空待审修订,保持已发布内容不变 + if post.PendingBody != "" { + post.PendingTitle = "" + post.PendingBody = "" + post.RejectReason = reason + return s.repo.Update(post) + } + + // 普通退回(首次审核) post.Status = model.PostStatusRejected post.RejectReason = reason return s.repo.Update(post) @@ -316,6 +347,13 @@ func (s *PostService) Restore(postID uint) error { return nil } +// ListPendingRevisions 查询有待审修订的帖子(approved 且 PendingBody 不为空) +func (s *PostService) ListPendingRevisions(keyword string, page, pageSize int) ([]model.Post, int64, error) { + return s.listPageable(page, pageSize, func(offset, limit int) ([]model.Post, int64, error) { + return s.repo.FindPendingRevisions(keyword, offset, limit) + }) +} + // IsPostAccessible 检查用户是否有权限访问/操作帖子(作者或 moderator+) func (s *PostService) IsPostAccessible(userID uint, role string, postUserID uint) bool { return userID == postUserID || model.HasMinRole(role, model.RoleModerator) diff --git a/internal/service/repository.go b/internal/service/repository.go index 4e5809b..96edfd3 100644 --- a/internal/service/repository.go +++ b/internal/service/repository.go @@ -31,7 +31,7 @@ type userAdminStore interface { IncrementTokenVersion(userID uint) error } -// postStore PostService 所需的最小仓储接口(ISP:9 个方法) +// postStore PostService 所需的最小仓储接口(ISP:10 个方法) type postStore interface { Create(post *model.Post) error FindByID(id uint) (*model.Post, error) @@ -39,6 +39,7 @@ type postStore interface { FindPageable(keyword string, offset, limit int) ([]model.Post, int64, error) FindAdminPageable(keyword, status string, offset, limit int) ([]model.Post, int64, error) FindByUserIDAndStatus(userID uint, status string, offset, limit int) ([]model.Post, int64, error) + FindPendingRevisions(keyword string, offset, limit int) ([]model.Post, int64, error) Update(post *model.Post) error SoftDelete(id uint) error Restore(id uint) error diff --git a/templates/MetaLab-2026/html/posts/show.html b/templates/MetaLab-2026/html/posts/show.html index 15a134c..677a6e6 100644 --- a/templates/MetaLab-2026/html/posts/show.html +++ b/templates/MetaLab-2026/html/posts/show.html @@ -16,6 +16,9 @@ {{if .Post.IsLocked}} 已锁定 {{end}} + {{if .Post.PendingBody}} + 修订审核中 + {{end}} diff --git a/templates/MetaLab-2026/static/css/posts.css b/templates/MetaLab-2026/static/css/posts.css index 629afc3..01ea963 100644 --- a/templates/MetaLab-2026/static/css/posts.css +++ b/templates/MetaLab-2026/static/css/posts.css @@ -179,6 +179,7 @@ .post-status-approved { background: #d1fae5; color: #059669; } .post-status-rejected { background: #fef2f2; color: #dc2626; } .post-status-locked { background: #fee2e2; color: #b91c1c; } +.post-status-revision { background: #e0e7ff; color: #4338ca; } .post-reject-reason { margin-top: 20px; diff --git a/templates/admin/html/posts/index.html b/templates/admin/html/posts/index.html index 2b18f99..a227e97 100644 --- a/templates/admin/html/posts/index.html +++ b/templates/admin/html/posts/index.html @@ -22,6 +22,47 @@
{{end}} + {{if .Revisions}} +| ID | +标题 | +作者 | +状态 | +时间 | +操作 | +
|---|---|---|---|---|---|
| {{.ID}} | +{{.Title}} 修订中 | +{{if .AuthorName}}{{.AuthorName}}({{.UserID}}){{else}}UID{{.UserID}}{{end}} | +{{index $.StatusNames .Status}}{{if .IsLocked}} 已锁定{{end}} | +{{.CreatedAt.Format "2006-01-02 15:04"}} | +
+
+
+
+ {{if and (not .IsLocked)}}
+
+ {{end}}
+ {{if .IsLocked}}
+
+ {{end}}
+
+ |
+
| 暂无帖子 | |||||
| {{.ID}} | -{{.Title}} | +{{.Title}}{{if .PendingBody}} 修订中{{end}} | {{if .AuthorName}}{{.AuthorName}}({{.UserID}}){{else}}UID{{.UserID}}{{end}} | {{index $.StatusNames .Status}}{{if .IsLocked}} 已锁定{{end}} | {{.CreatedAt.Format "2006-01-02 15:04"}} | @@ -50,9 +91,13 @@ {{end}} - {{if eq .Status "approved"}} + {{if and (eq .Status "approved") (not .PendingBody)}} {{end}} + {{if and (eq .Status "approved") .PendingBody}} + + + {{end}} {{if and (not .IsLocked) (ne .Status "pending")}} {{end}} diff --git a/templates/admin/static/css/posts.css b/templates/admin/static/css/posts.css index 5470847..fbe08e5 100644 --- a/templates/admin/static/css/posts.css +++ b/templates/admin/static/css/posts.css @@ -81,6 +81,13 @@ .status-rejected { background: #fee2e2; color: #dc2626; } .status-locked { background: #fee2e2; color: #b91c1c; } +.revision-section { margin-bottom: 24px; } +.revision-section h2 { font-size: 16px; font-weight: 600; margin-bottom: 12px; color: #4338ca; } +.revision-count { background: #4338ca; color: #fff; border-radius: 10px; padding: 1px 8px; font-size: 13px; } +.revision-table { border: 2px solid #c7d2fe; } +.revision-row { background: #eef2ff; } +.revision-badge { background: #e0e7ff; color: #4338ca; font-size: 11px; border-radius: 4px; padding: 1px 6px; margin-left: 6px; } + .actions-cell { min-width: 180px; } .action-btns {