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-26 01:50:54 +08:00
|
|
|
|
"errors"
|
2026-03-09 21:28:58 +08:00
|
|
|
|
|
|
|
|
|
|
"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)
|
2026-03-26 18:14:16 +08:00
|
|
|
|
TransferOwner(groupID, oldOwnerID, newOwnerID string) error
|
2026-03-09 21:28:58 +08:00
|
|
|
|
|
|
|
|
|
|
// 群成员操作
|
|
|
|
|
|
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
|
2026-03-14 18:01:55 +08:00
|
|
|
|
|
|
|
|
|
|
// 管理端群组操作
|
|
|
|
|
|
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)
|
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
|
|
|
|
|
|
|
|
|
|
// 游标分页方法
|
|
|
|
|
|
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)
|
2026-03-14 18:01:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GroupMemberWithUser 群成员带用户信息的结构
|
|
|
|
|
|
type GroupMemberWithUser struct {
|
|
|
|
|
|
model.GroupMember
|
|
|
|
|
|
User *model.User
|
2026-03-09 21:28:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-26 18:14:16 +08:00
|
|
|
|
// TransferOwner 转让群主(在事务中更新群主和角色)
|
|
|
|
|
|
func (r *groupRepository) TransferOwner(groupID, oldOwnerID, newOwnerID string) error {
|
|
|
|
|
|
return r.db.Transaction(func(tx *gorm.DB) error {
|
|
|
|
|
|
// 更新群组的群主
|
|
|
|
|
|
if err := tx.Model(&model.Group{}).Where("id = ?", groupID).Update("owner_id", newOwnerID).Error; err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新原群主为管理员
|
|
|
|
|
|
if err := tx.Model(&model.GroupMember{}).
|
|
|
|
|
|
Where("group_id = ? AND user_id = ?", groupID, oldOwnerID).
|
|
|
|
|
|
Update("role", model.GroupRoleAdmin).Error; err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新新群主为群主
|
|
|
|
|
|
if err := tx.Model(&model.GroupMember{}).
|
|
|
|
|
|
Where("group_id = ? AND user_id = ?", groupID, newOwnerID).
|
|
|
|
|
|
Update("role", model.GroupRoleOwner).Error; err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 21:28:58 +08:00
|
|
|
|
// 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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-26 01:50:54 +08:00
|
|
|
|
// GetExistingMembers 批量检查用户是否是群成员(解决 N+1 问题)
|
|
|
|
|
|
// 返回 map[userID]bool
|
|
|
|
|
|
func (r *groupRepository) GetExistingMembers(groupID string, userIDs []string) (map[string]bool, error) {
|
|
|
|
|
|
result := make(map[string]bool)
|
|
|
|
|
|
if len(userIDs) == 0 || groupID == "" {
|
|
|
|
|
|
return result, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化所有用户为 false
|
|
|
|
|
|
for _, id := range userIDs {
|
|
|
|
|
|
result[id] = false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 一次性查询所有群成员记录
|
|
|
|
|
|
var existingIDs []string
|
|
|
|
|
|
err := r.db.Model(&model.GroupMember{}).
|
|
|
|
|
|
Where("group_id = ? AND user_id IN ?", groupID, userIDs).
|
|
|
|
|
|
Pluck("user_id", &existingIDs).Error
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新已存在的成员
|
|
|
|
|
|
for _, id := range existingIDs {
|
|
|
|
|
|
result[id] = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return result, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 21:28:58 +08:00
|
|
|
|
// 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
|
|
|
|
|
|
}
|
2026-03-14 18:01:55 +08:00
|
|
|
|
|
|
|
|
|
|
// ==================== 管理端群组操作 ====================
|
|
|
|
|
|
|
|
|
|
|
|
// 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 {
|
2026-03-26 01:50:54 +08:00
|
|
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
2026-03-14 18:01:55 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
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 ==========
|
|
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
|
}
|