Files
backend/internal/repository/comment_repo.go
lan 9a1851f023
Some checks failed
Build Backend / build (push) Successful in 1m56s
Build Backend / build-docker (push) Has been cancelled
refactor: cleanup unused code and simplify internal packages
This commit performs a significant cleanup of the codebase by removing unused functions, methods, and entire files across various internal modules. This reduces technical debt and simplifies the project structure.

Key changes include:
- **cache**: Removed unused cache key generators and metrics snapshots.
- **dto**: Removed redundant converter functions and segment creation helpers.
- **middleware**: Deleted the unused `logger.go` middleware and simplified `ratelimit.go` and `casbin.go`.
- **model**: Removed unused ID helpers, database closing functions, and batch decryption logic.
- **pkg**: Cleaned up unused utility functions in `circuitbreaker`, `crypto`, `cursor`, `hook`, and `utils`.
- **service**: Deleted `account_cleanup_service.go` and removed unused helper functions in `log_cleanup_service.go` and `sensitive_service.go`.
- **repository**: Removed unused private loading methods in `comment_repo.go`.
2026-05-14 02:24:30 +08:00

664 lines
20 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 (
"with_you/internal/model"
"with_you/internal/pkg/cursor"
"context"
"gorm.io/gorm"
)
// CommentRepository 评论仓储接口
type CommentRepository interface {
Create(comment *model.Comment) error
GetByID(id string) (*model.Comment, error)
Update(comment *model.Comment) error
UpdateModerationStatus(commentID string, status model.CommentStatus, rejectReason string, reviewedBy string) error
Delete(id string) error
ApplyPublishedStats(comment *model.Comment) error
GetByPostID(postID string, page, pageSize int) ([]*model.Comment, int64, error)
GetByPostIDWithReplies(postID string, page, pageSize, replyLimit int) ([]*model.Comment, int64, error)
GetRepliesByRootID(rootID string, page, pageSize int) ([]*model.Comment, int64, error)
GetReplies(parentID string) ([]*model.Comment, error)
Like(commentID, userID string) error
Unlike(commentID, userID string) error
IsLiked(commentID, userID string) bool
IsLikedBatch(commentIDs []string, userID string) map[string]bool
GetAdminCommentList(filter AdminCommentListFilter) ([]*model.Comment, int64, error)
GetAdminCommentByID(id string) (*model.Comment, error)
BatchDelete(ids []string) ([]string, error)
GetCommentsByPostIDForAdmin(postID string, page, pageSize int) ([]*model.Comment, int64, error)
GetCommentsByCursor(ctx context.Context, postID string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Comment], error)
GetRepliesByCursor(ctx context.Context, rootID string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Comment], error)
}
// commentRepository 评论仓储实现
type commentRepository struct {
db *gorm.DB
}
// NewCommentRepository 创建评论仓储
func NewCommentRepository(db *gorm.DB) CommentRepository {
return &commentRepository{db: db}
}
// Create 创建评论
func (r *commentRepository) Create(comment *model.Comment) error {
return r.db.Create(comment).Error
}
// GetByID 根据ID获取评论
func (r *commentRepository) GetByID(id string) (*model.Comment, error) {
var comment model.Comment
err := r.db.Preload("User").First(&comment, "id = ?", id).Error
if err != nil {
return nil, err
}
return &comment, nil
}
// Update 更新评论
func (r *commentRepository) Update(comment *model.Comment) error {
return r.db.Save(comment).Error
}
// UpdateModerationStatus 更新评论审核状态
func (r *commentRepository) UpdateModerationStatus(commentID string, status model.CommentStatus, rejectReason string, reviewedBy string) error {
updates := map[string]any{
"status": status,
"reviewed_at": gorm.Expr("CURRENT_TIMESTAMP"),
"reviewed_by": reviewedBy,
"reject_reason": rejectReason,
"updated_at": gorm.Expr("updated_at"),
}
return r.db.Model(&model.Comment{}).
Where("id = ?", commentID).
UpdateColumns(updates).Error
}
// Delete 删除评论(软删除,同时清理关联数据)
func (r *commentRepository) Delete(id string) error {
return r.db.Transaction(func(tx *gorm.DB) error {
// 先查询评论获取post_id和parent_id
var comment model.Comment
if err := tx.First(&comment, "id = ?", id).Error; err != nil {
return err
}
// 删除评论点赞记录
if err := tx.Where("comment_id = ?", id).Delete(&model.CommentLike{}).Error; err != nil {
return err
}
// 删除评论(软删除)
if err := tx.Delete(&model.Comment{}, "id = ?", id).Error; err != nil {
return err
}
// 仅已发布评论才参与统计,避免 pending/rejected 影响计数
if comment.Status == model.CommentStatusPublished {
// 减少帖子的评论数并同步热度分
// 评论数属于统计字段不应影响帖子内容更新时间updated_at
if err := tx.Model(&model.Post{}).Where("id = ?", comment.PostID).
UpdateColumns(map[string]any{
"comments_count": gorm.Expr("comments_count - 1"),
"updated_at": gorm.Expr("updated_at"),
}).Error; err != nil {
return err
}
// 如果是回复,减少父评论的回复数
if comment.ParentID != nil && *comment.ParentID != "" {
if err := tx.Model(&model.Comment{}).Where("id = ?", *comment.ParentID).
UpdateColumn("replies_count", gorm.Expr("replies_count - 1")).Error; err != nil {
return err
}
}
}
return nil
})
}
// ApplyPublishedStats 在评论审核通过后更新帖子评论数/回复数
func (r *commentRepository) ApplyPublishedStats(comment *model.Comment) error {
if comment == nil {
return nil
}
return r.db.Transaction(func(tx *gorm.DB) error {
// 增加帖子的评论数并同步热度分
// 评论数属于统计字段不应影响帖子内容更新时间updated_at
if err := tx.Model(&model.Post{}).Where("id = ?", comment.PostID).
UpdateColumns(map[string]any{
"comments_count": gorm.Expr("comments_count + 1"),
"updated_at": gorm.Expr("updated_at"),
}).Error; err != nil {
return err
}
// 如果是回复,增加父评论的回复数
if comment.ParentID != nil && *comment.ParentID != "" {
if err := tx.Model(&model.Comment{}).Where("id = ?", *comment.ParentID).
UpdateColumn("replies_count", gorm.Expr("replies_count + 1")).Error; err != nil {
return err
}
}
return nil
})
}
// GetByPostID 获取帖子评论
func (r *commentRepository) GetByPostID(postID string, page, pageSize int) ([]*model.Comment, int64, error) {
var comments []*model.Comment
var total int64
r.db.Model(&model.Comment{}).Where("post_id = ? AND parent_id IS NULL AND status = ?", postID, model.CommentStatusPublished).Count(&total)
offset := (page - 1) * pageSize
err := r.db.Where("post_id = ? AND parent_id IS NULL AND status = ?", postID, model.CommentStatusPublished).
Preload("User").
Offset(offset).Limit(pageSize).
Order("created_at ASC").
Find(&comments).Error
return comments, total, err
}
// GetByPostIDWithReplies 获取帖子评论(包含回复,扁平化结构)
// 所有层级的回复都扁平化展示在顶级评论的 replies 中
func (r *commentRepository) GetByPostIDWithReplies(postID string, page, pageSize, replyLimit int) ([]*model.Comment, int64, error) {
var comments []*model.Comment
var total int64
r.db.Model(&model.Comment{}).Where("post_id = ? AND parent_id IS NULL AND status = ?", postID, model.CommentStatusPublished).Count(&total)
offset := (page - 1) * pageSize
err := r.db.Where("post_id = ? AND parent_id IS NULL AND status = ?", postID, model.CommentStatusPublished).
Preload("User").
Offset(offset).Limit(pageSize).
Order("created_at ASC").
Find(&comments).Error
if err != nil {
return nil, 0, err
}
if len(comments) == 0 {
return comments, total, nil
}
rootIDs := make([]string, 0, len(comments))
commentsByID := make(map[string]*model.Comment, len(comments))
for _, comment := range comments {
rootIDs = append(rootIDs, comment.ID)
commentsByID[comment.ID] = comment
}
// 批量加载所有回复,内存中按 root_id 分组并裁剪每个根评论的返回条数
var allReplies []*model.Comment
if err := r.db.Where("root_id IN ? AND status = ?", rootIDs, model.CommentStatusPublished).
Preload("User").
Order("created_at ASC").
Find(&allReplies).Error; err != nil {
return nil, 0, err
}
repliesByRoot := make(map[string][]*model.Comment, len(rootIDs))
for _, reply := range allReplies {
if reply.RootID == nil {
continue
}
rootID := *reply.RootID
if replyLimit <= 0 || len(repliesByRoot[rootID]) < replyLimit {
repliesByRoot[rootID] = append(repliesByRoot[rootID], reply)
}
}
type replyCountRow struct {
RootID string
Total int64
}
var replyCountRows []replyCountRow
if err := r.db.Model(&model.Comment{}).
Select("root_id, COUNT(*) AS total").
Where("root_id IN ? AND status = ?", rootIDs, model.CommentStatusPublished).
Group("root_id").
Scan(&replyCountRows).Error; err != nil {
return nil, 0, err
}
replyCountMap := make(map[string]int64, len(replyCountRows))
for _, row := range replyCountRows {
replyCountMap[row.RootID] = row.Total
}
for _, rootID := range rootIDs {
comment := commentsByID[rootID]
comment.Replies = repliesByRoot[rootID]
comment.RepliesCount = int(replyCountMap[rootID])
}
return comments, total, nil
}
// GetRepliesByRootID 根据根评论ID分页获取回复扁平化
func (r *commentRepository) GetRepliesByRootID(rootID string, page, pageSize int) ([]*model.Comment, int64, error) {
var replies []*model.Comment
var total int64
// 统计总数
r.db.Model(&model.Comment{}).Where("root_id = ? AND status = ?", rootID, model.CommentStatusPublished).Count(&total)
// 分页查询
offset := (page - 1) * pageSize
err := r.db.Where("root_id = ? AND status = ?", rootID, model.CommentStatusPublished).
Preload("User").
Order("created_at ASC").
Offset(offset).
Limit(pageSize).
Find(&replies).Error
return replies, total, err
}
// GetReplies 获取回复
func (r *commentRepository) GetReplies(parentID string) ([]*model.Comment, error) {
var comments []*model.Comment
err := r.db.Where("parent_id = ? AND status = ?", parentID, model.CommentStatusPublished).
Preload("User").
Order("created_at ASC").
Find(&comments).Error
return comments, err
}
// Like 点赞评论(使用事务保证数据一致性)
func (r *commentRepository) Like(commentID, userID string) error {
return r.db.Transaction(func(tx *gorm.DB) error {
// 检查是否已经点赞
var existing model.CommentLike
err := tx.Where("comment_id = ? AND user_id = ?", commentID, userID).First(&existing).Error
if err == nil {
// 已经点赞
return nil
}
if err != gorm.ErrRecordNotFound {
return err
}
// 创建点赞记录
like := &model.CommentLike{
CommentID: commentID,
UserID: userID,
}
if err := tx.Create(like).Error; err != nil {
return err
}
// 增加评论点赞数
return tx.Model(&model.Comment{}).Where("id = ?", commentID).
UpdateColumn("likes_count", gorm.Expr("likes_count + 1")).Error
})
}
// Unlike 取消点赞评论(使用事务保证数据一致性)
func (r *commentRepository) Unlike(commentID, userID string) error {
return r.db.Transaction(func(tx *gorm.DB) error {
result := tx.Where("comment_id = ? AND user_id = ?", commentID, userID).Delete(&model.CommentLike{})
if result.Error != nil {
return result.Error
}
if result.RowsAffected > 0 {
// 减少评论点赞数
return tx.Model(&model.Comment{}).Where("id = ?", commentID).
UpdateColumn("likes_count", gorm.Expr("likes_count - 1")).Error
}
return nil
})
}
// IsLiked 检查是否已点赞
func (r *commentRepository) IsLiked(commentID, userID string) bool {
var count int64
r.db.Model(&model.CommentLike{}).Where("comment_id = ? AND user_id = ?", commentID, userID).Count(&count)
return count > 0
}
// IsLikedBatch 批量检查是否点赞(解决 N+1 问题)
// 返回 map[commentID]bool
func (r *commentRepository) IsLikedBatch(commentIDs []string, userID string) map[string]bool {
result := make(map[string]bool)
if len(commentIDs) == 0 || userID == "" {
return result
}
// 初始化所有 commentID 为 false
for _, commentID := range commentIDs {
result[commentID] = false
}
// 一次性查询所有点赞记录
var likedCommentIDs []string
r.db.Model(&model.CommentLike{}).
Where("comment_id IN ? AND user_id = ?", commentIDs, userID).
Pluck("comment_id", &likedCommentIDs)
// 更新已点赞的评论
for _, commentID := range likedCommentIDs {
result[commentID] = true
}
return result
}
// AdminCommentListFilter 管理端评论列表筛选条件
type AdminCommentListFilter struct {
Keyword string
PostID string
AuthorID string
Status string
StartDate string
EndDate string
Page int
PageSize int
}
// GetAdminCommentList 获取管理端评论列表
func (r *commentRepository) GetAdminCommentList(filter AdminCommentListFilter) ([]*model.Comment, int64, error) {
var comments []*model.Comment
var total int64
query := r.db.Model(&model.Comment{}).Preload("User")
// 应用筛选条件
if filter.Keyword != "" {
query = query.Where("content LIKE ?", "%"+filter.Keyword+"%")
}
if filter.PostID != "" {
query = query.Where("post_id = ?", filter.PostID)
}
if filter.AuthorID != "" {
query = query.Where("user_id = ?", filter.AuthorID)
}
if filter.Status != "" {
query = query.Where("status = ?", filter.Status)
}
if filter.StartDate != "" {
query = query.Where("created_at >= ?", filter.StartDate)
}
if filter.EndDate != "" {
query = query.Where("created_at <= ?", filter.EndDate+" 23:59:59")
}
// 统计总数
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
// 分页查询
offset := (filter.Page - 1) * filter.PageSize
if err := query.Offset(offset).Limit(filter.PageSize).Order("created_at DESC").Find(&comments).Error; err != nil {
return nil, 0, err
}
return comments, total, nil
}
// GetAdminCommentByID 获取管理端评论详情(包含关联信息)
func (r *commentRepository) GetAdminCommentByID(id string) (*model.Comment, error) {
var comment model.Comment
err := r.db.Preload("User").First(&comment, "id = ?", id).Error
if err != nil {
return nil, err
}
return &comment, nil
}
// BatchDelete 批量删除评论(使用单次事务批量删除,避免 N+1 问题)
func (r *commentRepository) BatchDelete(ids []string) ([]string, error) {
if len(ids) == 0 {
return nil, nil
}
// 先查询所有评论获取 post_id 和状态信息
type commentInfo struct {
ID string
PostID string
Status model.CommentStatus
}
var comments []commentInfo
if err := r.db.Model(&model.Comment{}).Where("id IN ?", ids).Scan(&comments).Error; err != nil {
return ids, err
}
err := r.db.Transaction(func(tx *gorm.DB) error {
// 批量删除评论点赞记录
if err := tx.Where("comment_id IN ?", ids).Delete(&model.CommentLike{}).Error; err != nil {
return err
}
// 批量删除评论(软删除)
if err := tx.Where("id IN ?", ids).Delete(&model.Comment{}).Error; err != nil {
return err
}
// 更新帖子的评论数(仅统计已发布评论)
postCountMap := make(map[string]int)
for _, c := range comments {
if c.Status == model.CommentStatusPublished {
postCountMap[c.PostID]++
}
}
for postID, count := range postCountMap {
if err := tx.Model(&model.Post{}).Where("id = ?", postID).
UpdateColumns(map[string]any{
"comments_count": gorm.Expr("comments_count - ?", count),
"updated_at": gorm.Expr("updated_at"),
}).Error; err != nil {
return err
}
}
return nil
})
if err != nil {
return ids, err
}
return nil, nil
}
// GetCommentsByPostIDForAdmin 管理端获取帖子的评论列表(包含所有状态)
func (r *commentRepository) GetCommentsByPostIDForAdmin(postID string, page, pageSize int) ([]*model.Comment, int64, error) {
var comments []*model.Comment
var total int64
r.db.Model(&model.Comment{}).Where("post_id = ?", postID).Count(&total)
offset := (page - 1) * pageSize
err := r.db.Where("post_id = ?", postID).
Preload("User").
Offset(offset).Limit(pageSize).
Order("created_at DESC").
Find(&comments).Error
return comments, total, err
}
// ========== Cursor Pagination Methods ==========
// GetCommentsByCursor 游标分页获取帖子评论(包含回复)
// 排序方式created_at ASC最早发布的为一楼
func (r *commentRepository) GetCommentsByCursor(ctx context.Context, postID string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Comment], error) {
// 构建基础查询 - 只查询顶级评论
query := r.db.WithContext(ctx).Model(&model.Comment{}).
Where("post_id = ? AND parent_id IS NULL AND status = ?", postID, model.CommentStatusPublished)
// 使用游标构建器
builder := cursor.NewBuilder(query, cursor.SortByCreatedAtAsc).
WithCursor(req.Cursor, req.Direction).
WithPageSize(req.PageSize)
if builder.Error() != nil {
// 无效游标,返回空列表
return cursor.NewCursorPageResult([]*model.Comment{}, "", "", false), nil
}
// 执行查询
var comments []*model.Comment
query = builder.Build().Preload("User")
if err := query.Find(&comments).Error; err != nil {
return nil, err
}
// 构建响应
pageSize := builder.GetPageSize()
hasMore := cursor.HasMore(len(comments), pageSize)
if hasMore {
comments = comments[:pageSize]
}
// 加载每个评论的回复默认加载前3条
if len(comments) > 0 {
r.loadRepliesForComments(comments, 3)
}
// 生成游标
var nextCursor, prevCursor string
if len(comments) > 0 {
// 下一页游标
if hasMore {
lastComment := comments[len(comments)-1]
nextCursor = cursor.NewCursor(
cursor.FormatTime(lastComment.CreatedAt),
lastComment.ID,
cursor.SortByCreatedAtAsc,
).Encode()
}
// 上一页游标
firstComment := comments[0]
prevCursor = cursor.NewCursor(
cursor.FormatTime(firstComment.CreatedAt),
firstComment.ID,
cursor.SortByCreatedAtAsc,
).Encode()
}
return cursor.NewCursorPageResult(comments, nextCursor, prevCursor, hasMore), nil
}
// loadRepliesForComments 为评论列表加载回复
func (r *commentRepository) loadRepliesForComments(comments []*model.Comment, replyLimit int) {
if len(comments) == 0 {
return
}
rootIDs := make([]string, 0, len(comments))
commentsByID := make(map[string]*model.Comment, len(comments))
for _, comment := range comments {
rootIDs = append(rootIDs, comment.ID)
commentsByID[comment.ID] = comment
}
// 批量加载所有回复
var allReplies []*model.Comment
if err := r.db.Where("root_id IN ? AND status = ?", rootIDs, model.CommentStatusPublished).
Preload("User").
Order("created_at ASC").
Find(&allReplies).Error; err != nil {
return
}
// 按 root_id 分组并裁剪
repliesByRoot := make(map[string][]*model.Comment, len(rootIDs))
for _, reply := range allReplies {
if reply.RootID == nil {
continue
}
rootID := *reply.RootID
if replyLimit <= 0 || len(repliesByRoot[rootID]) < replyLimit {
repliesByRoot[rootID] = append(repliesByRoot[rootID], reply)
}
}
// 查询每个根评论的回复总数
type replyCountRow struct {
RootID string
Total int64
}
var replyCountRows []replyCountRow
if err := r.db.Model(&model.Comment{}).
Select("root_id, COUNT(*) AS total").
Where("root_id IN ? AND status = ?", rootIDs, model.CommentStatusPublished).
Group("root_id").
Scan(&replyCountRows).Error; err != nil {
return
}
replyCountMap := make(map[string]int64, len(replyCountRows))
for _, row := range replyCountRows {
replyCountMap[row.RootID] = row.Total
}
// 设置回复和回复数
for _, rootID := range rootIDs {
comment := commentsByID[rootID]
comment.Replies = repliesByRoot[rootID]
comment.RepliesCount = int(replyCountMap[rootID])
}
}
// GetRepliesByCursor 游标分页获取根评论的回复
// 排序方式created_at ASC回复按时间正序
func (r *commentRepository) GetRepliesByCursor(ctx context.Context, rootID string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Comment], error) {
// 构建基础查询
query := r.db.WithContext(ctx).Model(&model.Comment{}).
Where("root_id = ? AND status = ?", rootID, model.CommentStatusPublished)
// 使用游标构建器(回复按时间升序)
builder := cursor.NewBuilder(query, cursor.SortByCreatedAtAsc).
WithCursor(req.Cursor, req.Direction).
WithPageSize(req.PageSize)
if builder.Error() != nil {
// 无效游标,返回空列表
return cursor.NewCursorPageResult([]*model.Comment{}, "", "", false), nil
}
// 执行查询
var replies []*model.Comment
query = builder.Build().Preload("User")
if err := query.Find(&replies).Error; err != nil {
return nil, err
}
// 构建响应
pageSize := builder.GetPageSize()
hasMore := cursor.HasMore(len(replies), pageSize)
if hasMore {
replies = replies[:pageSize]
}
// 生成游标
var nextCursor, prevCursor string
if len(replies) > 0 {
// 下一页游标
if hasMore {
lastReply := replies[len(replies)-1]
nextCursor = cursor.NewCursor(
cursor.FormatTime(lastReply.CreatedAt),
lastReply.ID,
cursor.SortByCreatedAtAsc,
).Encode()
}
// 上一页游标
firstReply := replies[0]
prevCursor = cursor.NewCursor(
cursor.FormatTime(firstReply.CreatedAt),
firstReply.ID,
cursor.SortByCreatedAtAsc,
).Encode()
}
return cursor.NewCursorPageResult(replies, nextCursor, prevCursor, hasMore), nil
}