245 lines
6.9 KiB
Go
245 lines
6.9 KiB
Go
package service
|
||
|
||
import (
|
||
"errors"
|
||
"fmt"
|
||
"regexp"
|
||
"strings"
|
||
|
||
"metazone.cc/mce/internal/common"
|
||
"metazone.cc/mce/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, common.ErrCommentEmpty
|
||
}
|
||
|
||
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)
|
||
|
||
// 解析 @ 提及 + 发送通知
|
||
commenterName, _ := s.repo.GetUsernameByID(userID)
|
||
if commenterName == "" {
|
||
commenterName = "用户"
|
||
}
|
||
s.parseMentions(comment.ID, postID, userID, commenterName, body)
|
||
|
||
// 通知文章作者(评论者 ≠ 作者),RelatedID 存 postID 以便消息页生成跳转链接
|
||
if s.notifier != nil && userID != postAuthorID {
|
||
s.notifier.Create(postAuthorID, model.NotifyComment,
|
||
"新评论",
|
||
fmt.Sprintf("%s 评论了你的文章", commenterName),
|
||
&comment.PostID, &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, common.ErrCommentEmpty
|
||
}
|
||
|
||
parent, err := s.repo.FindByID(parentID)
|
||
if err != nil {
|
||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||
return nil, common.ErrCommentNotFound
|
||
}
|
||
return nil, fmt.Errorf("查询父评论失败: %w", err)
|
||
}
|
||
|
||
comment := &model.Comment{
|
||
PostID: parent.PostID,
|
||
RootID: parent.RootID,
|
||
ParentID: &parentID,
|
||
Body: body,
|
||
UserID: userID,
|
||
ReplyToUID: parent.UserID,
|
||
}
|
||
|
||
if err := s.repo.Create(comment); err != nil {
|
||
return nil, fmt.Errorf("创建回复失败: %w", err)
|
||
}
|
||
|
||
// 文章评论数+1
|
||
_ = s.repo.IncrCommentsCount(parent.PostID)
|
||
|
||
// 解析 @ 提及 + 发送通知
|
||
commenterName, _ := s.repo.GetUsernameByID(userID)
|
||
if commenterName == "" {
|
||
commenterName = "用户"
|
||
}
|
||
s.parseMentions(comment.ID, parent.PostID, userID, commenterName, body)
|
||
|
||
// 通知被回复者(不自己回复自己),RelatedID 存 postID 以便消息页生成跳转链接
|
||
if s.notifier != nil && parent.UserID != userID {
|
||
s.notifier.Create(parent.UserID, model.NotifyCommentReply,
|
||
"新回复",
|
||
fmt.Sprintf("%s 回复了你的评论", commenterName),
|
||
&parent.PostID, &comment.ID)
|
||
}
|
||
|
||
return comment, nil
|
||
}
|
||
|
||
// Delete 软删除评论(requesterID 为请求者,isAdmin 表示是否为管理员)
|
||
func (s *CommentService) Delete(commentID uint, requesterID uint, isAdmin bool) error {
|
||
comment, err := s.repo.FindByID(commentID)
|
||
if err != nil {
|
||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||
return common.ErrCommentNotFound
|
||
}
|
||
return fmt.Errorf("查询评论失败: %w", err)
|
||
}
|
||
|
||
if comment.IsDeleted {
|
||
return nil
|
||
}
|
||
|
||
// 权限检查:仅本人或管理员可删除
|
||
if !isAdmin && comment.UserID != requesterID {
|
||
return common.ErrPermissionDenied
|
||
}
|
||
|
||
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()
|
||
comments, total, err := s.repo.FindRootComments(postID, p.Offset(), p.PageSize)
|
||
if err != nil {
|
||
return nil, 0, err
|
||
}
|
||
// 批量加载有效@提及映射,前端据此决定是否为链接
|
||
if len(comments) > 0 {
|
||
_ = s.repo.LoadMentionsForComments(comments)
|
||
}
|
||
return comments, total, nil
|
||
}
|
||
|
||
// 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{}
|
||
}
|
||
// 批量加载有效@提及映射
|
||
if len(list) > 0 {
|
||
_ = s.repo.LoadMentionsForComments(list)
|
||
}
|
||
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 搜索LV2+用户(用于@艾特悬浮窗),已关注优先,最多5条
|
||
func (s *CommentService) SearchUsers(keyword string, followerUID uint) ([]model.UserSearchResult, error) {
|
||
rows, err := s.repo.SearchUsersByLevel(keyword, 2, 5, followerUID)
|
||
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: model.GetLevelByExp(row.Exp),
|
||
Avatar: row.Avatar,
|
||
}
|
||
}
|
||
return users, nil
|
||
}
|
||
|
||
// parseMentions 解析@用户,存储到 comment_mentions 表,并向被@用户发送通知
|
||
func (s *CommentService) parseMentions(commentID uint, postID uint, commenterID uint, commenterName string, body string) {
|
||
matches := mentionPattern.FindAllStringSubmatch(body, -1)
|
||
seen := make(map[string]bool)
|
||
notified := make(map[uint]bool) // 防止重复通知同一用户
|
||
|
||
for _, m := range matches {
|
||
username := m[1]
|
||
if seen[username] {
|
||
continue
|
||
}
|
||
seen[username] = true
|
||
rows, _ := s.repo.SearchUsersByLevel(username, 2, 1, 0)
|
||
if len(rows) == 0 {
|
||
continue
|
||
}
|
||
mentionedUID := rows[0].UID
|
||
_ = s.repo.CreateMention(&model.CommentMention{
|
||
CommentID: commentID,
|
||
UID: mentionedUID,
|
||
OriginalUsername: username,
|
||
})
|
||
|
||
// 向被@用户发送通知(排除自己@自己、重复通知)
|
||
if s.notifier != nil && mentionedUID != commenterID && !notified[mentionedUID] {
|
||
notified[mentionedUID] = true
|
||
s.notifier.Create(mentionedUID, model.NotifyCommentMention,
|
||
"新提及",
|
||
fmt.Sprintf("%s 在评论中 @ 了你", commenterName),
|
||
&postID, &commentID)
|
||
}
|
||
}
|
||
}
|