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
|
|
|
|
// NotificationRepository 通知仓储接口
|
|
|
|
|
|
type NotificationRepository interface {
|
|
|
|
|
|
Create(notification *model.Notification) error
|
|
|
|
|
|
GetByID(id string) (*model.Notification, error)
|
|
|
|
|
|
GetByUserID(userID string, page, pageSize int, unreadOnly bool) ([]*model.Notification, int64, error)
|
|
|
|
|
|
MarkAsRead(id string) error
|
|
|
|
|
|
MarkAllAsRead(userID string) error
|
|
|
|
|
|
Delete(id string) error
|
|
|
|
|
|
GetUnreadCount(userID string) (int64, error)
|
|
|
|
|
|
DeleteAllByUserID(userID string) error
|
|
|
|
|
|
GetNotificationsByCursor(ctx context.Context, userID string, unreadOnly bool, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Notification], error)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// notificationRepository 通知仓储实现
|
|
|
|
|
|
type notificationRepository struct {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
db *gorm.DB
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// NewNotificationRepository 创建通知仓储
|
2026-03-26 18:14:16 +08:00
|
|
|
|
func NewNotificationRepository(db *gorm.DB) NotificationRepository {
|
|
|
|
|
|
return ¬ificationRepository{db: db}
|
2026-03-09 21:28:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Create 创建通知
|
2026-03-26 18:14:16 +08:00
|
|
|
|
func (r *notificationRepository) Create(notification *model.Notification) error {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
return r.db.Create(notification).Error
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetByID 根据ID获取通知
|
2026-03-26 18:14:16 +08:00
|
|
|
|
func (r *notificationRepository) GetByID(id string) (*model.Notification, error) {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
var notification model.Notification
|
|
|
|
|
|
err := r.db.First(¬ification, "id = ?", id).Error
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
return ¬ification, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetByUserID 获取用户通知
|
2026-03-26 18:14:16 +08:00
|
|
|
|
func (r *notificationRepository) GetByUserID(userID string, page, pageSize int, unreadOnly bool) ([]*model.Notification, int64, error) {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
var notifications []*model.Notification
|
|
|
|
|
|
var total int64
|
|
|
|
|
|
|
|
|
|
|
|
query := r.db.Model(&model.Notification{}).Where("user_id = ?", userID)
|
|
|
|
|
|
|
|
|
|
|
|
if unreadOnly {
|
|
|
|
|
|
query = query.Where("is_read = ?", false)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
query.Count(&total)
|
|
|
|
|
|
|
|
|
|
|
|
offset := (page - 1) * pageSize
|
|
|
|
|
|
err := query.Offset(offset).Limit(pageSize).Order("created_at DESC").Find(¬ifications).Error
|
|
|
|
|
|
|
|
|
|
|
|
return notifications, total, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// MarkAsRead 标记为已读
|
2026-03-26 18:14:16 +08:00
|
|
|
|
func (r *notificationRepository) MarkAsRead(id string) error {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
return r.db.Model(&model.Notification{}).Where("id = ?", id).Update("is_read", true).Error
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// MarkAllAsRead 标记所有为已读
|
2026-03-26 18:14:16 +08:00
|
|
|
|
func (r *notificationRepository) MarkAllAsRead(userID string) error {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
return r.db.Model(&model.Notification{}).Where("user_id = ?", userID).Update("is_read", true).Error
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Delete 删除通知
|
2026-03-26 18:14:16 +08:00
|
|
|
|
func (r *notificationRepository) Delete(id string) error {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
return r.db.Delete(&model.Notification{}, "id = ?", id).Error
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetUnreadCount 获取未读数量
|
2026-03-26 18:14:16 +08:00
|
|
|
|
func (r *notificationRepository) GetUnreadCount(userID string) (int64, error) {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
var count int64
|
|
|
|
|
|
err := r.db.Model(&model.Notification{}).Where("user_id = ? AND is_read = ?", userID, false).Count(&count).Error
|
|
|
|
|
|
return count, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// DeleteAllByUserID 删除用户所有通知
|
2026-03-26 18:14:16 +08:00
|
|
|
|
func (r *notificationRepository) DeleteAllByUserID(userID string) error {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
return r.db.Where("user_id = ?", userID).Delete(&model.Notification{}).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
|
|
|
|
|
|
|
|
|
|
// ========== Cursor Pagination Methods ==========
|
|
|
|
|
|
|
|
|
|
|
|
// GetNotificationsByCursor 游标分页获取用户通知
|
|
|
|
|
|
// 排序方式:created_at DESC
|
2026-03-26 18:14:16 +08:00
|
|
|
|
func (r *notificationRepository) GetNotificationsByCursor(ctx context.Context, userID string, unreadOnly bool, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Notification], 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.Notification{}).Where("user_id = ?", userID)
|
|
|
|
|
|
|
|
|
|
|
|
if unreadOnly {
|
|
|
|
|
|
query = query.Where("is_read = ?", false)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 使用游标构建器
|
|
|
|
|
|
builder := cursor.NewBuilder(query, cursor.SortByCreatedAtDesc).
|
|
|
|
|
|
WithCursor(req.Cursor, req.Direction).
|
|
|
|
|
|
WithPageSize(req.PageSize)
|
|
|
|
|
|
|
|
|
|
|
|
if builder.Error() != nil {
|
|
|
|
|
|
// 无效游标,返回空列表
|
|
|
|
|
|
return cursor.NewCursorPageResult[*model.Notification]([]*model.Notification{}, "", "", false), nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 执行查询
|
|
|
|
|
|
var notifications []*model.Notification
|
|
|
|
|
|
query = builder.Build()
|
|
|
|
|
|
if err := query.Find(¬ifications).Error; err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 构建响应
|
|
|
|
|
|
pageSize := builder.GetPageSize()
|
|
|
|
|
|
hasMore := cursor.HasMore(len(notifications), pageSize)
|
|
|
|
|
|
if hasMore {
|
|
|
|
|
|
notifications = notifications[:pageSize]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 生成游标
|
|
|
|
|
|
var nextCursor, prevCursor string
|
|
|
|
|
|
if len(notifications) > 0 {
|
|
|
|
|
|
// 下一页游标
|
|
|
|
|
|
if hasMore {
|
|
|
|
|
|
lastNotification := notifications[len(notifications)-1]
|
|
|
|
|
|
nextCursor = cursor.NewCursor(
|
|
|
|
|
|
cursor.FormatTime(lastNotification.CreatedAt),
|
|
|
|
|
|
lastNotification.ID,
|
|
|
|
|
|
cursor.SortByCreatedAtDesc,
|
|
|
|
|
|
).Encode()
|
|
|
|
|
|
}
|
|
|
|
|
|
// 上一页游标
|
|
|
|
|
|
firstNotification := notifications[0]
|
|
|
|
|
|
prevCursor = cursor.NewCursor(
|
|
|
|
|
|
cursor.FormatTime(firstNotification.CreatedAt),
|
|
|
|
|
|
firstNotification.ID,
|
|
|
|
|
|
cursor.SortByCreatedAtDesc,
|
|
|
|
|
|
).Encode()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return cursor.NewCursorPageResult(notifications, nextCursor, prevCursor, hasMore), nil
|
|
|
|
|
|
}
|