diff --git a/internal/model/notification.go b/internal/model/notification.go index 40d215a..aabbc1a 100644 --- a/internal/model/notification.go +++ b/internal/model/notification.go @@ -11,8 +11,9 @@ const ( NotifyLevelUp = "level_up" NotifyComment = "comment" NotifyCommentReply = "comment_reply" - NotifyLikeAggregated = "like_aggregated" // 每日聚合点赞通知 - NotifyFollow = "follow" // 关注通知 + NotifyLikeAggregated = "like_aggregated" // 每日聚合点赞通知 + NotifyFollow = "follow" // 关注通知 + NotifyCommentMention = "comment_mention" // 评论@提及通知 ) // Notification 消息/通知模型 @@ -39,6 +40,7 @@ var NotifyTypeNames = map[string]string{ NotifyLevelUp: "等级提升", NotifyComment: "评论", NotifyCommentReply: "回复", + NotifyCommentMention: "@ 提及", NotifyLikeAggregated: "点赞", NotifyFollow: "关注", } @@ -54,6 +56,7 @@ var NotifyCategory = map[string]string{ NotifyLevelUp: "system", NotifyComment: "mention", NotifyCommentReply: "mention", + NotifyCommentMention: "mention", NotifyLikeAggregated: "like", NotifyFollow: "follow", } diff --git a/internal/service/comment_service.go b/internal/service/comment_service.go index 8252094..b610aa1 100644 --- a/internal/service/comment_service.go +++ b/internal/service/comment_service.go @@ -55,15 +55,15 @@ func (s *CommentService) CreateRoot(userID uint, postID uint, body string) (*mod // 文章评论数+1 _ = s.repo.IncrCommentsCount(postID) - // 解析 @ 提及 - s.parseMentions(comment.ID, body) + // 解析 @ 提及 + 发送通知 + commenterName, _ := s.repo.GetUsernameByID(userID) + if commenterName == "" { + commenterName = "用户" + } + s.parseMentions(comment.ID, postID, userID, commenterName, 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), @@ -103,15 +103,15 @@ func (s *CommentService) CreateReply(userID uint, parentID uint, body string) (* // 文章评论数+1 _ = s.repo.IncrCommentsCount(parent.PostID) - // 解析 @ 提及 - s.parseMentions(comment.ID, body) + // 解析 @ 提及 + 发送通知 + 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 { - commenterName, _ := s.repo.GetUsernameByID(userID) - if commenterName == "" { - commenterName = "用户" - } s.notifier.Create(parent.UserID, model.NotifyCommentReply, "新回复", fmt.Sprintf("%s 回复了你的评论", commenterName), @@ -209,10 +209,12 @@ func (s *CommentService) SearchUsers(keyword string, followerUID uint) ([]model. return users, nil } -// parseMentions 解析@用户并存储到 comment_mentions 表 -func (s *CommentService) parseMentions(commentID uint, body string) { +// 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] { @@ -220,12 +222,23 @@ func (s *CommentService) parseMentions(commentID uint, body string) { } seen[username] = true rows, _ := s.repo.SearchUsersByLevel(username, 2, 1, 0) - if len(rows) > 0 { - _ = s.repo.CreateMention(&model.CommentMention{ - CommentID: commentID, - UID: rows[0].UID, - OriginalUsername: username, - }) + 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) } } } diff --git a/internal/service/notification_notify.go b/internal/service/notification_notify.go index 820fcf5..6c335ff 100644 --- a/internal/service/notification_notify.go +++ b/internal/service/notification_notify.go @@ -49,7 +49,7 @@ func categoryTypes(category string) []string { case "system": return []string{model.NotifyAuditApproved, model.NotifyAuditRejected, model.NotifyPostApproved, model.NotifyPostRejected, model.NotifyPostLocked, model.NotifyPostUnlocked, model.NotifyLevelUp} case "mention": - return []string{model.NotifyComment, model.NotifyCommentReply} + return []string{model.NotifyComment, model.NotifyCommentReply, model.NotifyCommentMention} case "like": return []string{model.NotifyLikeAggregated} case "follow": diff --git a/templates/MetaLab-2026/html/messages/index.html b/templates/MetaLab-2026/html/messages/index.html index 8676a80..b75a58a 100644 --- a/templates/MetaLab-2026/html/messages/index.html +++ b/templates/MetaLab-2026/html/messages/index.html @@ -97,6 +97,12 @@ {{else}} {{$link = printf "/posts/%d#comments" (derefUint $m.RelatedID)}} {{end}} + {{else if eq $m.NotifyType "comment_mention"}} + {{if $m.CommentID}} + {{$link = printf "/posts/%d#mention-%d" (derefUint $m.RelatedID) (derefUint $m.CommentID)}} + {{else}} + {{$link = printf "/posts/%d#comments" (derefUint $m.RelatedID)}} + {{end}} {{else if or (eq $m.NotifyType "audit_approved") (eq $m.NotifyType "audit_rejected")}} {{$link = "/settings"}} {{else if eq $m.NotifyType "follow"}} diff --git a/templates/MetaLab-2026/static/css/posts.css b/templates/MetaLab-2026/static/css/posts.css index 6c2efd3..f31d486 100644 --- a/templates/MetaLab-2026/static/css/posts.css +++ b/templates/MetaLab-2026/static/css/posts.css @@ -1096,6 +1096,12 @@ border-bottom: none; } +/* @提及通知进入:持久半透明高亮 */ +.comment-card.highlight-mention, +.comment-reply-card.highlight-mention { + background: rgba(255, 248, 225, 0.5) !important; +} + .comment-header { display: flex; justify-content: space-between; diff --git a/templates/MetaLab-2026/static/js/post-comments.js b/templates/MetaLab-2026/static/js/post-comments.js index 96f31ad..733f2ed 100644 --- a/templates/MetaLab-2026/static/js/post-comments.js +++ b/templates/MetaLab-2026/static/js/post-comments.js @@ -498,21 +498,45 @@ } // ====== 评论高亮 ====== - function highlightComment(commentId) { + function highlightComment(commentId, persistent) { var card = commentsList.querySelector('.comment-card[data-id="' + commentId + '"]') || commentsList.querySelector('.comment-reply-card[data-id="' + commentId + '"]'); if (card) { commentsList.insertBefore(card, commentsList.firstChild); card.scrollIntoView({ behavior: 'smooth', block: 'start' }); - card.style.background = '#fff8e1'; - card.style.transition = 'background 1.5s'; - setTimeout(function() { card.style.background = ''; }, 2000); + if (persistent) { + card.classList.add('highlight-mention'); + } else { + card.style.background = '#fff8e1'; + card.style.transition = 'background 1.5s'; + setTimeout(function() { card.style.background = ''; }, 2000); + } return true; } return false; } var hash = window.location.hash; + + // @提及通知进入:置顶 + 持久高亮 + var mentionMatch = hash.match(/^#mention-(\d+)$/); + if (mentionMatch) { + var mentionCommentId = mentionMatch[1]; + var mentionInterval = setInterval(function() { + if (highlightComment(mentionCommentId, true)) { + clearInterval(mentionInterval); + } + }, 300); + setTimeout(function() { + clearInterval(mentionInterval); + if (!commentsList.querySelector('.comment-card[data-id="' + mentionCommentId + '"]') && + !commentsList.querySelector('.comment-reply-card[data-id="' + mentionCommentId + '"]')) { + showToast('该评论已不存在'); + } + }, 3000); + } + + // 普通评论/回复链接:短暂高亮 var commentMatch = hash.match(/^#comment-(\d+)$/); if (commentMatch) { var targetCommentId = commentMatch[1];