This repository has been archived on 2026-06-21. You can view files and clone it, but cannot push or open issues or pull requests.
Files
MetaLab/internal/service/post_service.go
Victor_Jay 8a8d801187 fix: 修复帖子系统5项缺陷 + 代码块插入位置/language-class/分割线占位
- 作者显示 用户名(UID): 修正Post.AuthorName GORM标签从 -:all 改为 -:migration;<-:false;column:author_name
- 审核流程: 帖子详情页新增"提交审核"按钮(draft/rejected时作者可见)
- 图片上传: 新增 POST /api/posts/upload-image 端点,限制5MB/jpg/png/gif/webp
- 代码块语言标注: 工具栏插入代码块时弹出语言输入框,生成 language-xxx class
- 状态中文显示: 管理后台/帖子详情页状态改为 PostStatusDisplayNames 映射
- 修复代码块插入位置错误: init时调用switchMode同步DOM; 无选区时appendChild; 防<pre>嵌套
- 修复 bluemonday.UGCPolicy() 剥离 code/pre 的 class 属性,显式 AllowAttrs("class")
- 修复分割线工具栏插入多余text占位符: hr/link/image以外不强制填占位文本
2026-05-27 19:22:47 +08:00

222 lines
6.1 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 做安全消毒
// UGC 默认策略会剥离 code/pre 的 class 属性,需要显式放行以保留 language-xxx
var ucgPolicy = func() *bluemonday.Policy {
p := bluemonday.UGCPolicy()
p.AllowAttrs("class").OnElements("code", "pre")
return p
}()
// 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)
}