feat(compose): 正文内嵌隐藏块并升级 Markdown 编辑器
去掉整帖可见性表单,改为 :::hide 按块脱敏;工具栏补齐代码块、链接/图片对话框与粘贴拖拽上传。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/freefire/jiang13-bbs/markdown"
|
||||
"github.com/freefire/jiang13-bbs/model"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -402,7 +403,7 @@ type PostDetail struct {
|
||||
DeletedAt *time.Time `json:"deleted_at,omitempty"`
|
||||
}
|
||||
|
||||
// CreatePostInput 发帖入参
|
||||
// CreatePostInput 发帖入参(content_access / access_points 由正文 :::hide 派生)
|
||||
type CreatePostInput struct {
|
||||
UserID uint
|
||||
BoardID uint
|
||||
@@ -410,8 +411,6 @@ type CreatePostInput struct {
|
||||
Content string
|
||||
Tags string
|
||||
PostType string
|
||||
ContentAccess string
|
||||
AccessPoints int
|
||||
TypeMeta string
|
||||
Status string
|
||||
AttachmentIDs []uint
|
||||
@@ -422,8 +421,6 @@ type UpdatePostInput struct {
|
||||
Title string
|
||||
Content string
|
||||
Tags string
|
||||
ContentAccess *string
|
||||
AccessPoints *int
|
||||
TypeMeta *string
|
||||
AttachmentIDs *[]uint // nil=不改附件;非 nil=替换列表
|
||||
}
|
||||
@@ -464,12 +461,10 @@ func (s *PostService) GetByIDForViewer(id, viewerID uint, loadActor func() *Acto
|
||||
post.ViewCount++
|
||||
|
||||
detail := buildPostDetail(&post)
|
||||
locked, hint := s.evalContentAccess(&post, viewerID, loadActor)
|
||||
detail.ContentLocked = locked
|
||||
sanitized, fullyLocked, hint := s.sanitizePostContent(&post, viewerID, loadActor)
|
||||
detail.Content = sanitized
|
||||
detail.ContentLocked = fullyLocked
|
||||
detail.AccessHint = hint
|
||||
if locked {
|
||||
detail.Content = ""
|
||||
}
|
||||
|
||||
atts, _ := s.listAttachmentDTOs(post.ID, viewerID, post.UserID)
|
||||
detail.Attachments = atts
|
||||
@@ -521,56 +516,81 @@ func buildPostDetail(post *model.Post) *PostDetail {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PostService) evalContentAccess(post *model.Post, viewerID uint, loadActor func() *Actor) (locked bool, hint string) {
|
||||
access := model.NormalizeContentAccess(post.ContentAccess)
|
||||
if access == model.ContentAccessPublic {
|
||||
return false, ""
|
||||
// sanitizePostContent 按读者能力对正文 :::hide 块脱敏;fullyLocked 表示无可见正文。
|
||||
func (s *PostService) sanitizePostContent(post *model.Post, viewerID uint, loadActor func() *Actor) (content string, fullyLocked bool, hint string) {
|
||||
caps := s.buildHideViewerCaps(post, viewerID, loadActor)
|
||||
sanitized, fully := markdown.SanitizeForViewer(post.Content, caps)
|
||||
if !fully {
|
||||
return sanitized, false, ""
|
||||
}
|
||||
// 作者与版主始终可见
|
||||
access := model.NormalizeContentAccess(post.ContentAccess)
|
||||
switch access {
|
||||
case model.ContentAccessPoints:
|
||||
return sanitized, true, "支付积分后可见隐藏内容"
|
||||
case model.ContentAccessMixed:
|
||||
if post.AccessPoints > 0 {
|
||||
return sanitized, true, "满足条件后可见隐藏内容"
|
||||
}
|
||||
return sanitized, true, "满足条件后可见隐藏内容"
|
||||
case model.ContentAccessReply:
|
||||
return sanitized, true, "回复本帖后可见隐藏内容"
|
||||
case model.ContentAccessLogin:
|
||||
return sanitized, true, "登录后可见隐藏内容"
|
||||
default:
|
||||
return sanitized, true, "正文暂不可见"
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PostService) buildHideViewerCaps(post *model.Post, viewerID uint, loadActor func() *Actor) markdown.ViewerCaps {
|
||||
caps := markdown.ViewerCaps{}
|
||||
if viewerID > 0 && post.UserID == viewerID {
|
||||
return false, ""
|
||||
caps.Bypass = true
|
||||
caps.LoggedIn = true
|
||||
return caps
|
||||
}
|
||||
if loadActor != nil && loadActor().CanModerateBoard(post.BoardID) {
|
||||
return false, ""
|
||||
caps.Bypass = true
|
||||
caps.LoggedIn = true
|
||||
return caps
|
||||
}
|
||||
if viewerID == 0 {
|
||||
return caps
|
||||
}
|
||||
caps.LoggedIn = true
|
||||
|
||||
needReply := false
|
||||
needPoints := false
|
||||
blocks, err := markdown.ParseHideBlocks(post.Content)
|
||||
if err == nil {
|
||||
for _, b := range blocks {
|
||||
if b.Kind == markdown.HideKindReply {
|
||||
needReply = true
|
||||
}
|
||||
if b.Kind == markdown.HideKindPoints {
|
||||
needPoints = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
access := model.NormalizeContentAccess(post.ContentAccess)
|
||||
needReply = access == model.ContentAccessReply || access == model.ContentAccessMixed
|
||||
needPoints = post.AccessPoints > 0 || access == model.ContentAccessPoints || access == model.ContentAccessMixed
|
||||
}
|
||||
|
||||
switch access {
|
||||
case model.ContentAccessLogin:
|
||||
if viewerID == 0 {
|
||||
return true, "登录后可见全文"
|
||||
}
|
||||
return false, ""
|
||||
case model.ContentAccessReply:
|
||||
if viewerID == 0 {
|
||||
return true, "回复本帖后可见全文"
|
||||
}
|
||||
if needReply {
|
||||
var n int64
|
||||
s.db.Model(&model.Comment{}).
|
||||
Where("post_id = ? AND user_id = ? AND status = ? AND deleted_at IS NULL",
|
||||
post.ID, viewerID, model.ContentStatusPublished).
|
||||
Count(&n)
|
||||
if n == 0 {
|
||||
return true, "回复本帖后可见全文"
|
||||
}
|
||||
return false, ""
|
||||
case model.ContentAccessPoints:
|
||||
need := post.AccessPoints
|
||||
if need <= 0 {
|
||||
need = 1
|
||||
}
|
||||
if viewerID == 0 {
|
||||
return true, "支付积分后可见全文"
|
||||
}
|
||||
caps.HasReplied = n > 0
|
||||
}
|
||||
if needPoints {
|
||||
var n int64
|
||||
s.db.Model(&model.PostContentUnlock{}).
|
||||
Where("post_id = ? AND user_id = ?", post.ID, viewerID).Count(&n)
|
||||
if n > 0 {
|
||||
return false, ""
|
||||
}
|
||||
return true, "支付积分后可见全文"
|
||||
default:
|
||||
return false, ""
|
||||
caps.PointsPaid = n > 0
|
||||
}
|
||||
return caps
|
||||
}
|
||||
|
||||
func (s *PostService) listAttachmentDTOs(postID, viewerID, authorID uint) ([]PostAttachmentDTO, error) {
|
||||
@@ -605,21 +625,25 @@ func (s *PostService) listAttachmentDTOs(postID, viewerID, authorID uint) ([]Pos
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// UnlockContent 积分解锁正文
|
||||
// UnlockContent 积分解锁正文隐藏块(一次支付解锁本帖全部积分块)
|
||||
func (s *PostService) UnlockContent(userID, postID uint) (*PostDetail, error) {
|
||||
var post model.Post
|
||||
if err := s.db.Preload("Board").Preload("User").First(&post, postID).Error; err != nil {
|
||||
return nil, ErrPostNotFound
|
||||
}
|
||||
if model.NormalizeContentAccess(post.ContentAccess) != model.ContentAccessPoints {
|
||||
derived, derr := markdown.DeriveAccessFromContent(post.Content)
|
||||
if derr != nil {
|
||||
return nil, derr
|
||||
}
|
||||
need := derived.Points
|
||||
if need <= 0 {
|
||||
need = post.AccessPoints
|
||||
}
|
||||
if need <= 0 {
|
||||
return nil, errors.New("本文无需积分解锁")
|
||||
}
|
||||
if post.UserID == userID {
|
||||
return buildPostDetail(&post), nil
|
||||
}
|
||||
need := post.AccessPoints
|
||||
if need <= 0 {
|
||||
need = 1
|
||||
return s.detailAfterInteract(&post, userID)
|
||||
}
|
||||
err := s.db.Transaction(func(tx *gorm.DB) error {
|
||||
var n int64
|
||||
@@ -645,11 +669,7 @@ func (s *PostService) UnlockContent(userID, postID uint) (*PostDetail, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
detail := buildPostDetail(&post)
|
||||
atts, _ := s.listAttachmentDTOs(post.ID, userID, post.UserID)
|
||||
detail.Attachments = atts
|
||||
s.fillInteractState(detail, &post, userID, nil)
|
||||
return detail, nil
|
||||
return s.detailAfterInteract(&post, userID)
|
||||
}
|
||||
|
||||
// Create 创建帖子。status 由 handler 按角色计算
|
||||
@@ -673,18 +693,15 @@ func (s *PostService) Create(in CreatePostInput) (*PostDetail, error) {
|
||||
if !model.ValidPostType(postType) {
|
||||
return nil, errors.New("无效的帖子类型")
|
||||
}
|
||||
access := model.NormalizeContentAccess(in.ContentAccess)
|
||||
accessPts := in.AccessPoints
|
||||
if access == model.ContentAccessPoints {
|
||||
if accessPts <= 0 {
|
||||
return nil, errors.New("请设置解锁所需积分")
|
||||
}
|
||||
if accessPts > 100000 {
|
||||
return nil, errors.New("解锁积分过高")
|
||||
}
|
||||
} else {
|
||||
accessPts = 0
|
||||
if err := markdown.ValidateHideContent(content); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
derived, err := markdown.DeriveAccessFromContent(content)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
access := model.NormalizeContentAccess(derived.Access)
|
||||
accessPts := derived.Points
|
||||
typeMeta, err := NormalizeAndValidateTypeMeta(postType, in.TypeMeta)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -840,31 +857,19 @@ func (s *PostService) Update(actor *Actor, postID, userID uint, in UpdatePostInp
|
||||
if c == "" {
|
||||
return nil, errors.New("内容不能为空")
|
||||
}
|
||||
if err := markdown.ValidateHideContent(c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
derived, err := markdown.DeriveAccessFromContent(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
updates["content"] = c
|
||||
updates["content_access"] = model.NormalizeContentAccess(derived.Access)
|
||||
updates["access_points"] = derived.Points
|
||||
}
|
||||
updates["tags"] = in.Tags
|
||||
|
||||
if in.ContentAccess != nil {
|
||||
access := model.NormalizeContentAccess(*in.ContentAccess)
|
||||
updates["content_access"] = access
|
||||
if access == model.ContentAccessPoints {
|
||||
pts := post.AccessPoints
|
||||
if in.AccessPoints != nil {
|
||||
pts = *in.AccessPoints
|
||||
}
|
||||
if pts <= 0 {
|
||||
return nil, errors.New("请设置解锁所需积分")
|
||||
}
|
||||
updates["access_points"] = pts
|
||||
} else {
|
||||
updates["access_points"] = 0
|
||||
}
|
||||
} else if in.AccessPoints != nil && model.NormalizeContentAccess(post.ContentAccess) == model.ContentAccessPoints {
|
||||
if *in.AccessPoints <= 0 {
|
||||
return nil, errors.New("请设置解锁所需积分")
|
||||
}
|
||||
updates["access_points"] = *in.AccessPoints
|
||||
}
|
||||
if in.TypeMeta != nil {
|
||||
pt := model.NormalizePostType(post.PostType)
|
||||
meta, err := NormalizeAndValidateTypeMeta(pt, *in.TypeMeta)
|
||||
|
||||
Reference in New Issue
Block a user