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
153 lines
3.5 KiB
Go
153 lines
3.5 KiB
Go
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,
|
||
}
|
||
}
|