Files
mce/internal/service/comment_service.go
Victor_Jay 43c35ddaf2 fix: 评论系统全量检查修复(7 项 Gap 全部修复)
- Delete 权限:新增 requesterID/isAdmin 参数,Service 层鉴权(仅本人/管理员可删)
- 通知内容:改用 GetUsernameByID 显示真实用户名,非数字 UID
- SearchUsers:Repository 返回 exp,Service 调用 GetLevelByExp 计算等级
- 通知高亮:Notification 新增 CommentID 字段,消息链接支持 #comment-{id}
- 已删除提示:前端 hash 检测 + 3 秒超时 Toast "该评论已不存在"
- Admin CSS:移除不存在的 comments.css 引用
- 错误哨兵:改用 common.ErrCommentNotFound/ErrCommentEmpty/PermissionDenied
2026-06-01 14:05:47 +08:00

221 lines
6.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, 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)
// 解析 @ 提及
s.parseMentions(comment.ID, body)
// 通知文章作者(评论者 ≠ 作者RelatedID 存 postID 以便消息页生成跳转链接
if s.notifier != nil && userID != postAuthorID {
commenterName, _ := s.repo.GetUsernameByID(userID)
if commenterName == "" {
commenterName = "用户"
}
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,
}
// 获取被回复者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 {
commenterName, _ := s.repo.GetUsernameByID(userID)
if commenterName == "" {
commenterName = "用户"
}
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()
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: model.GetLevelByExp(row.Exp),
}
}
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,
})
}
}
}