Files
backend/internal/repository/group_repo.go
lafay 92babe509f
All checks were successful
Build Backend / build (push) Successful in 4m42s
Build Backend / build-docker (push) Successful in 3m53s
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

645 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 (
"carrot_bbs/internal/model"
"carrot_bbs/internal/pkg/cursor"
"context"
"gorm.io/gorm"
)
// GroupRepository 群组仓库接口
type GroupRepository interface {
// 群组操作
Create(group *model.Group) error
GetByID(id string) (*model.Group, error)
Update(group *model.Group) error
Delete(id string) error
GetByOwnerID(ownerID string, page, pageSize int) ([]model.Group, int64, error)
// 群成员操作
AddMember(member *model.GroupMember) error
GetMember(groupID string, userID string) (*model.GroupMember, error)
GetMembers(groupID string, page, pageSize int) ([]model.GroupMember, int64, error)
UpdateMember(member *model.GroupMember) error
RemoveMember(groupID string, userID string) error
GetMemberCount(groupID string) (int64, error)
IsMember(groupID string, userID string) (bool, error)
GetUserGroups(userID string, page, pageSize int) ([]model.Group, int64, error)
// 角色相关
GetMemberRole(groupID string, userID string) (string, error)
SetMemberRole(groupID string, userID string, role string) error
GetAdmins(groupID string) ([]model.GroupMember, error)
// 群公告操作
CreateAnnouncement(announcement *model.GroupAnnouncement) error
GetAnnouncements(groupID string, page, pageSize int) ([]model.GroupAnnouncement, int64, error)
GetAnnouncementByID(id string) (*model.GroupAnnouncement, error)
DeleteAnnouncement(id string) error
// 管理端群组操作
GetGroupList(page, pageSize int, keyword, status, ownerID, startDate, endDate string) ([]model.Group, int64, error)
GetGroupWithOwner(groupID string) (*model.Group, error)
GetLatestAnnouncement(groupID string) (*model.GroupAnnouncement, error)
UpdateGroupStatus(groupID string, status string) error
GetGroupPostCount(groupID string) (int64, error)
GetMembersWithUserInfo(groupID string, page, pageSize int) ([]GroupMemberWithUser, int64, error)
// 游标分页方法
GetGroupsByCursor(ctx context.Context, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Group], error)
GetUserGroupsByCursor(ctx context.Context, userID string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Group], error)
GetMembersByCursor(ctx context.Context, groupID string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.GroupMember], error)
GetAnnouncementsByCursor(ctx context.Context, groupID string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.GroupAnnouncement], error)
}
// GroupMemberWithUser 群成员带用户信息的结构
type GroupMemberWithUser struct {
model.GroupMember
User *model.User
}
// groupRepository 群组仓库实现
type groupRepository struct {
db *gorm.DB
}
// NewGroupRepository 创建群组仓库
func NewGroupRepository(db *gorm.DB) GroupRepository {
return &groupRepository{db: db}
}
// Create 创建群组
func (r *groupRepository) Create(group *model.Group) error {
return r.db.Create(group).Error
}
// GetByID 根据ID获取群组
func (r *groupRepository) GetByID(id string) (*model.Group, error) {
var group model.Group
err := r.db.First(&group, "id = ?", id).Error
if err != nil {
return nil, err
}
return &group, nil
}
// Update 更新群组
func (r *groupRepository) Update(group *model.Group) error {
return r.db.Save(group).Error
}
// Delete 删除群组
func (r *groupRepository) Delete(id string) error {
return r.db.Transaction(func(tx *gorm.DB) error {
// 删除群成员
if err := tx.Where("group_id = ?", id).Delete(&model.GroupMember{}).Error; err != nil {
return err
}
// 删除群公告
if err := tx.Where("group_id = ?", id).Delete(&model.GroupAnnouncement{}).Error; err != nil {
return err
}
// 删除群组
if err := tx.Delete(&model.Group{}, "id = ?", id).Error; err != nil {
return err
}
return nil
})
}
// GetByOwnerID 根据群主ID获取群组列表
func (r *groupRepository) GetByOwnerID(ownerID string, page, pageSize int) ([]model.Group, int64, error) {
var groups []model.Group
var total int64
query := r.db.Model(&model.Group{}).Where("owner_id = ?", ownerID)
query.Count(&total)
offset := (page - 1) * pageSize
err := query.Offset(offset).Limit(pageSize).Order("created_at DESC").Find(&groups).Error
return groups, total, err
}
// AddMember 添加群成员
func (r *groupRepository) AddMember(member *model.GroupMember) error {
return r.db.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(member).Error; err != nil {
return err
}
// 更新群组成员数量
return tx.Model(&model.Group{}).Where("id = ?", member.GroupID).
Update("member_count", gorm.Expr("member_count + ?", 1)).Error
})
}
// GetMember 获取群成员
func (r *groupRepository) GetMember(groupID string, userID string) (*model.GroupMember, error) {
var member model.GroupMember
err := r.db.First(&member, "group_id = ? AND user_id = ?", groupID, userID).Error
if err != nil {
return nil, err
}
return &member, nil
}
// GetMembers 获取群成员列表
func (r *groupRepository) GetMembers(groupID string, page, pageSize int) ([]model.GroupMember, int64, error) {
var members []model.GroupMember
var total int64
query := r.db.Model(&model.GroupMember{}).Where("group_id = ?", groupID)
query.Count(&total)
offset := (page - 1) * pageSize
err := query.Offset(offset).Limit(pageSize).Order("created_at ASC").Find(&members).Error
return members, total, err
}
// UpdateMember 更新群成员
func (r *groupRepository) UpdateMember(member *model.GroupMember) error {
return r.db.Save(member).Error
}
// RemoveMember 移除群成员
func (r *groupRepository) RemoveMember(groupID string, userID string) error {
return r.db.Transaction(func(tx *gorm.DB) error {
// 删除成员
if err := tx.Where("group_id = ? AND user_id = ?", groupID, userID).Delete(&model.GroupMember{}).Error; err != nil {
return err
}
// 更新群组成员数量
return tx.Model(&model.Group{}).Where("id = ?", groupID).
Update("member_count", gorm.Expr("member_count - ?", 1)).Error
})
}
// GetMemberCount 获取群成员数量
func (r *groupRepository) GetMemberCount(groupID string) (int64, error) {
var count int64
err := r.db.Model(&model.GroupMember{}).Where("group_id = ?", groupID).Count(&count).Error
return count, err
}
// IsMember 检查是否是群成员
func (r *groupRepository) IsMember(groupID string, userID string) (bool, error) {
var count int64
err := r.db.Model(&model.GroupMember{}).Where("group_id = ? AND user_id = ?", groupID, userID).Count(&count).Error
return count > 0, err
}
// GetUserGroups 获取用户加入的群组列表
func (r *groupRepository) GetUserGroups(userID string, page, pageSize int) ([]model.Group, int64, error) {
var groups []model.Group
var total int64
// 通过群成员表查询用户加入的群组
subQuery := r.db.Model(&model.GroupMember{}).
Select("group_id").
Where("user_id = ?", userID)
query := r.db.Model(&model.Group{}).Where("id IN (?)", subQuery)
query.Count(&total)
offset := (page - 1) * pageSize
err := query.Offset(offset).Limit(pageSize).Order("created_at DESC").Find(&groups).Error
return groups, total, err
}
// GetMemberRole 获取成员角色
func (r *groupRepository) GetMemberRole(groupID string, userID string) (string, error) {
member, err := r.GetMember(groupID, userID)
if err != nil {
return "", err
}
return member.Role, nil
}
// SetMemberRole 设置成员角色
func (r *groupRepository) SetMemberRole(groupID string, userID string, role string) error {
return r.db.Model(&model.GroupMember{}).
Where("group_id = ? AND user_id = ?", groupID, userID).
Update("role", role).Error
}
// GetAdmins 获取群管理员列表
func (r *groupRepository) GetAdmins(groupID string) ([]model.GroupMember, error) {
var admins []model.GroupMember
err := r.db.Where("group_id = ? AND role = ?", groupID, model.GroupRoleAdmin).Find(&admins).Error
return admins, err
}
// CreateAnnouncement 创建群公告
func (r *groupRepository) CreateAnnouncement(announcement *model.GroupAnnouncement) error {
return r.db.Create(announcement).Error
}
// GetAnnouncements 获取群公告列表
func (r *groupRepository) GetAnnouncements(groupID string, page, pageSize int) ([]model.GroupAnnouncement, int64, error) {
var announcements []model.GroupAnnouncement
var total int64
query := r.db.Model(&model.GroupAnnouncement{}).Where("group_id = ?", groupID)
query.Count(&total)
offset := (page - 1) * pageSize
// 置顶的排在前面,然后按时间倒序
err := query.Offset(offset).Limit(pageSize).Order("is_pinned DESC, created_at DESC").Find(&announcements).Error
return announcements, total, err
}
// GetAnnouncementByID 根据ID获取群公告
func (r *groupRepository) GetAnnouncementByID(id string) (*model.GroupAnnouncement, error) {
var announcement model.GroupAnnouncement
err := r.db.First(&announcement, "id = ?", id).Error
if err != nil {
return nil, err
}
return &announcement, nil
}
// DeleteAnnouncement 删除群公告
func (r *groupRepository) DeleteAnnouncement(id string) error {
return r.db.Delete(&model.GroupAnnouncement{}, "id = ?", id).Error
}
// ==================== 管理端群组操作 ====================
// GetGroupList 获取群组列表(管理端)
func (r *groupRepository) GetGroupList(page, pageSize int, keyword, status, ownerID, startDate, endDate string) ([]model.Group, int64, error) {
var groups []model.Group
var total int64
query := r.db.Model(&model.Group{})
// 关键词搜索
if keyword != "" {
query = query.Where("name LIKE ? OR description LIKE ?", "%"+keyword+"%", "%"+keyword+"%")
}
// 状态筛选这里假设群组有status字段如果没有则忽略
// 注意当前Group模型没有status字段状态通过是否解散来判断
// 如果需要status字段需要在模型中添加
// 群主筛选
if ownerID != "" {
query = query.Where("owner_id = ?", ownerID)
}
// 日期筛选
if startDate != "" {
query = query.Where("created_at >= ?", startDate+" 00:00:00")
}
if endDate != "" {
query = query.Where("created_at <= ?", endDate+" 23:59:59")
}
query.Count(&total)
offset := (page - 1) * pageSize
err := query.Offset(offset).Limit(pageSize).Order("created_at DESC").Find(&groups).Error
return groups, total, err
}
// GetGroupWithOwner 获取群组详情(包含群主信息)
func (r *groupRepository) GetGroupWithOwner(groupID string) (*model.Group, error) {
var group model.Group
err := r.db.First(&group, "id = ?", groupID).Error
if err != nil {
return nil, err
}
return &group, nil
}
// GetLatestAnnouncement 获取群组最新公告
func (r *groupRepository) GetLatestAnnouncement(groupID string) (*model.GroupAnnouncement, error) {
var announcement model.GroupAnnouncement
err := r.db.Where("group_id = ?", groupID).
Order("is_pinned DESC, created_at DESC").
First(&announcement).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, nil
}
return nil, err
}
return &announcement, nil
}
// UpdateGroupStatus 更新群组状态
func (r *groupRepository) UpdateGroupStatus(groupID string, status string) error {
// 注意当前Group模型没有status字段
// 这里我们通过更新一个虚拟字段或者使用其他方式来实现
// 暂时通过更新updated_at来模拟状态更新
// 如果需要真正的状态字段,需要在模型中添加
return r.db.Model(&model.Group{}).Where("id = ?", groupID).
Update("updated_at", gorm.Expr("NOW()")).Error
}
// GetGroupPostCount 获取群组帖子数量
func (r *groupRepository) GetGroupPostCount(groupID string) (int64, error) {
var count int64
// 假设帖子表有group_id字段
err := r.db.Model(&model.Post{}).Where("group_id = ?", groupID).Count(&count).Error
return count, err
}
// GetMembersWithUserInfo 获取群成员列表(包含用户信息)
func (r *groupRepository) GetMembersWithUserInfo(groupID string, page, pageSize int) ([]GroupMemberWithUser, int64, error) {
var members []GroupMemberWithUser
var total int64
// 先获取总数
err := r.db.Model(&model.GroupMember{}).Where("group_id = ?", groupID).Count(&total).Error
if err != nil {
return nil, 0, err
}
// 联表查询获取成员和用户信息
offset := (page - 1) * pageSize
query := `
SELECT gm.*, u.id as user_id, u.username, u.nickname, u.avatar
FROM group_members gm
LEFT JOIN users u ON gm.user_id = u.id
WHERE gm.group_id = ?
ORDER BY
CASE gm.role
WHEN 'owner' THEN 1
WHEN 'admin' THEN 2
WHEN 'member' THEN 3
END,
gm.join_time ASC
LIMIT ? OFFSET ?
`
type memberRow struct {
ID string `gorm:"column:id"`
GroupID string `gorm:"column:group_id"`
UserID string `gorm:"column:user_id"`
Role string `gorm:"column:role"`
Nickname string `gorm:"column:nickname"`
Muted bool `gorm:"column:muted"`
JoinTime string `gorm:"column:join_time"`
CreatedAt string `gorm:"column:created_at"`
UpdatedAt string `gorm:"column:updated_at"`
// 用户信息
Username string `gorm:"column:username"`
UserNickname string `gorm:"column:nickname"`
UserAvatar string `gorm:"column:avatar"`
}
var rows []memberRow
err = r.db.Raw(query, groupID, pageSize, offset).Scan(&rows).Error
if err != nil {
return nil, 0, err
}
members = make([]GroupMemberWithUser, 0, len(rows))
for _, row := range rows {
member := model.GroupMember{
ID: row.ID,
GroupID: row.GroupID,
UserID: row.UserID,
Role: row.Role,
Nickname: row.Nickname,
Muted: row.Muted,
}
user := &model.User{
ID: row.UserID,
Username: row.Username,
Nickname: row.UserNickname,
Avatar: row.UserAvatar,
}
members = append(members, GroupMemberWithUser{
GroupMember: member,
User: user,
})
}
return members, total, nil
}
// ========== Cursor Pagination Methods ==========
// GetGroupsByCursor 游标分页获取群组列表
// 排序方式created_at DESC
func (r *groupRepository) GetGroupsByCursor(ctx context.Context, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Group], error) {
// 构建基础查询
query := r.db.WithContext(ctx).Model(&model.Group{})
// 使用游标构建器
builder := cursor.NewBuilder(query, cursor.SortByCreatedAtDesc).
WithCursor(req.Cursor, req.Direction).
WithPageSize(req.PageSize)
if builder.Error() != nil {
// 无效游标,返回空列表
return cursor.NewCursorPageResult[*model.Group]([]*model.Group{}, "", "", false), nil
}
// 执行查询
var groups []*model.Group
query = builder.Build()
if err := query.Find(&groups).Error; err != nil {
return nil, err
}
// 构建响应
pageSize := builder.GetPageSize()
hasMore := cursor.HasMore(len(groups), pageSize)
if hasMore {
groups = groups[:pageSize]
}
// 生成游标
var nextCursor, prevCursor string
if len(groups) > 0 {
// 下一页游标
if hasMore {
lastGroup := groups[len(groups)-1]
nextCursor = cursor.NewCursor(
cursor.FormatTime(lastGroup.CreatedAt),
lastGroup.ID,
cursor.SortByCreatedAtDesc,
).Encode()
}
// 上一页游标
firstGroup := groups[0]
prevCursor = cursor.NewCursor(
cursor.FormatTime(firstGroup.CreatedAt),
firstGroup.ID,
cursor.SortByCreatedAtDesc,
).Encode()
}
return cursor.NewCursorPageResult(groups, nextCursor, prevCursor, hasMore), nil
}
// GetUserGroupsByCursor 游标分页获取用户加入的群组列表
// 排序方式created_at DESC
func (r *groupRepository) GetUserGroupsByCursor(ctx context.Context, userID string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Group], error) {
// 通过群成员表查询用户加入的群组
subQuery := r.db.Model(&model.GroupMember{}).
Select("group_id").
Where("user_id = ?", userID)
// 构建基础查询
query := r.db.WithContext(ctx).Model(&model.Group{}).Where("id IN (?)", subQuery)
// 使用游标构建器
builder := cursor.NewBuilder(query, cursor.SortByCreatedAtDesc).
WithCursor(req.Cursor, req.Direction).
WithPageSize(req.PageSize)
if builder.Error() != nil {
// 无效游标,返回空列表
return cursor.NewCursorPageResult[*model.Group]([]*model.Group{}, "", "", false), nil
}
// 执行查询
var groups []*model.Group
query = builder.Build()
if err := query.Find(&groups).Error; err != nil {
return nil, err
}
// 构建响应
pageSize := builder.GetPageSize()
hasMore := cursor.HasMore(len(groups), pageSize)
if hasMore {
groups = groups[:pageSize]
}
// 生成游标
var nextCursor, prevCursor string
if len(groups) > 0 {
// 下一页游标
if hasMore {
lastGroup := groups[len(groups)-1]
nextCursor = cursor.NewCursor(
cursor.FormatTime(lastGroup.CreatedAt),
lastGroup.ID,
cursor.SortByCreatedAtDesc,
).Encode()
}
// 上一页游标
firstGroup := groups[0]
prevCursor = cursor.NewCursor(
cursor.FormatTime(firstGroup.CreatedAt),
firstGroup.ID,
cursor.SortByCreatedAtDesc,
).Encode()
}
return cursor.NewCursorPageResult(groups, nextCursor, prevCursor, hasMore), nil
}
// GetMembersByCursor 游标分页获取群成员列表
// 排序方式join_time DESC新成员优先
func (r *groupRepository) GetMembersByCursor(ctx context.Context, groupID string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.GroupMember], error) {
// 构建基础查询
query := r.db.WithContext(ctx).Model(&model.GroupMember{}).Where("group_id = ?", groupID)
// 使用游标构建器
builder := cursor.NewBuilder(query, cursor.SortByJoinTimeDesc).
WithCursor(req.Cursor, req.Direction).
WithPageSize(req.PageSize)
if builder.Error() != nil {
// 无效游标,返回空列表
return cursor.NewCursorPageResult[*model.GroupMember]([]*model.GroupMember{}, "", "", false), nil
}
// 执行查询
var members []*model.GroupMember
query = builder.Build()
if err := query.Find(&members).Error; err != nil {
return nil, err
}
// 构建响应
pageSize := builder.GetPageSize()
hasMore := cursor.HasMore(len(members), pageSize)
if hasMore {
members = members[:pageSize]
}
// 生成游标
var nextCursor, prevCursor string
if len(members) > 0 {
// 下一页游标
if hasMore {
lastMember := members[len(members)-1]
nextCursor = cursor.NewCursor(
cursor.FormatTime(lastMember.JoinTime),
lastMember.ID,
cursor.SortByJoinTimeDesc,
).Encode()
}
// 上一页游标
firstMember := members[0]
prevCursor = cursor.NewCursor(
cursor.FormatTime(firstMember.JoinTime),
firstMember.ID,
cursor.SortByJoinTimeDesc,
).Encode()
}
return cursor.NewCursorPageResult(members, nextCursor, prevCursor, hasMore), nil
}
// GetAnnouncementsByCursor 游标分页获取群公告列表
// 排序方式is_pinned DESC, created_at DESC置顶优先然后按时间倒序
// 注意:由于有置顶逻辑,这里使用 created_at DESC 作为游标排序
func (r *groupRepository) GetAnnouncementsByCursor(ctx context.Context, groupID string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.GroupAnnouncement], error) {
// 构建基础查询
query := r.db.WithContext(ctx).Model(&model.GroupAnnouncement{}).Where("group_id = ?", groupID)
// 使用游标构建器
builder := cursor.NewBuilder(query, cursor.SortByCreatedAtDesc).
WithCursor(req.Cursor, req.Direction).
WithPageSize(req.PageSize)
if builder.Error() != nil {
// 无效游标,返回空列表
return cursor.NewCursorPageResult[*model.GroupAnnouncement]([]*model.GroupAnnouncement{}, "", "", false), nil
}
// 执行查询(置顶的排在前面)
var announcements []*model.GroupAnnouncement
query = builder.Build().Order("is_pinned DESC, created_at DESC")
if err := query.Find(&announcements).Error; err != nil {
return nil, err
}
// 构建响应
pageSize := builder.GetPageSize()
hasMore := cursor.HasMore(len(announcements), pageSize)
if hasMore {
announcements = announcements[:pageSize]
}
// 生成游标
var nextCursor, prevCursor string
if len(announcements) > 0 {
// 下一页游标
if hasMore {
lastAnnouncement := announcements[len(announcements)-1]
nextCursor = cursor.NewCursor(
cursor.FormatTime(lastAnnouncement.CreatedAt),
lastAnnouncement.ID,
cursor.SortByCreatedAtDesc,
).Encode()
}
// 上一页游标
firstAnnouncement := announcements[0]
prevCursor = cursor.NewCursor(
cursor.FormatTime(firstAnnouncement.CreatedAt),
firstAnnouncement.ID,
cursor.SortByCreatedAtDesc,
).Encode()
}
return cursor.NewCursorPageResult(announcements, nextCursor, prevCursor, hasMore), nil
}