feat: 定时发布系统 — Phase 5 完成
- Post ScheduledAt 字段 - Scheduler goroutine 每分钟扫描到期文章 - Create/Update 校验最短 30min 定时 - Approve 审核晚于定时则即时发布 - public 列表+空间页过滤未到期文章 - 写文章页 datetime-local 选择器(min=now+30min) - AutoMigrate 追加新表
This commit is contained in:
@ -69,7 +69,7 @@ func (s *PostService) findPostWithAuthor(id uint) (*model.Post, error) {
|
||||
// Create 创建帖子
|
||||
// body 为 Vditor 输出的 Markdown,后端直接存储不做 HTML 消毒
|
||||
// MD 是纯文本,无 XSS 注入风险;前端渲染时由 Vditor 转 HTML
|
||||
func (s *PostService) Create(userID uint, title, body string, visibility, postType, reprintSource, declaration string, reprintProhibited bool, categoryID string) (*model.Post, error) {
|
||||
func (s *PostService) Create(userID uint, title, body string, visibility, postType, reprintSource, declaration string, reprintProhibited bool, categoryID, scheduledAt string) (*model.Post, error) {
|
||||
status := model.PostStatusApproved
|
||||
if s.auditEnabled() {
|
||||
status = model.PostStatusDraft
|
||||
@ -93,6 +93,13 @@ func (s *PostService) Create(userID uint, title, body string, visibility, postTy
|
||||
id := uint(cid)
|
||||
post.CategoryID = &id
|
||||
}
|
||||
// 定时发布
|
||||
if t, err := parseScheduledAt(scheduledAt); err == nil {
|
||||
if time.Until(t) < 30*time.Minute {
|
||||
return nil, common.ErrScheduledTooSoon
|
||||
}
|
||||
post.ScheduledAt = &t
|
||||
}
|
||||
if err := s.repo.Create(post); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -224,3 +231,15 @@ func (s *PostService) Unpin(postID uint) error {
|
||||
post.PinnedAt = nil
|
||||
return s.repo.Update(post)
|
||||
}
|
||||
|
||||
// parseScheduledAt 解析 RFC3339 时间,非法格式返回零值+error
|
||||
func parseScheduledAt(s string) (time.Time, error) {
|
||||
if s == "" {
|
||||
return time.Time{}, nil
|
||||
}
|
||||
t, err := time.Parse(time.RFC3339, s)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user