Files
mce/internal/repository/comment_repo.go
Victor_Jay b7acc91f8c feat: 实现评论系统(后端 + 前端)
- 新增 Comment/CommentMention 模型,Post 新增 CommentsCount 冗余计数器
- 新增 CommentRepo(CRUD + @搜索 LV3+ + 回复统计 + 文章作者查询)
- 新增 CommentService(发表/回复/删除/@解析/通知 NotifyComment/NotifyCommentReply)
- 新增 CommentController(6 个 API 端点),commentUseCase ISP 接口
- 新增评论路由(公开读取 + 需登录写入),依赖注入,错误哨兵,数据库迁移
- 前端评论区 HTML/CSS/JS:分页加载、回复折叠展开、内联回复表单、@提及自动补全
2026-06-01 13:24:16 +08:00

251 lines
7.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
}