feat: 锁定文章需填写锁定理由,编辑器显示锁定理由

- Post 模型新增 LockReason 字段,锁定必填理由,解锁清空
- PostLockRequest DTO:reason 必填,1-500字
- Admin JS:锁定操作弹出 prompt 要求填写理由
- 编辑器锁定横幅显示"锁定理由:XXX",理由为空时不显示该行
This commit is contained in:
2026-05-31 20:10:55 +08:00
parent b658eeb8e9
commit 968e8af8a3
8 changed files with 23 additions and 6 deletions

View File

@ -116,7 +116,13 @@ func (ctrl *AdminPostController) Lock(c *gin.Context) {
return return
} }
if err := ctrl.postService.Lock(uint(id)); err != nil { var req model.PostLockRequest
if err := c.ShouldBindJSON(&req); err != nil {
common.Error(c, http.StatusBadRequest, "请填写锁定理由")
return
}
if err := ctrl.postService.Lock(uint(id), req.Reason); err != nil {
if errors.Is(err, common.ErrPostNotFound) || errors.Is(err, common.ErrPostCannotLock) { 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 {

View File

@ -42,7 +42,7 @@ type adminPostUseCase interface {
ListPendingRevisions(keyword string, page, pageSize int) ([]model.Post, int64, error) ListPendingRevisions(keyword string, page, pageSize int) ([]model.Post, int64, error)
Approve(postID uint) error Approve(postID uint) error
Reject(postID uint, reason string) error Reject(postID uint, reason string) error
Lock(postID uint) error Lock(postID uint, reason string) error
Unlock(postID uint) error Unlock(postID uint) error
Restore(postID uint) error Restore(postID uint) error
} }

View File

@ -154,7 +154,6 @@ func (ctrl *StudioController) WritePage(c *gin.Context) {
"Post": post, "Post": post,
"ActiveTab": "write", "ActiveTab": "write",
"ExtraCSS": "/static/css/studio.css", "ExtraCSS": "/static/css/studio.css",
"StatusNames": common.PostStatusDisplayNames,
})) }))
return return
} }

View File

@ -47,6 +47,11 @@ type PostRejectRequest struct {
Reason string `json:"reason" binding:"required,min=1,max=500"` Reason string `json:"reason" binding:"required,min=1,max=500"`
} }
// PostLockRequest 锁定请求
type PostLockRequest struct {
Reason string `json:"reason" binding:"required,min=1,max=500"`
}
// PostListQuery 列表查询参数 // PostListQuery 列表查询参数
type PostListQuery struct { type PostListQuery struct {
Keyword string `form:"keyword"` Keyword string `form:"keyword"`

View File

@ -15,6 +15,7 @@ type Post struct {
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"` IsLocked bool `gorm:"default:false" json:"is_locked"`
LockReason string `gorm:"type:varchar(500);default:''" json:"lock_reason,omitempty"`
PendingTitle string `gorm:"type:varchar(200);default:''" json:"pending_title,omitempty"` PendingTitle string `gorm:"type:varchar(200);default:''" json:"pending_title,omitempty"`
PendingBody string `gorm:"type:text" json:"pending_body,omitempty"` PendingBody string `gorm:"type:text" json:"pending_body,omitempty"`
RejectReason string `gorm:"type:varchar(500);default:''" json:"reject_reason,omitempty"` RejectReason string `gorm:"type:varchar(500);default:''" json:"reject_reason,omitempty"`

View File

@ -341,7 +341,7 @@ func (s *PostService) Reject(postID uint, reason string) error {
} }
// Lock 锁定:设置 IsLocked 标志,禁止编辑,不改变帖子发布状态 // Lock 锁定:设置 IsLocked 标志,禁止编辑,不改变帖子发布状态
func (s *PostService) Lock(postID uint) error { func (s *PostService) Lock(postID uint, reason string) error {
post, err := s.findPost(postID) post, err := s.findPost(postID)
if err != nil { if err != nil {
return err return err
@ -351,6 +351,7 @@ func (s *PostService) Lock(postID uint) error {
return common.ErrPostCannotLock return common.ErrPostCannotLock
} }
post.IsLocked = true post.IsLocked = true
post.LockReason = reason
return s.repo.Update(post) return s.repo.Update(post)
} }
@ -364,6 +365,7 @@ func (s *PostService) Unlock(postID uint) error {
return common.ErrPostCannotUnlock return common.ErrPostCannotUnlock
} }
post.IsLocked = false post.IsLocked = false
post.LockReason = ""
return s.repo.Update(post) return s.repo.Update(post)
} }

View File

@ -13,7 +13,9 @@
<div class="locked-banner-icon">🔒</div> <div class="locked-banner-icon">🔒</div>
<div> <div>
<strong>这篇文章已被锁定,无法编辑。</strong> <strong>这篇文章已被锁定,无法编辑。</strong>
<p>如需解锁请联系管理员。当前状态:{{if $.StatusNames}}{{index $.StatusNames .Post.Status}}、已锁定{{end}}</p> {{if .Post.LockReason}}
<p>锁定理由:{{.Post.LockReason}}</p>
{{end}}
</div> </div>
</div> </div>
{{end}} {{end}}

View File

@ -47,7 +47,9 @@
// 锁定 // 锁定
handleClick('.post-lock', function(id) { handleClick('.post-lock', function(id) {
api('/api/admin/posts/' + id + '/lock', 'POST') var reason = prompt('请输入锁定理由(必填):');
if (!reason || !reason.trim()) return;
api('/api/admin/posts/' + id + '/lock', 'POST', { reason: reason.trim() })
.then(function(d) { if (d.success) refreshPage(); else alert(d.message); }) .then(function(d) { if (d.success) refreshPage(); else alert(d.message); })
.catch(function() { alert('操作失败'); }); .catch(function() { alert('操作失败'); });
}); });