Files
backend/internal/repository/group_repo.go
lafay 2f584c1f2c
All checks were successful
Build Backend / build (push) Successful in 5m10s
Build Backend / build-docker (push) Successful in 2m22s
This is a breaking change that renames the entire project from "Carrot BBS" to "WithYou".
The Go module name has been changed from `carrot_bbs` to `with_you`, which requires
all import paths to be updated throughout the codebase and by external consumers.

BREAKING CHANGE: Module name changed from carrot_bbs to with_you. All imports
and dependencies must be updated from carrot_bbs/* to with_you/*.
2026-04-22 16:01:59 +08:00

703 lines
22 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 (
"with_you/internal/model"
"with_you/internal/pkg/cursor"
"context"
"errors"
"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)
TransferOwner(groupID, oldOwnerID, newOwnerID string) 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
}
// 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
})
}
// 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
}
// 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
}
// 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 errors.Is(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{}, "", "", 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{}, "", "", 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{}, "", "", 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{}, "", "", 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
}