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
This commit is contained in:
152
internal/pkg/cursor/cursor.go
Normal file
152
internal/pkg/cursor/cursor.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package cursor
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
)
|
||||
|
||||
// SortOrder 游标排序类型
|
||||
type SortOrder int
|
||||
|
||||
const (
|
||||
// SortByCreatedAtDesc 按创建时间降序
|
||||
SortByCreatedAtDesc SortOrder = iota
|
||||
// SortByCreatedAtAsc 按创建时间升序
|
||||
SortByCreatedAtAsc
|
||||
// SortByIDDesc 按ID降序
|
||||
SortByIDDesc
|
||||
// SortByIDAsc 按ID升序
|
||||
SortByIDAsc
|
||||
// SortBySeqDesc 按序列号降序(用于消息)
|
||||
SortBySeqDesc
|
||||
// SortBySeqAsc 按序列号升序
|
||||
SortBySeqAsc
|
||||
// SortByUpdatedAtDesc 按更新时间降序
|
||||
SortByUpdatedAtDesc
|
||||
// SortByUpdatedAtAsc 按更新时间升序
|
||||
SortByUpdatedAtAsc
|
||||
// SortByJoinTimeDesc 按加入时间降序(用于群成员)
|
||||
SortByJoinTimeDesc
|
||||
// SortByJoinTimeAsc 按加入时间升序(用于群成员)
|
||||
SortByJoinTimeAsc
|
||||
)
|
||||
|
||||
// String 返回排序类型的字符串表示
|
||||
func (s SortOrder) String() string {
|
||||
switch s {
|
||||
case SortByCreatedAtDesc:
|
||||
return "created_at DESC"
|
||||
case SortByCreatedAtAsc:
|
||||
return "created_at ASC"
|
||||
case SortByIDDesc:
|
||||
return "id DESC"
|
||||
case SortByIDAsc:
|
||||
return "id ASC"
|
||||
case SortBySeqDesc:
|
||||
return "seq DESC"
|
||||
case SortBySeqAsc:
|
||||
return "seq ASC"
|
||||
case SortByUpdatedAtDesc:
|
||||
return "updated_at DESC"
|
||||
case SortByUpdatedAtAsc:
|
||||
return "updated_at ASC"
|
||||
case SortByJoinTimeDesc:
|
||||
return "join_time DESC"
|
||||
case SortByJoinTimeAsc:
|
||||
return "join_time ASC"
|
||||
default:
|
||||
return "created_at DESC"
|
||||
}
|
||||
}
|
||||
|
||||
// IsDescending 是否为降序
|
||||
func (s SortOrder) IsDescending() bool {
|
||||
return s == SortByCreatedAtDesc || s == SortByIDDesc || s == SortBySeqDesc || s == SortByUpdatedAtDesc || s == SortByJoinTimeDesc
|
||||
}
|
||||
|
||||
// SortField 返回排序字段名
|
||||
func (s SortOrder) SortField() string {
|
||||
switch s {
|
||||
case SortByCreatedAtDesc, SortByCreatedAtAsc:
|
||||
return "created_at"
|
||||
case SortByIDDesc, SortByIDAsc:
|
||||
return "id"
|
||||
case SortBySeqDesc, SortBySeqAsc:
|
||||
return "seq"
|
||||
case SortByUpdatedAtDesc, SortByUpdatedAtAsc:
|
||||
return "updated_at"
|
||||
case SortByJoinTimeDesc, SortByJoinTimeAsc:
|
||||
return "join_time"
|
||||
default:
|
||||
return "created_at"
|
||||
}
|
||||
}
|
||||
|
||||
// cursorJSON 游标 JSON 结构(用于序列化)
|
||||
type cursorJSON struct {
|
||||
SortValue string `json:"sv"` // 排序字段值(时间戳或ID)
|
||||
ID string `json:"id"` // 记录唯一ID
|
||||
Order SortOrder `json:"so"` // 排序方式
|
||||
}
|
||||
|
||||
// Cursor 游标数据结构
|
||||
type Cursor struct {
|
||||
SortValue string // 排序字段值(时间戳或ID)
|
||||
ID string // 记录唯一ID
|
||||
Order SortOrder // 排序方式
|
||||
}
|
||||
|
||||
// Encode 编码游标为 Base64URL 字符串
|
||||
func (c *Cursor) Encode() string {
|
||||
if c == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
data, err := json.Marshal(&cursorJSON{
|
||||
SortValue: c.SortValue,
|
||||
ID: c.ID,
|
||||
Order: c.Order,
|
||||
})
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return base64.URLEncoding.EncodeToString(data)
|
||||
}
|
||||
|
||||
// DecodeCursor 解码 Base64URL 字符串为游标
|
||||
func DecodeCursor(encoded string) (*Cursor, error) {
|
||||
if encoded == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
data, err := base64.URLEncoding.DecodeString(encoded)
|
||||
if err != nil {
|
||||
// 尝试使用 RawURLEncoding(不带填充的编码)
|
||||
data, err = base64.RawURLEncoding.DecodeString(encoded)
|
||||
if err != nil {
|
||||
return nil, errors.New("invalid cursor encoding: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
var cj cursorJSON
|
||||
if err := json.Unmarshal(data, &cj); err != nil {
|
||||
return nil, errors.New("invalid cursor format: " + err.Error())
|
||||
}
|
||||
|
||||
return &Cursor{
|
||||
SortValue: cj.SortValue,
|
||||
ID: cj.ID,
|
||||
Order: cj.Order,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewCursor 创建新的游标
|
||||
func NewCursor(sortValue, id string, order SortOrder) *Cursor {
|
||||
return &Cursor{
|
||||
SortValue: sortValue,
|
||||
ID: id,
|
||||
Order: order,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user