feat: @提及支持改名后链接跟随UID + 显示名自动更新

- comment_mentions新增original_username列,创建时记录原始@用户名
- Mentions map改为{original_username: {uid, name}}结构,name为当前显示名
- 用户改名后:旧@文本仍能匹配original_username,链接跟随uid不变,显示名自动更新
- 前端renderMentions适配新结构
This commit is contained in:
2026-06-03 01:00:44 +08:00
parent be8ce04334
commit ae15025b1d
4 changed files with 37 additions and 25 deletions

View File

@ -25,16 +25,23 @@ type Comment struct {
ReplyToName string `gorm:"-:migration;<-:false" json:"reply_to_name,omitempty"` ReplyToName string `gorm:"-:migration;<-:false" json:"reply_to_name,omitempty"`
// RepliesCount 回复数非DB字段查询填充 // RepliesCount 回复数非DB字段查询填充
RepliesCount int `gorm:"-:migration;<-:false" json:"replies_count,omitempty"` RepliesCount int `gorm:"-:migration;<-:false" json:"replies_count,omitempty"`
// Mentions 有效@提及映射 username -> uid非DB字段批量查询填充 // Mentions 有效@提及映射 original_username -> {uid, 当前显示名}非DB字段批量查询填充
// 前端据此判断 @username 是否为有效提及并生成链接 // key=创建时的原始@用户名value={uid,当前显示名},用户改名后链接仍有效且显示新名
Mentions map[string]uint `gorm:"-" json:"mentions,omitempty"` Mentions map[string]MentionInfo `gorm:"-" json:"mentions,omitempty"`
} }
// CommentMention @提及映射绑定UID不是username,支持改名后自动更新显示名) // CommentMention @提及映射绑定UID + 原始用户名,支持改名后自动更新显示名)
type CommentMention struct { type CommentMention struct {
ID uint `gorm:"primarykey" json:"id"` ID uint `gorm:"primarykey" json:"id"`
CommentID uint `gorm:"uniqueIndex:idx_comment_uid,priority:1;not null" json:"comment_id"` CommentID uint `gorm:"uniqueIndex:idx_comment_uid,priority:1;not null" json:"comment_id"`
UID uint `gorm:"uniqueIndex:idx_comment_uid,priority:2;not null" json:"uid"` UID uint `gorm:"uniqueIndex:idx_comment_uid,priority:2;not null" json:"uid"`
OriginalUsername string `gorm:"type:varchar(64);not null;default:''" json:"original_username"`
}
// MentionInfo @提及显示信息(前端渲染用)
type MentionInfo struct {
UID uint `json:"uid"`
Name string `json:"name"` // 当前显示名
} }
// UserSearchResult @搜索用户结果 // UserSearchResult @搜索用户结果

View File

@ -17,8 +17,9 @@ func (r *CommentRepo) FindMentionsByComment(commentID uint) ([]model.CommentMent
return list, err return list, err
} }
// LoadMentionsForComments 批量加载多条评论的有效@提及映射username -> uid // LoadMentionsForComments 批量加载多条评论的有效@提及映射
// 传入的 comments 会被原地修改,填充 Mentions 字段 // key=创建时的原始@用户名, value={uid, 用户当前显示名}
// 用户改名后原始名匹配body中的旧@文本显示名随users表更新
func (r *CommentRepo) LoadMentionsForComments(comments []model.Comment) error { func (r *CommentRepo) LoadMentionsForComments(comments []model.Comment) error {
if len(comments) == 0 { if len(comments) == 0 {
return nil return nil
@ -29,15 +30,15 @@ func (r *CommentRepo) LoadMentionsForComments(comments []model.Comment) error {
commentIDs[i] = c.ID commentIDs[i] = c.ID
} }
// 查询 comment_mentions JOIN users拿到 username 和 uid
type mentionRow struct { type mentionRow struct {
CommentID uint CommentID uint
UID uint UID uint
Username string OriginalUsername string
CurrentName string
} }
var rows []mentionRow var rows []mentionRow
err := r.db.Table("comment_mentions"). err := r.db.Table("comment_mentions").
Select("comment_mentions.comment_id, comment_mentions.uid, users.username"). Select("comment_mentions.comment_id, comment_mentions.uid, comment_mentions.original_username, users.username AS current_name").
Joins("LEFT JOIN users ON users.id = comment_mentions.uid"). Joins("LEFT JOIN users ON users.id = comment_mentions.uid").
Where("comment_mentions.comment_id IN ?", commentIDs). Where("comment_mentions.comment_id IN ?", commentIDs).
Find(&rows).Error Find(&rows).Error
@ -45,13 +46,16 @@ func (r *CommentRepo) LoadMentionsForComments(comments []model.Comment) error {
return err return err
} }
// 按 comment_id 分组 // 按 comment_id 分组key = original_username, value = MentionInfo
mentionMap := make(map[uint]map[string]uint) mentionMap := make(map[uint]map[string]model.MentionInfo)
for _, row := range rows { for _, row := range rows {
if mentionMap[row.CommentID] == nil { if mentionMap[row.CommentID] == nil {
mentionMap[row.CommentID] = make(map[string]uint) mentionMap[row.CommentID] = make(map[string]model.MentionInfo)
}
mentionMap[row.CommentID][row.OriginalUsername] = model.MentionInfo{
UID: row.UID,
Name: row.CurrentName,
} }
mentionMap[row.CommentID][row.Username] = row.UID
} }
for i := range comments { for i := range comments {

View File

@ -224,6 +224,7 @@ func (s *CommentService) parseMentions(commentID uint, body string) {
_ = s.repo.CreateMention(&model.CommentMention{ _ = s.repo.CreateMention(&model.CommentMention{
CommentID: commentID, CommentID: commentID,
UID: rows[0].UID, UID: rows[0].UID,
OriginalUsername: username,
}) })
} }
} }

View File

@ -14,17 +14,17 @@
var currentPage = 1; var currentPage = 1;
var totalPages = 1; var totalPages = 1;
// 渲染@提及:有效提及渲染为蓝色链接,无效提及保持纯文本 // 渲染@提及:有效提及渲染为蓝色链接(显示当前用户名),无效提及保持纯文本
// mentions: { original_username: {uid, name} } — name为当前显示名用户改名后自动更新
function renderMentions(body, mentions) { function renderMentions(body, mentions) {
body = escapeHtml(body); body = escapeHtml(body);
if (!mentions || Object.keys(mentions).length === 0) { if (!mentions || Object.keys(mentions).length === 0) {
// 无 mentions 数据时仍高亮@,但不加链接(降级处理)
return body.replace(/@(\S{1,16})/g, '<span class="reply-to">@$1</span>'); return body.replace(/@(\S{1,16})/g, '<span class="reply-to">@$1</span>');
} }
return body.replace(/@(\S{1,16})/g, function(match, username) { return body.replace(/@(\S{1,16})/g, function(match, username) {
var uid = mentions[username]; var info = mentions[username];
if (uid) { if (info && info.uid) {
return '<a class="mention-link" href="/space/' + uid + '">@' + escapeHtml(username) + '</a>'; return '<a class="mention-link" href="/space/' + info.uid + '">@' + escapeHtml(info.name) + '</a>';
} }
return match; return match;
}); });