Files
mce/internal/service/comment_service.go
Victor_Jay b7acc91f8c feat: 实现评论系统(后端 + 前端)
- 新增 Comment/CommentMention 模型,Post 新增 CommentsCount 冗余计数器
- 新增 CommentRepo(CRUD + @搜索 LV3+ + 回复统计 + 文章作者查询)
- 新增 CommentService(发表/回复/删除/@解析/通知 NotifyComment/NotifyCommentReply)
- 新增 CommentController(6 个 API 端点),commentUseCase ISP 接口
- 新增评论路由(公开读取 + 需登录写入),依赖注入,错误哨兵,数据库迁移
- 前端评论区 HTML/CSS/JS:分页加载、回复折叠展开、内联回复表单、@提及自动补全
2026-06-01 13:24:16 +08:00

199 lines
5.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 (
"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)
// 通知文章作者(评论者 ≠ 作者)
if s.notifier != nil && userID != postAuthorID {
commenterUID, _ := s.repo.GetUserUIDByID(userID)
s.notifier.Create(postAuthorID, model.NotifyComment,
"新评论",
fmt.Sprintf("%s 评论了你的文章", commenterUID),
&comment.ID)
}
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)
// 通知被回复者(不自己回复自己)
if s.notifier != nil && parent.UserID != userID {
commenterUID, _ := s.repo.GetUserUIDByID(userID)
s.notifier.Create(parent.UserID, model.NotifyCommentReply,
"新回复",
fmt.Sprintf("%s 回复了你的评论", commenterUID),
&comment.ID)
}
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
}
// 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,
})
}
}
}