Files
mce/internal/repository/comment_repo.go
Victor_Jay 36c571b6bb feat: 评论系统补充功能完成——图片上传、后台管理、通知跳转高亮
- 评论图片上传(LV4+,经验≥1500),JPEG/PNG 自动转 WebP,二进制搜索质量优化
- 后台评论管理页面,支持关键词搜索、分页、删除,侧边栏新增入口
- 通知跳转高亮:评论通知 RelatedID 改为 postID,消息页链接到 #comments,前端自动滚动
- 新增 adminCommentUseCase/commentStore 接口(遵循 ISP),AdminCommentRow 移至 model 层
- 前端 @mention 联想/图片上传光标插入/评论展开收起/内联回复交互完善
2026-06-01 13:35:41 +08:00

279 lines
8.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 repository
import (
"metazone.cc/metalab/internal/model"
"strconv"
"gorm.io/gorm"
)
// CommentRepo 评论数据访问
type CommentRepo struct {
db *gorm.DB
}
// NewCommentRepo 构造函数
func NewCommentRepo(db *gorm.DB) *CommentRepo {
return &CommentRepo{db: db}
}
// Create 创建评论
func (r *CommentRepo) Create(comment *model.Comment) error {
return r.db.Create(comment).Error
}
// FindByID 按 ID 查找评论(含作者信息)
func (r *CommentRepo) FindByID(id uint) (*model.Comment, error) {
var c model.Comment
err := r.db.Table("comments").
Select("comments.*, users.username as author_name, users.avatar as author_avatar").
Joins("LEFT JOIN users ON users.uid = comments.user_id").
Where("comments.id = ?", id).
First(&c).Error
if err != nil {
return nil, err
}
return &c, nil
}
// FindRootComments 分页查询文章的顶级评论parent_id IS NULL未删除
func (r *CommentRepo) FindRootComments(postID uint, offset, limit int) ([]model.Comment, int64, error) {
var total int64
base := r.db.Table("comments").
Where("post_id = ? AND parent_id IS NULL AND is_deleted = false", postID)
if err := base.Count(&total).Error; err != nil {
return nil, 0, err
}
var list []model.Comment
err := base.
Select("comments.*, users.username as author_name, users.avatar as author_avatar").
Joins("LEFT JOIN users ON users.uid = comments.user_id").
Order("comments.created_at DESC").
Offset(offset).Limit(limit).
Find(&list).Error
if err != nil {
return nil, 0, err
}
// 为每条顶级评论填充回复数
if len(list) > 0 {
ids := make([]uint, len(list))
for i, c := range list {
ids[i] = c.ID
}
countMap := r.batchCountReplies(ids)
for i := range list {
list[i].RepliesCount = countMap[list[i].ID]
}
}
return list, total, nil
}
// FindReplies 查询某条顶级评论的所有回复(不分页,全量返回,未删除)
func (r *CommentRepo) FindReplies(rootID uint) ([]model.Comment, error) {
var list []model.Comment
err := r.db.Table("comments").
Select("comments.*, users.username as author_name, users.avatar as author_avatar").
Joins("LEFT JOIN users ON users.uid = comments.user_id").
Where("comments.root_id = ? AND comments.parent_id IS NOT NULL AND comments.is_deleted = false", rootID).
Order("comments.created_at ASC").
Find(&list).Error
if err != nil {
return nil, err
}
// 填充 reply_to_name
r.fillReplyToNames(list)
return list, nil
}
// fillReplyToNames 通过 JOIN users 获取被回复者当前显示名
func (r *CommentRepo) fillReplyToNames(list []model.Comment) {
if len(list) == 0 {
return
}
type nameRow struct {
UID string
Username string
}
var rows []nameRow
r.db.Table("users").
Select("uid, username").
Where("uid IN (SELECT reply_to_uid FROM comments WHERE root_id IN (?) AND parent_id IS NOT NULL)", r.rootIDsFromList(list)).
Find(&rows)
m := make(map[string]string)
for _, row := range rows {
m[row.UID] = row.Username
}
for i := range list {
if list[i].ReplyToUID != "" {
list[i].ReplyToName = m[list[i].ReplyToUID]
}
}
}
func (r *CommentRepo) rootIDsFromList(list []model.Comment) []uint {
ids := make([]uint, len(list))
for i, c := range list {
ids[i] = c.RootID
}
return ids
}
// batchCountReplies 批量统计回复数
func (r *CommentRepo) batchCountReplies(rootIDs []uint) map[uint]int {
type countRow struct {
RootID uint
Count int
}
var rows []countRow
r.db.Table("comments").
Select("root_id, COUNT(*) as count").
Where("root_id IN ? AND parent_id IS NOT NULL AND is_deleted = false", rootIDs).
Group("root_id").
Find(&rows)
m := make(map[uint]int)
for _, row := range rows {
m[row.RootID] = row.Count
}
return m
}
// SoftDelete 软删除评论(设置 is_deleted=true
func (r *CommentRepo) SoftDelete(id uint) error {
return r.db.Model(&model.Comment{}).Where("id = ?", id).Update("is_deleted", true).Error
}
// IncrCommentsCount 文章评论数+1
func (r *CommentRepo) IncrCommentsCount(postID uint) error {
return r.db.Model(&model.Post{}).Where("id = ?", postID).
UpdateColumn("comments_count", gorm.Expr("comments_count + 1")).Error
}
// DecrCommentsCount 文章评论数-1
func (r *CommentRepo) DecrCommentsCount(postID uint) error {
return r.db.Model(&model.Post{}).Where("id = ?", postID).
UpdateColumn("comments_count", gorm.Expr("GREATEST(comments_count - 1, 0)")).Error
}
// CountRepliesByRootByStatus 统计某条顶级评论的回复数(可选是否统计已删除)
func (r *CommentRepo) CountRepliesByRoot(rootID uint, includeDeleted bool) (int, error) {
var count int64
q := r.db.Model(&model.Comment{}).Where("root_id = ? AND parent_id IS NOT NULL", rootID)
if !includeDeleted {
q = q.Where("is_deleted = false")
}
err := q.Count(&count).Error
return int(count), err
}
// CreateMention 创建@提及记录
func (r *CommentRepo) CreateMention(mention *model.CommentMention) error {
return r.db.Create(mention).Error
}
// FindMentionsByComment 查询某条评论的@提及列表
func (r *CommentRepo) FindMentionsByComment(commentID uint) ([]model.CommentMention, error) {
var list []model.CommentMention
err := r.db.Where("comment_id = ?", commentID).Find(&list).Error
return list, err
}
// SearchUsersByLevel 搜索 LV3+ 用户(用于@艾特)
func (r *CommentRepo) SearchUsersByLevel(keyword string, minLevel int, limit int) ([]struct {
UID string
Username string
}, error) {
// LV3 = 1500 exp
type result struct {
UID string
Username string
}
var list []result
// level 是计算字段,用 exp >= thresholds[minLevel] 判断
threshold := model.LevelThresholds[minLevel]
err := r.db.Table("users").
Select("uid, username").
Where("username LIKE ? AND exp >= ? AND deleted_at IS NULL", "%"+keyword+"%", threshold).
Order("exp DESC").
Limit(limit).
Find(&list).Error
var res []struct {
UID string
Username string
}
for _, u := range list {
res = append(res, struct {
UID string
Username string
}{UID: u.UID, Username: u.Username})
}
return res, err
}
// GetUserUIDByID 通过 uint ID 获取 user 的 UID 字符串
func (r *CommentRepo) GetUserUIDByID(userID uint) (string, error) {
var uid uint
err := r.db.Table("users").Select("uid").Where("uid = ?", userID).Scan(&uid).Error
if err != nil {
return "", err
}
return strconv.FormatUint(uint64(uid), 10), nil
}
// FindPostIDByComment 查询评论所属的文章ID
func (r *CommentRepo) FindPostIDByComment(commentID uint) (uint, error) {
var postID uint
err := r.db.Table("comments").Select("post_id").Where("id = ?", commentID).Scan(&postID).Error
return postID, err
}
// UpdateRootID 更新评论的 root_id 字段(顶级评论创建后回填)
func (r *CommentRepo) UpdateRootID(id, rootID uint) error {
return r.db.Model(&model.Comment{}).Where("id = ?", id).Update("root_id", rootID).Error
}
// FindPostAuthorID 查询文章的作者 user_id
func (r *CommentRepo) FindPostAuthorID(postID uint) (uint, error) {
var authorID uint
err := r.db.Table("posts").Select("user_id").Where("id = ?", postID).Scan(&authorID).Error
return authorID, err
}
// ListAllComments 后台评论列表(支持关键词搜索、分页)
func (r *CommentRepo) ListAllComments(keyword string, showDeleted bool, offset, limit int) ([]model.AdminCommentRow, int64, error) {
var total int64
q := r.db.Table("comments").
Joins("LEFT JOIN users ON users.uid = comments.user_id").
Joins("LEFT JOIN posts ON posts.id = comments.post_id")
if !showDeleted {
q = q.Where("comments.is_deleted = false")
}
if keyword != "" {
like := "%" + keyword + "%"
q = q.Where("comments.body LIKE ? OR users.username LIKE ? OR posts.title LIKE ?", like, like, like)
}
if err := q.Count(&total).Error; err != nil {
return nil, 0, err
}
var rows []model.AdminCommentRow
err := q.
Select("comments.id, comments.post_id, posts.title as post_title, comments.body, users.username as author_name, comments.is_deleted, comments.created_at, comments.updated_at").
Order("comments.created_at DESC").
Offset(offset).Limit(limit).
Scan(&rows).Error
return rows, total, err
}