Files
mce/internal/service/post_service.go

218 lines
6.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package service
import (
"bytes"
"fmt"
"metazone.cc/metalab/internal/common"
"metazone.cc/metalab/internal/config"
"metazone.cc/metalab/internal/model"
"github.com/microcosm-cc/bluemonday"
"github.com/yuin/goldmark"
)
// ucgPolicy 对 Goldmark 输出的 HTML 做安全消毒
// 移除 script / iframe / javscript: / data: 等危险内容
var ucgPolicy = bluemonday.UGCPolicy()
// postStore 帖子服务所需的最小仓储接口ISP
type postStore interface {
Create(post *model.Post) error
FindByID(id uint) (*model.Post, error)
FindByIDWithAuthor(id uint) (*model.Post, error)
FindPageable(keyword string, offset, limit int) ([]model.Post, int64, error)
FindAdminPageable(keyword, status string, offset, limit int) ([]model.Post, int64, error)
Update(post *model.Post) error
SoftDelete(id uint) error
Restore(id uint) error
}
// PostService 帖子业务逻辑
type PostService struct {
repo postStore
ss *config.SiteSettings
md goldmark.Markdown
}
// NewPostService 构造函数
func NewPostService(repo postStore, ss *config.SiteSettings) *PostService {
return &PostService{
repo: repo,
ss: ss,
md: goldmark.New(),
}
}
// auditEnabled 审核是否开启(文案审核与全局审核开关一致)
func (s *PostService) auditEnabled() bool {
return s.ss.IsAuditEnabled()
}
// renderMarkdown 将 Markdown 渲染为安全 HTMLGoldmark + bluemonday 消毒)
func (s *PostService) renderMarkdown(body string) (string, error) {
var buf bytes.Buffer
if err := s.md.Convert([]byte(body), &buf); err != nil {
return "", err
}
return ucgPolicy.Sanitize(buf.String()), nil
}
// Create 创建帖子
func (s *PostService) Create(userID uint, title, body string) (*model.Post, error) {
bodyHTML, err := s.renderMarkdown(body)
if err != nil {
bodyHTML = body // 渲染失败降级使用纯文本
}
status := model.PostStatusApproved
if s.auditEnabled() {
status = model.PostStatusDraft
}
post := &model.Post{
Title: title,
Body: body,
BodyHTML: bodyHTML,
UserID: userID,
Status: status,
AllowComment: true,
}
if err := s.repo.Create(post); err != nil {
return nil, err
}
return post, nil
}
// GetByID 按 ID 获取帖子(含作者名,仅 approved 公开可见)
func (s *PostService) GetByID(id uint) (*model.Post, error) {
post, err := s.repo.FindByIDWithAuthor(id)
if err != nil {
return nil, common.ErrUserNotFound // 复用哨兵表示资源不存在
}
return post, nil
}
// List 公开帖子列表(仅 approved
func (s *PostService) List(keyword string, page, pageSize int) ([]model.Post, int64, error) {
p := common.Pagination{Page: page, PageSize: pageSize}
p.DefaultPagination()
return s.repo.FindPageable(keyword, p.Offset(), p.PageSize)
}
// ListAdmin 管理后台帖子列表(全状态)
func (s *PostService) ListAdmin(keyword, status string, page, pageSize int) ([]model.Post, int64, error) {
p := common.Pagination{Page: page, PageSize: pageSize}
p.DefaultPagination()
return s.repo.FindAdminPageable(keyword, status, p.Offset(), p.PageSize)
}
// Update 编辑帖子(权限在 controller 层检查)
func (s *PostService) Update(postID uint, title, body string) error {
post, err := s.repo.FindByID(postID)
if err != nil {
return common.ErrUserNotFound
}
// pending / locked 禁止编辑
if post.Status == model.PostStatusPending || post.Status == model.PostStatusLocked {
return fmt.Errorf("当前状态不允许编辑")
}
post.Title = title
post.Body = body
bodyHTML, err := s.renderMarkdown(body)
if err != nil {
bodyHTML = body
}
post.BodyHTML = bodyHTML
// rejected 状态编辑后自动重置为 draft
if post.Status == model.PostStatusRejected {
post.Status = model.PostStatusDraft
post.RejectReason = ""
}
return s.repo.Update(post)
}
// Delete 软删除(权限在 controller 层检查)
func (s *PostService) Delete(postID uint) error {
return s.repo.SoftDelete(postID)
}
// SubmitForAudit 提交审核draft → pending
func (s *PostService) SubmitForAudit(postID uint) error {
post, err := s.repo.FindByID(postID)
if err != nil {
return common.ErrUserNotFound
}
if post.Status != model.PostStatusDraft && post.Status != model.PostStatusRejected {
return fmt.Errorf("仅草稿和已退回帖子可提交审核")
}
post.Status = model.PostStatusPending
return s.repo.Update(post)
}
// Approve 审核通过pending → approved
func (s *PostService) Approve(postID uint) error {
post, err := s.repo.FindByID(postID)
if err != nil {
return common.ErrUserNotFound
}
if post.Status != model.PostStatusPending {
return fmt.Errorf("仅待审核帖子可通过")
}
post.Status = model.PostStatusApproved
post.RejectReason = ""
return s.repo.Update(post)
}
// Reject 退回pending/approved → rejected
func (s *PostService) Reject(postID uint, reason string) error {
post, err := s.repo.FindByID(postID)
if err != nil {
return common.ErrUserNotFound
}
if post.Status != model.PostStatusPending && post.Status != model.PostStatusApproved {
return fmt.Errorf("仅待审核和已发布帖子可退回")
}
post.Status = model.PostStatusRejected
post.RejectReason = reason
return s.repo.Update(post)
}
// Lock 锁定:任意状态 → locked
func (s *PostService) Lock(postID uint) error {
post, err := s.repo.FindByID(postID)
if err != nil {
return common.ErrUserNotFound
}
post.Status = model.PostStatusLocked
return s.repo.Update(post)
}
// Unlock 解锁locked → 草稿
func (s *PostService) Unlock(postID uint) error {
post, err := s.repo.FindByID(postID)
if err != nil {
return common.ErrUserNotFound
}
if post.Status != model.PostStatusLocked {
return fmt.Errorf("仅锁定状态可解锁")
}
post.Status = model.PostStatusDraft
return s.repo.Update(post)
}
// Restore 恢复软删除
func (s *PostService) Restore(postID uint) error {
return s.repo.Restore(postID)
}
// RenderMarkdown 公开的 Markdown 渲染(供预览 API 使用,不写 DB
func (s *PostService) RenderMarkdown(body string) (string, error) {
return s.renderMarkdown(body)
}