2026-03-09 21:28:58 +08:00
|
|
|
|
package repository
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"carrot_bbs/internal/model"
|
feat(api): add cursor-based pagination for posts, comments, messages, groups, and notifications
Implement cursor-based pagination across multiple API endpoints to improve performance and enable efficient infinite scrolling. This replaces traditional offset-based pagination for high-volume data retrieval.
Changes:
- Add cursor pagination DTOs for all entity types (Post, Comment, Message, Conversation, Group, Notification, GroupMember, GroupAnnouncement)
- Implement cursor pagination methods in repositories with support for various sort orders (created_at, updated_at, join_time, seq)
- Add cursor pagination handlers that maintain backward compatibility with existing offset pagination
- Add new API routes for cursor-based endpoints (/cursor suffix)
- Add helper converter functions for pointer slice types in DTO conversions
2026-03-20 23:03:23 +08:00
|
|
|
|
"carrot_bbs/internal/pkg/cursor"
|
|
|
|
|
|
"context"
|
2026-03-09 21:28:58 +08:00
|
|
|
|
|
|
|
|
|
|
"gorm.io/gorm"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-03-26 18:14:16 +08:00
|
|
|
|
// CommentRepository 评论仓储接口
|
|
|
|
|
|
type CommentRepository interface {
|
|
|
|
|
|
Create(comment *model.Comment) error
|
|
|
|
|
|
GetByID(id string) (*model.Comment, error)
|
|
|
|
|
|
Update(comment *model.Comment) error
|
2026-04-11 04:23:24 +08:00
|
|
|
|
UpdateModerationStatus(commentID string, status model.CommentStatus, rejectReason string, reviewedBy string) error
|
2026-03-26 18:14:16 +08:00
|
|
|
|
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 {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
db *gorm.DB
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// NewCommentRepository 创建评论仓储
|
2026-03-26 18:14:16 +08:00
|
|
|
|
func NewCommentRepository(db *gorm.DB) CommentRepository {
|
|
|
|
|
|
return &commentRepository{db: db}
|
2026-03-09 21:28:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Create 创建评论
|
2026-03-26 18:14:16 +08:00
|
|
|
|
func (r *commentRepository) Create(comment *model.Comment) error {
|
2026-03-10 12:58:23 +08:00
|
|
|
|
return r.db.Create(comment).Error
|
2026-03-09 21:28:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetByID 根据ID获取评论
|
2026-03-26 18:14:16 +08:00
|
|
|
|
func (r *commentRepository) GetByID(id string) (*model.Comment, error) {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
var comment model.Comment
|
|
|
|
|
|
err := r.db.Preload("User").First(&comment, "id = ?", id).Error
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
return &comment, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Update 更新评论
|
2026-03-26 18:14:16 +08:00
|
|
|
|
func (r *commentRepository) Update(comment *model.Comment) error {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
return r.db.Save(comment).Error
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// UpdateModerationStatus 更新评论审核状态
|
2026-04-11 04:23:24 +08:00
|
|
|
|
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"),
|
|
|
|
|
|
}
|
2026-03-09 21:28:58 +08:00
|
|
|
|
return r.db.Model(&model.Comment{}).
|
|
|
|
|
|
Where("id = ?", commentID).
|
2026-04-11 04:23:24 +08:00
|
|
|
|
UpdateColumns(updates).Error
|
2026-03-09 21:28:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Delete 删除评论(软删除,同时清理关联数据)
|
2026-03-26 18:14:16 +08:00
|
|
|
|
func (r *commentRepository) Delete(id string) error {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 12:58:23 +08:00
|
|
|
|
// 仅已发布评论才参与统计,避免 pending/rejected 影响计数
|
|
|
|
|
|
if comment.Status == model.CommentStatusPublished {
|
|
|
|
|
|
// 减少帖子的评论数并同步热度分
|
2026-03-23 01:06:41 +08:00
|
|
|
|
// 评论数属于统计字段,不应影响帖子内容更新时间(updated_at)
|
2026-03-10 12:58:23 +08:00
|
|
|
|
if err := tx.Model(&model.Post{}).Where("id = ?", comment.PostID).
|
2026-03-30 04:49:35 +08:00
|
|
|
|
UpdateColumns(map[string]any{
|
2026-03-10 12:58:23 +08:00
|
|
|
|
"comments_count": gorm.Expr("comments_count - 1"),
|
2026-03-23 14:08:36 +08:00
|
|
|
|
"updated_at": gorm.Expr("updated_at"),
|
2026-03-10 12:58:23 +08:00
|
|
|
|
}).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 在评论审核通过后更新帖子评论数/回复数
|
2026-03-26 18:14:16 +08:00
|
|
|
|
func (r *commentRepository) ApplyPublishedStats(comment *model.Comment) error {
|
2026-03-10 12:58:23 +08:00
|
|
|
|
if comment == nil {
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
return r.db.Transaction(func(tx *gorm.DB) error {
|
|
|
|
|
|
// 增加帖子的评论数并同步热度分
|
2026-03-23 01:06:41 +08:00
|
|
|
|
// 评论数属于统计字段,不应影响帖子内容更新时间(updated_at)
|
2026-03-09 21:28:58 +08:00
|
|
|
|
if err := tx.Model(&model.Post{}).Where("id = ?", comment.PostID).
|
2026-03-23 01:06:41 +08:00
|
|
|
|
UpdateColumns(map[string]any{
|
2026-03-10 12:58:23 +08:00
|
|
|
|
"comments_count": gorm.Expr("comments_count + 1"),
|
2026-03-23 14:08:36 +08:00
|
|
|
|
"updated_at": gorm.Expr("updated_at"),
|
2026-03-09 21:28:58 +08:00
|
|
|
|
}).Error; err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 12:58:23 +08:00
|
|
|
|
// 如果是回复,增加父评论的回复数
|
2026-03-09 21:28:58 +08:00
|
|
|
|
if comment.ParentID != nil && *comment.ParentID != "" {
|
|
|
|
|
|
if err := tx.Model(&model.Comment{}).Where("id = ?", *comment.ParentID).
|
2026-03-10 12:58:23 +08:00
|
|
|
|
UpdateColumn("replies_count", gorm.Expr("replies_count + 1")).Error; err != nil {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetByPostID 获取帖子评论
|
2026-03-26 18:14:16 +08:00
|
|
|
|
func (r *commentRepository) GetByPostID(postID string, page, pageSize int) ([]*model.Comment, int64, error) {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
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 中
|
2026-03-26 18:14:16 +08:00
|
|
|
|
func (r *commentRepository) GetByPostIDWithReplies(postID string, page, pageSize, replyLimit int) ([]*model.Comment, int64, error) {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// loadFlatReplies 加载评论的所有回复(扁平化,所有层级都在同一层)
|
2026-03-26 18:14:16 +08:00
|
|
|
|
func (r *commentRepository) loadFlatReplies(rootComment *model.Comment, limit int) {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
var allReplies []*model.Comment
|
|
|
|
|
|
|
|
|
|
|
|
// 查询所有以该评论为根评论的回复(不包括顶级评论本身)
|
|
|
|
|
|
r.db.Where("root_id = ? AND status = ?", rootComment.ID, model.CommentStatusPublished).
|
|
|
|
|
|
Preload("User").
|
|
|
|
|
|
Order("created_at ASC").
|
|
|
|
|
|
Limit(limit).
|
|
|
|
|
|
Find(&allReplies)
|
|
|
|
|
|
|
|
|
|
|
|
rootComment.Replies = allReplies
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetRepliesByRootID 根据根评论ID分页获取回复(扁平化)
|
2026-03-26 18:14:16 +08:00
|
|
|
|
func (r *commentRepository) GetRepliesByRootID(rootID string, page, pageSize int) ([]*model.Comment, int64, error) {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
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 获取回复
|
2026-03-26 18:14:16 +08:00
|
|
|
|
func (r *commentRepository) GetReplies(parentID string) ([]*model.Comment, error) {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-26 01:50:54 +08:00
|
|
|
|
// Like 点赞评论(使用事务保证数据一致性)
|
2026-03-26 18:14:16 +08:00
|
|
|
|
func (r *commentRepository) Like(commentID, userID string) error {
|
2026-03-26 01:50:54 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
2026-03-09 21:28:58 +08:00
|
|
|
|
|
2026-03-26 01:50:54 +08:00
|
|
|
|
// 创建点赞记录
|
|
|
|
|
|
like := &model.CommentLike{
|
|
|
|
|
|
CommentID: commentID,
|
|
|
|
|
|
UserID: userID,
|
|
|
|
|
|
}
|
|
|
|
|
|
if err := tx.Create(like).Error; err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
2026-03-09 21:28:58 +08:00
|
|
|
|
|
2026-03-26 01:50:54 +08:00
|
|
|
|
// 增加评论点赞数
|
|
|
|
|
|
return tx.Model(&model.Comment{}).Where("id = ?", commentID).
|
|
|
|
|
|
UpdateColumn("likes_count", gorm.Expr("likes_count + 1")).Error
|
|
|
|
|
|
})
|
2026-03-09 21:28:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-26 01:50:54 +08:00
|
|
|
|
// Unlike 取消点赞评论(使用事务保证数据一致性)
|
2026-03-26 18:14:16 +08:00
|
|
|
|
func (r *commentRepository) Unlike(commentID, userID string) error {
|
2026-03-26 01:50:54 +08:00
|
|
|
|
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
|
|
|
|
|
|
})
|
2026-03-09 21:28:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// IsLiked 检查是否已点赞
|
2026-03-26 18:14:16 +08:00
|
|
|
|
func (r *commentRepository) IsLiked(commentID, userID string) bool {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
var count int64
|
|
|
|
|
|
r.db.Model(&model.CommentLike{}).Where("comment_id = ? AND user_id = ?", commentID, userID).Count(&count)
|
|
|
|
|
|
return count > 0
|
|
|
|
|
|
}
|
2026-03-14 18:01:55 +08:00
|
|
|
|
|
2026-03-26 01:50:54 +08:00
|
|
|
|
// IsLikedBatch 批量检查是否点赞(解决 N+1 问题)
|
|
|
|
|
|
// 返回 map[commentID]bool
|
2026-03-26 18:14:16 +08:00
|
|
|
|
func (r *commentRepository) IsLikedBatch(commentIDs []string, userID string) map[string]bool {
|
2026-03-26 01:50:54 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-14 18:01:55 +08:00
|
|
|
|
// AdminCommentListFilter 管理端评论列表筛选条件
|
|
|
|
|
|
type AdminCommentListFilter struct {
|
|
|
|
|
|
Keyword string
|
|
|
|
|
|
PostID string
|
|
|
|
|
|
AuthorID string
|
|
|
|
|
|
Status string
|
|
|
|
|
|
StartDate string
|
|
|
|
|
|
EndDate string
|
|
|
|
|
|
Page int
|
|
|
|
|
|
PageSize int
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetAdminCommentList 获取管理端评论列表
|
2026-03-26 18:14:16 +08:00
|
|
|
|
func (r *commentRepository) GetAdminCommentList(filter AdminCommentListFilter) ([]*model.Comment, int64, error) {
|
2026-03-14 18:01:55 +08:00
|
|
|
|
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 获取管理端评论详情(包含关联信息)
|
2026-03-26 18:14:16 +08:00
|
|
|
|
func (r *commentRepository) GetAdminCommentByID(id string) (*model.Comment, error) {
|
2026-03-14 18:01:55 +08:00
|
|
|
|
var comment model.Comment
|
|
|
|
|
|
err := r.db.Preload("User").First(&comment, "id = ?", id).Error
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
return &comment, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-26 01:50:54 +08:00
|
|
|
|
// BatchDelete 批量删除评论(使用单次事务批量删除,避免 N+1 问题)
|
2026-03-26 18:14:16 +08:00
|
|
|
|
func (r *commentRepository) BatchDelete(ids []string) ([]string, error) {
|
2026-03-26 01:50:54 +08:00
|
|
|
|
if len(ids) == 0 {
|
|
|
|
|
|
return nil, nil
|
|
|
|
|
|
}
|
2026-03-14 18:01:55 +08:00
|
|
|
|
|
2026-03-26 01:50:54 +08:00
|
|
|
|
// 先查询所有评论获取 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
|
2026-03-14 18:01:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-26 01:50:54 +08:00
|
|
|
|
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
|
2026-03-14 18:01:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetCommentsByPostIDForAdmin 管理端获取帖子的评论列表(包含所有状态)
|
2026-03-26 18:14:16 +08:00
|
|
|
|
func (r *commentRepository) GetCommentsByPostIDForAdmin(postID string, page, pageSize int) ([]*model.Comment, int64, error) {
|
2026-03-14 18:01:55 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
feat(api): add cursor-based pagination for posts, comments, messages, groups, and notifications
Implement cursor-based pagination across multiple API endpoints to improve performance and enable efficient infinite scrolling. This replaces traditional offset-based pagination for high-volume data retrieval.
Changes:
- Add cursor pagination DTOs for all entity types (Post, Comment, Message, Conversation, Group, Notification, GroupMember, GroupAnnouncement)
- Implement cursor pagination methods in repositories with support for various sort orders (created_at, updated_at, join_time, seq)
- Add cursor pagination handlers that maintain backward compatibility with existing offset pagination
- Add new API routes for cursor-based endpoints (/cursor suffix)
- Add helper converter functions for pointer slice types in DTO conversions
2026-03-20 23:03:23 +08:00
|
|
|
|
|
|
|
|
|
|
// ========== Cursor Pagination Methods ==========
|
|
|
|
|
|
|
|
|
|
|
|
// GetCommentsByCursor 游标分页获取帖子评论(包含回复)
|
2026-04-10 01:13:58 +08:00
|
|
|
|
// 排序方式:created_at ASC(最早发布的为一楼)
|
2026-03-26 18:14:16 +08:00
|
|
|
|
func (r *commentRepository) GetCommentsByCursor(ctx context.Context, postID string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Comment], error) {
|
feat(api): add cursor-based pagination for posts, comments, messages, groups, and notifications
Implement cursor-based pagination across multiple API endpoints to improve performance and enable efficient infinite scrolling. This replaces traditional offset-based pagination for high-volume data retrieval.
Changes:
- Add cursor pagination DTOs for all entity types (Post, Comment, Message, Conversation, Group, Notification, GroupMember, GroupAnnouncement)
- Implement cursor pagination methods in repositories with support for various sort orders (created_at, updated_at, join_time, seq)
- Add cursor pagination handlers that maintain backward compatibility with existing offset pagination
- Add new API routes for cursor-based endpoints (/cursor suffix)
- Add helper converter functions for pointer slice types in DTO conversions
2026-03-20 23:03:23 +08:00
|
|
|
|
// 构建基础查询 - 只查询顶级评论
|
|
|
|
|
|
query := r.db.WithContext(ctx).Model(&model.Comment{}).
|
|
|
|
|
|
Where("post_id = ? AND parent_id IS NULL AND status = ?", postID, model.CommentStatusPublished)
|
|
|
|
|
|
|
|
|
|
|
|
// 使用游标构建器
|
2026-04-10 01:13:58 +08:00
|
|
|
|
builder := cursor.NewBuilder(query, cursor.SortByCreatedAtAsc).
|
feat(api): add cursor-based pagination for posts, comments, messages, groups, and notifications
Implement cursor-based pagination across multiple API endpoints to improve performance and enable efficient infinite scrolling. This replaces traditional offset-based pagination for high-volume data retrieval.
Changes:
- Add cursor pagination DTOs for all entity types (Post, Comment, Message, Conversation, Group, Notification, GroupMember, GroupAnnouncement)
- Implement cursor pagination methods in repositories with support for various sort orders (created_at, updated_at, join_time, seq)
- Add cursor pagination handlers that maintain backward compatibility with existing offset pagination
- Add new API routes for cursor-based endpoints (/cursor suffix)
- Add helper converter functions for pointer slice types in DTO conversions
2026-03-20 23:03:23 +08:00
|
|
|
|
WithCursor(req.Cursor, req.Direction).
|
|
|
|
|
|
WithPageSize(req.PageSize)
|
|
|
|
|
|
|
|
|
|
|
|
if builder.Error() != nil {
|
|
|
|
|
|
// 无效游标,返回空列表
|
2026-04-07 00:07:40 +08:00
|
|
|
|
return cursor.NewCursorPageResult([]*model.Comment{}, "", "", false), nil
|
feat(api): add cursor-based pagination for posts, comments, messages, groups, and notifications
Implement cursor-based pagination across multiple API endpoints to improve performance and enable efficient infinite scrolling. This replaces traditional offset-based pagination for high-volume data retrieval.
Changes:
- Add cursor pagination DTOs for all entity types (Post, Comment, Message, Conversation, Group, Notification, GroupMember, GroupAnnouncement)
- Implement cursor pagination methods in repositories with support for various sort orders (created_at, updated_at, join_time, seq)
- Add cursor pagination handlers that maintain backward compatibility with existing offset pagination
- Add new API routes for cursor-based endpoints (/cursor suffix)
- Add helper converter functions for pointer slice types in DTO conversions
2026-03-20 23:03:23 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 执行查询
|
|
|
|
|
|
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,
|
2026-04-10 01:13:58 +08:00
|
|
|
|
cursor.SortByCreatedAtAsc,
|
feat(api): add cursor-based pagination for posts, comments, messages, groups, and notifications
Implement cursor-based pagination across multiple API endpoints to improve performance and enable efficient infinite scrolling. This replaces traditional offset-based pagination for high-volume data retrieval.
Changes:
- Add cursor pagination DTOs for all entity types (Post, Comment, Message, Conversation, Group, Notification, GroupMember, GroupAnnouncement)
- Implement cursor pagination methods in repositories with support for various sort orders (created_at, updated_at, join_time, seq)
- Add cursor pagination handlers that maintain backward compatibility with existing offset pagination
- Add new API routes for cursor-based endpoints (/cursor suffix)
- Add helper converter functions for pointer slice types in DTO conversions
2026-03-20 23:03:23 +08:00
|
|
|
|
).Encode()
|
|
|
|
|
|
}
|
|
|
|
|
|
// 上一页游标
|
|
|
|
|
|
firstComment := comments[0]
|
|
|
|
|
|
prevCursor = cursor.NewCursor(
|
|
|
|
|
|
cursor.FormatTime(firstComment.CreatedAt),
|
|
|
|
|
|
firstComment.ID,
|
2026-04-10 01:13:58 +08:00
|
|
|
|
cursor.SortByCreatedAtAsc,
|
feat(api): add cursor-based pagination for posts, comments, messages, groups, and notifications
Implement cursor-based pagination across multiple API endpoints to improve performance and enable efficient infinite scrolling. This replaces traditional offset-based pagination for high-volume data retrieval.
Changes:
- Add cursor pagination DTOs for all entity types (Post, Comment, Message, Conversation, Group, Notification, GroupMember, GroupAnnouncement)
- Implement cursor pagination methods in repositories with support for various sort orders (created_at, updated_at, join_time, seq)
- Add cursor pagination handlers that maintain backward compatibility with existing offset pagination
- Add new API routes for cursor-based endpoints (/cursor suffix)
- Add helper converter functions for pointer slice types in DTO conversions
2026-03-20 23:03:23 +08:00
|
|
|
|
).Encode()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return cursor.NewCursorPageResult(comments, nextCursor, prevCursor, hasMore), nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// loadRepliesForComments 为评论列表加载回复
|
2026-03-26 18:14:16 +08:00
|
|
|
|
func (r *commentRepository) loadRepliesForComments(comments []*model.Comment, replyLimit int) {
|
feat(api): add cursor-based pagination for posts, comments, messages, groups, and notifications
Implement cursor-based pagination across multiple API endpoints to improve performance and enable efficient infinite scrolling. This replaces traditional offset-based pagination for high-volume data retrieval.
Changes:
- Add cursor pagination DTOs for all entity types (Post, Comment, Message, Conversation, Group, Notification, GroupMember, GroupAnnouncement)
- Implement cursor pagination methods in repositories with support for various sort orders (created_at, updated_at, join_time, seq)
- Add cursor pagination handlers that maintain backward compatibility with existing offset pagination
- Add new API routes for cursor-based endpoints (/cursor suffix)
- Add helper converter functions for pointer slice types in DTO conversions
2026-03-20 23:03:23 +08:00
|
|
|
|
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(回复按时间正序)
|
2026-03-26 18:14:16 +08:00
|
|
|
|
func (r *commentRepository) GetRepliesByCursor(ctx context.Context, rootID string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Comment], error) {
|
feat(api): add cursor-based pagination for posts, comments, messages, groups, and notifications
Implement cursor-based pagination across multiple API endpoints to improve performance and enable efficient infinite scrolling. This replaces traditional offset-based pagination for high-volume data retrieval.
Changes:
- Add cursor pagination DTOs for all entity types (Post, Comment, Message, Conversation, Group, Notification, GroupMember, GroupAnnouncement)
- Implement cursor pagination methods in repositories with support for various sort orders (created_at, updated_at, join_time, seq)
- Add cursor pagination handlers that maintain backward compatibility with existing offset pagination
- Add new API routes for cursor-based endpoints (/cursor suffix)
- Add helper converter functions for pointer slice types in DTO conversions
2026-03-20 23:03:23 +08:00
|
|
|
|
// 构建基础查询
|
|
|
|
|
|
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 {
|
|
|
|
|
|
// 无效游标,返回空列表
|
2026-04-07 00:07:40 +08:00
|
|
|
|
return cursor.NewCursorPageResult([]*model.Comment{}, "", "", false), nil
|
feat(api): add cursor-based pagination for posts, comments, messages, groups, and notifications
Implement cursor-based pagination across multiple API endpoints to improve performance and enable efficient infinite scrolling. This replaces traditional offset-based pagination for high-volume data retrieval.
Changes:
- Add cursor pagination DTOs for all entity types (Post, Comment, Message, Conversation, Group, Notification, GroupMember, GroupAnnouncement)
- Implement cursor pagination methods in repositories with support for various sort orders (created_at, updated_at, join_time, seq)
- Add cursor pagination handlers that maintain backward compatibility with existing offset pagination
- Add new API routes for cursor-based endpoints (/cursor suffix)
- Add helper converter functions for pointer slice types in DTO conversions
2026-03-20 23:03:23 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 执行查询
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|