feat: 已发布帖子编辑后进修订审核,通过前保持展示旧内容

- Post 模型新增 PendingTitle/PendingBody 字段,分离已发布内容与待审修订
- Update: approved 帖子编辑写入 Pending 字段,不覆盖正文,不改变发布状态
- Approve: 有 PendingBody 时复制到正式字段后清空,无 PendingBody 走首次审核
- Reject: 有 PendingBody 时仅清空待审修订(保持已发布),无 PendingBody 走普通退回
- 新增 ListPendingRevisions 查询,管理后台独立展示"修订待审核"区域
- 详情页展示"修订审核中"标识,编辑页加载待审内容
- 通过修订后直接覆盖,不保留历史版本
This commit is contained in:
2026-05-30 20:24:43 +08:00
parent 89fa50a013
commit eabc1e7d3d
11 changed files with 124 additions and 9 deletions

View File

@ -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)

View File

@ -31,7 +31,7 @@ type userAdminStore interface {
IncrementTokenVersion(userID uint) error
}
// postStore PostService 所需的最小仓储接口ISP9 个方法)
// postStore PostService 所需的最小仓储接口ISP10 个方法)
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