fix: 锁定机制不再改变帖子状态,锁定与退回独立
- Post 模型新增 IsLocked 字段,与 Status 解耦 - Lock 仅设 IsLocked=true(不改 Status),已通过帖子锁定后仍公开可见 - Unlock 仅设 IsLocked=false(不改 Status) - Update/EditPage 编辑检查改用 IsLocked 而非 Status==locked - Reject 与 Lock 独立,可单独退回或锁定+退回组合使用 - 新增 ErrPostCannotLock 错误哨兵 - 前端模板编辑按钮/锁定标识基于 IsLocked 渲染
This commit is contained in:
@ -37,10 +37,11 @@ var (
|
|||||||
ErrRegistrationDisabled = errors.New("注册功能已关闭")
|
ErrRegistrationDisabled = errors.New("注册功能已关闭")
|
||||||
|
|
||||||
// 帖子相关
|
// 帖子相关
|
||||||
ErrPostNotFound = errors.New("帖子不存在")
|
ErrPostNotFound = errors.New("帖子不存在")
|
||||||
ErrPostCannotEdit = errors.New("当前状态不允许编辑")
|
ErrPostCannotEdit = errors.New("当前状态不允许编辑")
|
||||||
ErrPostCannotSubmit = errors.New("仅草稿和已退回帖子可提交审核")
|
ErrPostCannotSubmit = errors.New("仅草稿和已退回帖子可提交审核")
|
||||||
ErrPostCannotApprove = errors.New("仅待审核帖子可通过")
|
ErrPostCannotApprove = errors.New("仅待审核帖子可通过")
|
||||||
ErrPostCannotReject = errors.New("仅待审核和已发布帖子可退回")
|
ErrPostCannotReject = errors.New("仅待审核和已发布帖子可退回")
|
||||||
ErrPostCannotUnlock = errors.New("仅锁定状态可解锁")
|
ErrPostCannotLock = errors.New("待审核帖子不允许锁定")
|
||||||
|
ErrPostCannotUnlock = errors.New("仅锁定状态可解锁")
|
||||||
)
|
)
|
||||||
|
|||||||
@ -117,7 +117,7 @@ func (ctrl *AdminPostController) Lock(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := ctrl.postService.Lock(uint(id)); err != nil {
|
if err := ctrl.postService.Lock(uint(id)); err != nil {
|
||||||
if errors.Is(err, common.ErrPostNotFound) {
|
if errors.Is(err, common.ErrPostNotFound) || errors.Is(err, common.ErrPostCannotLock) {
|
||||||
common.Error(c, http.StatusBadRequest, err.Error())
|
common.Error(c, http.StatusBadRequest, err.Error())
|
||||||
} else {
|
} else {
|
||||||
common.Error(c, http.StatusInternalServerError, "锁定失败")
|
common.Error(c, http.StatusInternalServerError, "锁定失败")
|
||||||
|
|||||||
@ -139,7 +139,7 @@ func (ctrl *PostController) EditPage(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
post, err := ctrl.postService.GetByID(uint(id))
|
post, err := ctrl.postService.GetByID(uint(id))
|
||||||
if err != nil || post.Status == model.PostStatusPending || post.Status == model.PostStatusLocked {
|
if err != nil || post.Status == model.PostStatusPending || post.IsLocked {
|
||||||
c.HTML(http.StatusNotFound, "posts/404.html", common.BuildPageData(c, gin.H{
|
c.HTML(http.StatusNotFound, "posts/404.html", common.BuildPageData(c, gin.H{
|
||||||
"Title": "未找到或无法编辑",
|
"Title": "未找到或无法编辑",
|
||||||
}))
|
}))
|
||||||
|
|||||||
@ -14,6 +14,7 @@ type Post struct {
|
|||||||
Excerpt string `gorm:"type:varchar(500)" json:"excerpt"` // plain text summary for list
|
Excerpt string `gorm:"type:varchar(500)" json:"excerpt"` // plain text summary for list
|
||||||
UserID uint `gorm:"index;not null" json:"user_id"`
|
UserID uint `gorm:"index;not null" json:"user_id"`
|
||||||
Status string `gorm:"type:varchar(20);index;default:draft;not null" json:"status"`
|
Status string `gorm:"type:varchar(20);index;default:draft;not null" json:"status"`
|
||||||
|
IsLocked bool `gorm:"default:false" json:"is_locked"`
|
||||||
RejectReason string `gorm:"type:varchar(500);default:''" json:"reject_reason,omitempty"`
|
RejectReason string `gorm:"type:varchar(500);default:''" json:"reject_reason,omitempty"`
|
||||||
AllowComment bool `gorm:"default:true" json:"allow_comment"`
|
AllowComment bool `gorm:"default:true" json:"allow_comment"`
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
|
|||||||
@ -213,7 +213,7 @@ func (s *PostService) Update(postID uint, title, body string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if post.Status == model.PostStatusPending || post.Status == model.PostStatusLocked {
|
if post.Status == model.PostStatusPending || post.IsLocked {
|
||||||
return common.ErrPostCannotEdit
|
return common.ErrPostCannotEdit
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -263,6 +263,7 @@ func (s *PostService) Approve(postID uint) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Reject 退回:pending/approved → rejected
|
// Reject 退回:pending/approved → rejected
|
||||||
|
// 独立于锁定:可单独退回(允许作者修改后重审),也可锁定+退回(禁止再投稿)
|
||||||
func (s *PostService) Reject(postID uint, reason string) error {
|
func (s *PostService) Reject(postID uint, reason string) error {
|
||||||
post, err := s.findPost(postID)
|
post, err := s.findPost(postID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -276,26 +277,30 @@ func (s *PostService) Reject(postID uint, reason string) error {
|
|||||||
return s.repo.Update(post)
|
return s.repo.Update(post)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lock 锁定:任意状态 → locked(管理员可随时锁定)
|
// Lock 锁定:设置 IsLocked 标志,禁止编辑,不改变帖子发布状态
|
||||||
func (s *PostService) Lock(postID uint) error {
|
func (s *PostService) Lock(postID uint) error {
|
||||||
post, err := s.findPost(postID)
|
post, err := s.findPost(postID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
post.Status = model.PostStatusLocked
|
// 待审核帖子不允许锁定(审核流程中)
|
||||||
|
if post.Status == model.PostStatusPending {
|
||||||
|
return common.ErrPostCannotLock
|
||||||
|
}
|
||||||
|
post.IsLocked = true
|
||||||
return s.repo.Update(post)
|
return s.repo.Update(post)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unlock 解锁:locked → 草稿
|
// Unlock 解锁:清除 IsLocked 标志,恢复可编辑
|
||||||
func (s *PostService) Unlock(postID uint) error {
|
func (s *PostService) Unlock(postID uint) error {
|
||||||
post, err := s.findPost(postID)
|
post, err := s.findPost(postID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if post.Status != model.PostStatusLocked {
|
if !post.IsLocked {
|
||||||
return common.ErrPostCannotUnlock
|
return common.ErrPostCannotUnlock
|
||||||
}
|
}
|
||||||
post.Status = model.PostStatusDraft
|
post.IsLocked = false
|
||||||
return s.repo.Update(post)
|
return s.repo.Update(post)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -13,6 +13,9 @@
|
|||||||
{{if ne .Post.Status "approved"}}
|
{{if ne .Post.Status "approved"}}
|
||||||
<span class="post-status post-status-{{.Post.Status}}">{{index $.StatusNames .Post.Status}}</span>
|
<span class="post-status post-status-{{.Post.Status}}">{{index $.StatusNames .Post.Status}}</span>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
{{if .Post.IsLocked}}
|
||||||
|
<span class="post-status post-status-locked">已锁定</span>
|
||||||
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@ -27,7 +30,7 @@
|
|||||||
|
|
||||||
{{if .IsLoggedIn}}
|
{{if .IsLoggedIn}}
|
||||||
<div class="post-actions">
|
<div class="post-actions">
|
||||||
{{if and $.Post (or (eq $.Post.UserID $.UID) $.CanAccessAdmin) (ne $.Post.Status "pending") (ne $.Post.Status "locked")}}
|
{{if and $.Post (or (eq $.Post.UserID $.UID) $.CanAccessAdmin) (ne $.Post.Status "pending") (not $.Post.IsLocked)}}
|
||||||
<a href="/posts/{{$.Post.ID}}/edit" class="btn btn-secondary">编辑</a>
|
<a href="/posts/{{$.Post.ID}}/edit" class="btn btn-secondary">编辑</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if and $.Post (eq $.Post.UserID $.UID) (or (eq $.Post.Status "draft") (eq $.Post.Status "rejected"))}}
|
{{if and $.Post (eq $.Post.UserID $.UID) (or (eq $.Post.Status "draft") (eq $.Post.Status "rejected"))}}
|
||||||
|
|||||||
@ -13,7 +13,6 @@
|
|||||||
<option value="pending" {{if eq .Status "pending"}}selected{{end}}>待审核</option>
|
<option value="pending" {{if eq .Status "pending"}}selected{{end}}>待审核</option>
|
||||||
<option value="approved" {{if eq .Status "approved"}}selected{{end}}>已发布</option>
|
<option value="approved" {{if eq .Status "approved"}}selected{{end}}>已发布</option>
|
||||||
<option value="rejected" {{if eq .Status "rejected"}}selected{{end}}>已退回</option>
|
<option value="rejected" {{if eq .Status "rejected"}}selected{{end}}>已退回</option>
|
||||||
<option value="locked" {{if eq .Status "locked"}}selected{{end}}>已锁定</option>
|
|
||||||
</select>
|
</select>
|
||||||
<button type="submit" class="btn btn-primary">搜索</button>
|
<button type="submit" class="btn btn-primary">搜索</button>
|
||||||
</form>
|
</form>
|
||||||
@ -43,7 +42,7 @@
|
|||||||
<td>{{.ID}}</td>
|
<td>{{.ID}}</td>
|
||||||
<td><a href="/posts/{{.ID}}" target="_blank" class="post-link">{{.Title}}</a></td>
|
<td><a href="/posts/{{.ID}}" target="_blank" class="post-link">{{.Title}}</a></td>
|
||||||
<td>{{if .AuthorName}}{{.AuthorName}}({{.UserID}}){{else}}UID{{.UserID}}{{end}}</td>
|
<td>{{if .AuthorName}}{{.AuthorName}}({{.UserID}}){{else}}UID{{.UserID}}{{end}}</td>
|
||||||
<td><span class="status-badge status-{{.Status}}">{{index $.StatusNames .Status}}</span></td>
|
<td><span class="status-badge status-{{.Status}}">{{index $.StatusNames .Status}}</span>{{if .IsLocked}} <span class="status-badge status-locked">已锁定</span>{{end}}</td>
|
||||||
<td>{{.CreatedAt.Format "2006-01-02 15:04"}}</td>
|
<td>{{.CreatedAt.Format "2006-01-02 15:04"}}</td>
|
||||||
<td class="actions-cell">
|
<td class="actions-cell">
|
||||||
<div class="action-btns">
|
<div class="action-btns">
|
||||||
@ -54,10 +53,10 @@
|
|||||||
{{if eq .Status "approved"}}
|
{{if eq .Status "approved"}}
|
||||||
<button class="btn btn-sm btn-warning post-reject" data-id="{{.ID}}">退回</button>
|
<button class="btn btn-sm btn-warning post-reject" data-id="{{.ID}}">退回</button>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if and (ne .Status "locked") (ne .Status "pending")}}
|
{{if and (not .IsLocked) (ne .Status "pending")}}
|
||||||
<button class="btn btn-sm btn-danger post-lock" data-id="{{.ID}}">锁定</button>
|
<button class="btn btn-sm btn-danger post-lock" data-id="{{.ID}}">锁定</button>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if eq .Status "locked"}}
|
{{if .IsLocked}}
|
||||||
<button class="btn btn-sm btn-success post-unlock" data-id="{{.ID}}">解锁</button>
|
<button class="btn btn-sm btn-success post-unlock" data-id="{{.ID}}">解锁</button>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user