- 评论图片上传(LV4+,经验≥1500),JPEG/PNG 自动转 WebP,二进制搜索质量优化 - 后台评论管理页面,支持关键词搜索、分页、删除,侧边栏新增入口 - 通知跳转高亮:评论通知 RelatedID 改为 postID,消息页链接到 #comments,前端自动滚动 - 新增 adminCommentUseCase/commentStore 接口(遵循 ISP),AdminCommentRow 移至 model 层 - 前端 @mention 联想/图片上传光标插入/评论展开收起/内联回复交互完善
210 lines
5.6 KiB
Go
210 lines
5.6 KiB
Go
package service
|
||
|
||
import (
|
||
"errors"
|
||
"fmt"
|
||
"regexp"
|
||
"strings"
|
||
|
||
"metazone.cc/metalab/internal/common"
|
||
"metazone.cc/metalab/internal/model"
|
||
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
var mentionPattern = regexp.MustCompile(`@(\S{1,16})`)
|
||
|
||
// CommentService 评论业务逻辑
|
||
type CommentService struct {
|
||
repo commentStore
|
||
notifier commentNotifier
|
||
}
|
||
|
||
// NewCommentService 构造函数
|
||
func NewCommentService(repo commentStore, notifier commentNotifier) *CommentService {
|
||
return &CommentService{repo: repo, notifier: notifier}
|
||
}
|
||
|
||
// CreateRoot 创建顶级评论
|
||
func (s *CommentService) CreateRoot(userID uint, postID uint, body string) (*model.Comment, error) {
|
||
if strings.TrimSpace(body) == "" {
|
||
return nil, errors.New("评论内容不能为空")
|
||
}
|
||
|
||
postAuthorID, err := s.repo.FindPostAuthorID(postID)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("查询文章失败: %w", err)
|
||
}
|
||
|
||
comment := &model.Comment{
|
||
PostID: postID,
|
||
RootID: 0,
|
||
Body: body,
|
||
UserID: userID,
|
||
}
|
||
if err := s.repo.Create(comment); err != nil {
|
||
return nil, fmt.Errorf("创建评论失败: %w", err)
|
||
}
|
||
|
||
// RootID 指向自身(UPDATE 而非 INSERT)
|
||
comment.RootID = comment.ID
|
||
if err := s.repo.UpdateRootID(comment.ID, comment.ID); err != nil {
|
||
return nil, fmt.Errorf("更新根评论ID失败: %w", err)
|
||
}
|
||
|
||
// 文章评论数+1
|
||
_ = s.repo.IncrCommentsCount(postID)
|
||
|
||
// 解析 @ 提及
|
||
s.parseMentions(comment.ID, body)
|
||
|
||
// 通知文章作者(评论者 ≠ 作者),RelatedID 存 postID 以便消息页生成跳转链接
|
||
if s.notifier != nil && userID != postAuthorID {
|
||
commenterUID, _ := s.repo.GetUserUIDByID(userID)
|
||
s.notifier.Create(postAuthorID, model.NotifyComment,
|
||
"新评论",
|
||
fmt.Sprintf("%s 评论了你的文章", commenterUID),
|
||
&comment.PostID)
|
||
}
|
||
|
||
return comment, nil
|
||
}
|
||
|
||
// CreateReply 创建回复
|
||
func (s *CommentService) CreateReply(userID uint, parentID uint, body string) (*model.Comment, error) {
|
||
if strings.TrimSpace(body) == "" {
|
||
return nil, errors.New("回复内容不能为空")
|
||
}
|
||
|
||
parent, err := s.repo.FindByID(parentID)
|
||
if err != nil {
|
||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||
return nil, errors.New("父评论不存在")
|
||
}
|
||
return nil, fmt.Errorf("查询父评论失败: %w", err)
|
||
}
|
||
|
||
comment := &model.Comment{
|
||
PostID: parent.PostID,
|
||
RootID: parent.RootID,
|
||
ParentID: &parentID,
|
||
Body: body,
|
||
UserID: userID,
|
||
}
|
||
// 获取被回复者UID
|
||
if replyToUID, err := s.repo.GetUserUIDByID(parent.UserID); err == nil {
|
||
comment.ReplyToUID = replyToUID
|
||
}
|
||
|
||
if err := s.repo.Create(comment); err != nil {
|
||
return nil, fmt.Errorf("创建回复失败: %w", err)
|
||
}
|
||
|
||
// 文章评论数+1
|
||
_ = s.repo.IncrCommentsCount(parent.PostID)
|
||
|
||
// 解析 @ 提及
|
||
s.parseMentions(comment.ID, body)
|
||
|
||
// 通知被回复者(不自己回复自己),RelatedID 存 postID 以便消息页生成跳转链接
|
||
if s.notifier != nil && parent.UserID != userID {
|
||
commenterUID, _ := s.repo.GetUserUIDByID(userID)
|
||
s.notifier.Create(parent.UserID, model.NotifyCommentReply,
|
||
"新回复",
|
||
fmt.Sprintf("%s 回复了你的评论", commenterUID),
|
||
&parent.PostID)
|
||
}
|
||
|
||
return comment, nil
|
||
}
|
||
|
||
// Delete 软删除评论
|
||
func (s *CommentService) Delete(commentID uint) error {
|
||
comment, err := s.repo.FindByID(commentID)
|
||
if err != nil {
|
||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||
return errors.New("评论不存在")
|
||
}
|
||
return fmt.Errorf("查询评论失败: %w", err)
|
||
}
|
||
|
||
if comment.IsDeleted {
|
||
return nil
|
||
}
|
||
|
||
if err := s.repo.SoftDelete(commentID); err != nil {
|
||
return fmt.Errorf("删除评论失败: %w", err)
|
||
}
|
||
|
||
_ = s.repo.DecrCommentsCount(comment.PostID)
|
||
return nil
|
||
}
|
||
|
||
// ListRootComments 分页获取文章顶级评论
|
||
func (s *CommentService) ListRootComments(postID uint, page, pageSize int) ([]model.Comment, int64, error) {
|
||
p := common.Pagination{Page: page, PageSize: pageSize}
|
||
p.DefaultPagination()
|
||
return s.repo.FindRootComments(postID, p.Offset(), p.PageSize)
|
||
}
|
||
|
||
// ListReplies 获取某条顶级评论的全部回复
|
||
func (s *CommentService) ListReplies(rootID uint) ([]model.Comment, error) {
|
||
list, err := s.repo.FindReplies(rootID)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("查询回复失败: %w", err)
|
||
}
|
||
if list == nil {
|
||
list = []model.Comment{}
|
||
}
|
||
return list, nil
|
||
}
|
||
|
||
// ListAllComments 后台评论列表(分页+搜索)
|
||
func (s *CommentService) ListAllComments(keyword string, showDeleted bool, page, pageSize int) ([]model.AdminCommentRow, int64, error) {
|
||
p := common.Pagination{Page: page, PageSize: pageSize}
|
||
p.DefaultPagination()
|
||
rows, total, err := s.repo.ListAllComments(keyword, showDeleted, p.Offset(), p.PageSize)
|
||
if rows == nil {
|
||
rows = []model.AdminCommentRow{}
|
||
}
|
||
return rows, total, err
|
||
}
|
||
|
||
// SearchUsers 搜索LV3+用户(用于@艾特)
|
||
func (s *CommentService) SearchUsers(keyword string) ([]model.UserSearchResult, error) {
|
||
rows, err := s.repo.SearchUsersByLevel(keyword, 3, 10)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("搜索用户失败: %w", err)
|
||
}
|
||
|
||
users := make([]model.UserSearchResult, len(rows))
|
||
for i, row := range rows {
|
||
users[i] = model.UserSearchResult{
|
||
UID: row.UID,
|
||
Username: row.Username,
|
||
Level: 0,
|
||
}
|
||
}
|
||
return users, nil
|
||
}
|
||
|
||
// parseMentions 解析@用户并存储到 comment_mentions 表
|
||
func (s *CommentService) parseMentions(commentID uint, body string) {
|
||
matches := mentionPattern.FindAllStringSubmatch(body, -1)
|
||
seen := make(map[string]bool)
|
||
for _, m := range matches {
|
||
username := m[1]
|
||
if seen[username] {
|
||
continue
|
||
}
|
||
seen[username] = true
|
||
rows, _ := s.repo.SearchUsersByLevel(username, 0, 1)
|
||
if len(rows) > 0 {
|
||
_ = s.repo.CreateMention(&model.CommentMention{
|
||
CommentID: commentID,
|
||
UID: rows[0].UID,
|
||
})
|
||
}
|
||
}
|
||
}
|