- Post ScheduledAt 字段 - Scheduler goroutine 每分钟扫描到期文章 - Create/Update 校验最短 30min 定时 - Approve 审核晚于定时则即时发布 - public 列表+空间页过滤未到期文章 - 写文章页 datetime-local 选择器(min=now+30min) - AutoMigrate 追加新表
240 lines
6.2 KiB
Go
240 lines
6.2 KiB
Go
package service
|
||
|
||
import (
|
||
"strconv"
|
||
"time"
|
||
|
||
"metazone.cc/mce/internal/common"
|
||
"metazone.cc/mce/internal/model"
|
||
)
|
||
|
||
// Update 编辑帖子(权限在 controller 层检查)
|
||
func (s *PostService) Update(postID uint, title, body string, visibility, postType, reprintSource, declaration string, reprintProhibited bool, categoryID, scheduledAt string) error {
|
||
post, err := s.findPost(postID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
if post.Status == model.PostStatusPending || post.IsLocked {
|
||
return common.ErrPostCannotEdit
|
||
}
|
||
|
||
// 文章属性随时可更新(不影响已发布内容展示)
|
||
post.Visibility = visibility
|
||
post.PostType = postType
|
||
post.ReprintSource = reprintSource
|
||
post.Declaration = declaration
|
||
post.ReprintProhibited = reprintProhibited
|
||
// 分类
|
||
if cid, err := strconv.ParseUint(categoryID, 10, 64); err == nil && cid > 0 {
|
||
id := uint(cid)
|
||
post.CategoryID = &id
|
||
} else {
|
||
post.CategoryID = nil
|
||
}
|
||
// 定时发布
|
||
if t, err := parseScheduledAt(scheduledAt); err == nil {
|
||
if time.Until(t) < 30*time.Minute {
|
||
return common.ErrScheduledTooSoon
|
||
}
|
||
post.ScheduledAt = &t
|
||
}
|
||
|
||
// 已通过帖子:编辑内容保存为待审修订,不影响当前展示
|
||
if post.Status == model.PostStatusApproved {
|
||
post.PendingTitle = title
|
||
post.PendingBody = body
|
||
post.RejectReason = "" // 清空旧退回理由
|
||
err := s.repo.Update(post)
|
||
if err == nil {
|
||
s.invalidateHomeCache()
|
||
}
|
||
return err
|
||
}
|
||
|
||
// 草稿/已退回:直接修改原文(现有逻辑)
|
||
post.Title = title
|
||
post.Body = body
|
||
post.Excerpt = generateExcerpt(body)
|
||
|
||
if post.Status == model.PostStatusRejected {
|
||
post.Status = model.PostStatusDraft
|
||
post.RejectReason = ""
|
||
}
|
||
|
||
err = s.repo.Update(post)
|
||
if err == nil {
|
||
s.invalidateHomeCache()
|
||
}
|
||
return err
|
||
}
|
||
|
||
// Delete 软删除(权限在 controller 层检查)
|
||
func (s *PostService) Delete(postID uint) error {
|
||
if err := s.repo.SoftDelete(postID); err != nil {
|
||
return err
|
||
}
|
||
s.invalidateHomeCache()
|
||
return nil
|
||
}
|
||
|
||
// SubmitForAudit 提交审核:draft → pending
|
||
func (s *PostService) SubmitForAudit(postID uint) error {
|
||
post, err := s.findPost(postID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if post.IsLocked {
|
||
return common.ErrPostCannotEdit
|
||
}
|
||
if post.Status != model.PostStatusDraft && post.Status != model.PostStatusRejected {
|
||
return common.ErrPostCannotSubmit
|
||
}
|
||
post.Status = model.PostStatusPending
|
||
return s.repo.Update(post)
|
||
}
|
||
|
||
// Approve 审核通过:pending → approved;或修订审核通过复制 Pending 到正式字段
|
||
func (s *PostService) Approve(postID uint) error {
|
||
post, err := s.findPost(postID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
wasRevision := post.PendingBody != ""
|
||
|
||
// 修订审核:将待审内容覆盖到正式字段
|
||
if wasRevision {
|
||
post.Title = post.PendingTitle
|
||
post.Body = post.PendingBody
|
||
post.Excerpt = generateExcerpt(post.PendingBody)
|
||
post.PendingTitle = ""
|
||
post.PendingBody = ""
|
||
post.Status = model.PostStatusApproved
|
||
post.RejectReason = ""
|
||
if err := s.repo.Update(post); err != nil {
|
||
return err
|
||
}
|
||
s.invalidateHomeCache()
|
||
// 通知作者修订审核通过
|
||
if s.notifier != nil {
|
||
s.notifier.NotifyPostApproved(post.UserID, post.Title, post.ID)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// 首次审核通过
|
||
if post.Status != model.PostStatusPending {
|
||
return common.ErrPostCannotApprove
|
||
}
|
||
post.Status = model.PostStatusApproved
|
||
post.RejectReason = ""
|
||
// 审核晚于定时时间 → 即时发布
|
||
if post.ScheduledAt != nil && !post.ScheduledAt.After(time.Now()) {
|
||
post.ScheduledAt = nil
|
||
}
|
||
if err := s.repo.Update(post); err != nil {
|
||
return err
|
||
}
|
||
s.invalidateHomeCache()
|
||
// 通知作者审核通过
|
||
if s.notifier != nil {
|
||
s.notifier.NotifyPostApproved(post.UserID, post.Title, post.ID)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// Reject 退回:pending/approved → rejected;修订退回则清空 Pending 保持 approved
|
||
func (s *PostService) Reject(postID uint, reason string) error {
|
||
post, err := s.findPost(postID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if post.Status != model.PostStatusPending && post.Status != model.PostStatusApproved {
|
||
return common.ErrPostCannotReject
|
||
}
|
||
|
||
// 修订退回:清空待审修订,保持已发布内容不变
|
||
if post.PendingBody != "" {
|
||
post.PendingTitle = ""
|
||
post.PendingBody = ""
|
||
post.RejectReason = reason
|
||
if err := s.repo.Update(post); err != nil {
|
||
return err
|
||
}
|
||
// 通知作者修订被退回
|
||
if s.notifier != nil {
|
||
s.notifier.NotifyPostRejected(post.UserID, post.Title, reason, post.ID)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// 普通退回(首次审核)
|
||
post.Status = model.PostStatusRejected
|
||
post.RejectReason = reason
|
||
if err := s.repo.Update(post); err != nil {
|
||
return err
|
||
}
|
||
// 通知作者审核被退回
|
||
if s.notifier != nil {
|
||
s.notifier.NotifyPostRejected(post.UserID, post.Title, reason, post.ID)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// Lock 锁定:设置 IsLocked 标志,禁止编辑,不改变帖子发布状态
|
||
// 锁定状态独立于审核状态,任何状态的帖子均可锁定
|
||
func (s *PostService) Lock(postID uint, reason string) error {
|
||
post, err := s.findPost(postID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
post.IsLocked = true
|
||
post.LockReason = reason
|
||
if err := s.repo.Update(post); err != nil {
|
||
return err
|
||
}
|
||
// 通知作者稿件被锁定
|
||
if s.notifier != nil {
|
||
s.notifier.NotifyPostLocked(post.UserID, post.Title, reason, post.ID)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// Unlock 解锁:清除 IsLocked 标志,恢复可编辑
|
||
func (s *PostService) Unlock(postID uint) error {
|
||
post, err := s.findPost(postID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if !post.IsLocked {
|
||
return common.ErrPostCannotUnlock
|
||
}
|
||
post.IsLocked = false
|
||
post.LockReason = ""
|
||
if err := s.repo.Update(post); err != nil {
|
||
return err
|
||
}
|
||
// 通知作者稿件被解除锁定
|
||
if s.notifier != nil {
|
||
s.notifier.NotifyPostUnlocked(post.UserID, post.Title, post.ID)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// Restore 恢复软删除
|
||
func (s *PostService) Restore(postID uint) error {
|
||
err := s.repo.Restore(postID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
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)
|
||
})
|
||
}
|