Files
backend/internal/service/group_service.go
lan 2748c80095
All checks were successful
Build Backend / build (push) Successful in 2m20s
Build Backend / build-docker (push) Successful in 2m12s
fix(cache): enhance sequence generation atomicity and concurrency safety
Implement Lua scripts for atomic sequence incrementing and cold-start initialization in Redis to prevent race conditions. Improve the sequence buffer manager with CAS-like logic to prevent interval leakage during concurrent allocations. Update the message repository to use `SELECT ... FOR UPDATE` and conditional updates for `last_seq` to ensure database consistency during high concurrency.

- Add `nextSeqLua` and `initSeqLua` for atomic Redis operations
- Implement thread-safe buffer management in `seq_buffer.go`
- Add row-level locking in `message_repo.go`
- Update service layers to support group-aware sequence retrieval
2026-05-25 14:08:06 +08:00

1637 lines
49 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 service
import (
"cmp"
"context"
"errors"
"fmt"
"maps"
"slices"
"strconv"
"time"
"with_you/internal/cache"
apperrors "with_you/internal/errors"
"with_you/internal/model"
"with_you/internal/pkg/cursor"
"with_you/internal/pkg/utils"
"with_you/internal/pkg/ws"
"with_you/internal/repository"
"go.uber.org/zap"
"gorm.io/gorm"
)
// 缓存TTL常量
const (
GroupMembersTTL = 120 * time.Second // 群组成员缓存120秒
GroupMembersNullTTL = 5 * time.Second
GroupCacheJitter = 0.1
)
// 群组服务错误定义 - 使用统一的 AppError
var (
ErrGroupNotFound = apperrors.ErrGroupNotFound
ErrNotGroupMember = apperrors.ErrNotGroupMember
ErrNotGroupAdmin = apperrors.ErrNotGroupAdmin
ErrNotGroupOwner = apperrors.ErrNotGroupOwner
ErrGroupFull = apperrors.ErrGroupFull
ErrAlreadyMember = apperrors.ErrAlreadyGroupMember
ErrCannotRemoveOwner = apperrors.ErrCannotRemoveOwner
ErrCannotMuteOwner = apperrors.ErrCannotMuteOwner
ErrMuted = apperrors.ErrMuted
ErrMuteAllEnabled = apperrors.ErrMuteAllEnabled
ErrCannotJoin = apperrors.ErrCannotJoin
ErrJoinRequestPending = apperrors.ErrJoinRequestPending
ErrGroupRequestNotFound = apperrors.ErrGroupRequestNotFound
ErrGroupRequestHandled = apperrors.ErrGroupRequestHandled
ErrNotRequestTarget = apperrors.ErrNotRequestTarget
ErrNoEligibleInvitee = apperrors.ErrNoEligibleInvitee
ErrNotMutualFollow = apperrors.ErrNotMutualFollow
ErrInviteNotAccepted = apperrors.ErrInviteNotAccepted
)
// GroupService 群组服务接口
type GroupService interface {
// 群组管理
CreateGroup(ownerID string, name string, description string, memberIDs []string) (*model.Group, error)
GetGroupByID(id string) (*model.Group, error)
UpdateGroup(userID string, groupID string, updates map[string]any) error
DissolveGroup(userID string, groupID string) error
TransferOwner(userID string, groupID string, newOwnerID string) error
GetUserGroups(userID string, page, pageSize int) ([]model.Group, int64, error)
GetMemberCount(groupID string) (int, error)
GetMemberCountBatch(ctx context.Context, groupIDs []string) (map[string]int64, error)
// 成员管理
InviteMembers(userID string, groupID string, memberIDs []string) error
JoinGroup(userID string, groupID string) error
RespondInvite(userID string, flag string, approve bool, reason string) error
SetGroupAddRequest(userID string, flag string, approve bool, reason string) error
LeaveGroup(userID string, groupID string) error
RemoveMember(userID string, groupID string, targetUserID string) error
GetMembers(groupID string, page, pageSize int) ([]model.GroupMember, int64, error)
SetMemberRole(userID string, groupID string, targetUserID string, role string) error
SetMemberNickname(userID string, groupID string, nickname string) error
MuteMember(userID string, groupID string, targetUserID string, muted bool) error
// 群设置
SetMuteAll(userID string, groupID string, muteAll bool) error
SetJoinType(userID string, groupID string, joinType int) error
// 群公告
CreateAnnouncement(userID string, groupID string, content string) (*model.GroupAnnouncement, error)
GetAnnouncements(groupID string, page, pageSize int) ([]model.GroupAnnouncement, int64, error)
DeleteAnnouncement(userID string, announcementID string) error
// 权限检查
CanSendGroupMessage(userID string, groupID string) error
IsGroupAdmin(userID string, groupID string) bool
IsGroupOwner(userID string, groupID string) bool
// 获取成员信息
GetMember(groupID string, userID string) (*model.GroupMember, 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)
}
// GroupMembersResult 群组成员缓存结果
type GroupMembersResult struct {
Members []model.GroupMember
Total int64
}
// groupService 群组服务实现
type groupService struct {
groupRepo repository.GroupRepository
userRepo repository.UserRepository
messageRepo repository.MessageRepository
requestRepo repository.GroupJoinRequestRepository
notifyRepo repository.SystemNotificationRepository
wsHub ws.MessagePublisher
cache cache.Cache
conversationCache *cache.ConversationCache
}
// NewGroupService 创建群组服务
func NewGroupService(groupRepo repository.GroupRepository, userRepo repository.UserRepository, messageRepo repository.MessageRepository, requestRepo repository.GroupJoinRequestRepository, notifyRepo repository.SystemNotificationRepository, publisher ws.MessagePublisher, cacheBackend cache.Cache) GroupService {
convRepoAdapter := cache.NewConversationRepositoryAdapter(messageRepo)
msgRepoAdapter := cache.NewMessageRepositoryAdapter(messageRepo)
conversationCache := cache.NewConversationCache(
cacheBackend,
convRepoAdapter,
msgRepoAdapter,
cache.DefaultConversationCacheSettings(),
nil,
)
return &groupService{
groupRepo: groupRepo,
userRepo: userRepo,
messageRepo: messageRepo,
requestRepo: requestRepo,
notifyRepo: notifyRepo,
wsHub: publisher,
cache: cacheBackend,
conversationCache: conversationCache,
}
}
type groupNoticeData struct {
UserID string `json:"user_id,omitempty"`
Username string `json:"username,omitempty"`
OperatorID string `json:"operator_id,omitempty"`
}
type groupNoticeMessage struct {
NoticeType string `json:"notice_type"`
GroupID string `json:"group_id"`
Data groupNoticeData `json:"data"`
Timestamp int64 `json:"timestamp"`
MessageID string `json:"message_id,omitempty"`
Seq int64 `json:"seq,omitempty"`
}
func (s *groupService) publishGroupNotice(groupID string, notice groupNoticeMessage) {
members, _, err := s.groupRepo.GetMembers(groupID, 1, 1000)
if err != nil {
zap.L().Error("获取群成员失败",
zap.String("groupID", groupID),
zap.Error(err),
)
return
}
if s.wsHub != nil {
for _, m := range members {
s.wsHub.PublishToUser(m.UserID, "group_notice", notice)
}
}
}
// invalidateConversationCachesAfterSystemMessage 系统消息写入后失效相关缓存
func (s *groupService) invalidateConversationCachesAfterSystemMessage(conversationID string) {
if conversationID == "" || s.messageRepo == nil {
return
}
// 新系统消息会影响消息分页列表
cache.InvalidateMessagePages(s.cache, conversationID)
// 参与者列表可能发生变化(加群/退群)后,这里统一清理一次
s.cache.Delete(cache.ParticipantListKey(conversationID))
participants, err := s.messageRepo.GetConversationParticipants(conversationID)
if err != nil {
return
}
for _, p := range participants {
if p == nil || p.UserID == "" {
continue
}
// 会话最后消息、未读数会变化,清理用户维度缓存
cache.InvalidateConversationList(s.cache, p.UserID)
cache.InvalidateUnreadConversation(s.cache, p.UserID)
cache.InvalidateUnreadDetail(s.cache, p.UserID, conversationID)
}
}
// invalidateConversationCachesAfterMembershipChange 成员变更后失效相关缓存
func (s *groupService) invalidateConversationCachesAfterMembershipChange(conversationID, userID string) {
if conversationID == "" {
return
}
s.cache.Delete(cache.ParticipantListKey(conversationID))
if userID != "" {
s.cache.Delete(cache.ParticipantKey(conversationID, userID))
cache.InvalidateConversationList(s.cache, userID)
cache.InvalidateUnreadConversation(s.cache, userID)
cache.InvalidateUnreadDetail(s.cache, userID, conversationID)
}
}
// ==================== 群组管理 ====================
// CreateGroup 创建群组
func (s *groupService) CreateGroup(ownerID string, name string, description string, memberIDs []string) (*model.Group, error) {
// 创建群组ID会在BeforeCreate中自动生成
group := &model.Group{
Name: name,
Description: description,
OwnerID: ownerID,
MemberCount: 0,
MaxMembers: 500,
JoinType: model.JoinTypeAnyone,
MuteAll: false,
}
// 保存群组
if err := s.groupRepo.Create(group); err != nil {
return nil, err
}
// 添加群主为成员
ownerMember := &model.GroupMember{
GroupID: group.ID,
UserID: ownerID,
Role: model.GroupRoleOwner,
JoinTime: time.Now(),
}
if err := s.groupRepo.AddMember(ownerMember); err != nil {
// 回滚:删除群组
_ = s.groupRepo.Delete(group.ID)
return nil, err
}
// 邀请初始成员
if len(memberIDs) > 0 {
for _, memberID := range memberIDs {
if memberID == ownerID {
continue // 跳过群主
}
member := &model.GroupMember{
GroupID: group.ID,
UserID: memberID,
Role: model.GroupRoleMember,
JoinTime: time.Now(),
}
if err := s.groupRepo.AddMember(member); err != nil {
// 单个成员添加失败不回滚整个操作
continue
}
}
}
// 创建群组会话Conversation
conversationID, err := utils.GetSnowflake().GenerateID()
if err != nil {
conversationID = int64(time.Now().UnixNano())
}
conversation := &model.Conversation{
ID: strconv.FormatInt(conversationID, 10),
Type: model.ConversationTypeGroup,
GroupID: &group.ID,
}
// 收集所有参与者
participants := make([]string, 0, len(memberIDs)+1)
participants = append(participants, ownerID)
for _, memberID := range memberIDs {
if memberID != ownerID {
participants = append(participants, memberID)
}
}
// 使用仓储创建会话和参与者
if err := s.messageRepo.CreateConversationWithParticipants(conversation, participants); err != nil {
// 记录错误但不影响群组创建成功
}
return group, nil
}
// GetGroupByID 根据ID获取群组
func (s *groupService) GetGroupByID(id string) (*model.Group, error) {
group, err := s.groupRepo.GetByID(id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrGroupNotFound
}
return nil, err
}
return group, nil
}
// GetMemberCount 实时获取群成员数量
func (s *groupService) GetMemberCount(groupID string) (int, error) {
count, err := s.groupRepo.GetMemberCount(groupID)
if err != nil {
return 0, err
}
return int(count), nil
}
// GetMemberCountBatch 批量获取多个群的成员数量
func (s *groupService) GetMemberCountBatch(ctx context.Context, groupIDs []string) (map[string]int64, error) {
return s.groupRepo.GetMemberCountBatch(ctx, groupIDs)
}
// UpdateGroup 更新群组信息
func (s *groupService) UpdateGroup(userID string, groupID string, updates map[string]any) error {
// 检查群组是否存在
group, err := s.groupRepo.GetByID(groupID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrGroupNotFound
}
return err
}
// 检查权限:只有群主和管理员可以更新群信息
if !s.IsGroupAdmin(userID, groupID) {
return ErrNotGroupAdmin
}
// 不允许直接修改的字段
delete(updates, "id")
delete(updates, "owner_id")
delete(updates, "member_count")
delete(updates, "created_at")
// 应用更新
if name, ok := updates["name"].(string); ok {
group.Name = name
}
if description, ok := updates["description"].(string); ok {
group.Description = description
}
if avatar, ok := updates["avatar"].(string); ok {
group.Avatar = avatar
}
return s.groupRepo.Update(group)
}
// DissolveGroup 解散群组
func (s *groupService) DissolveGroup(userID string, groupID string) error {
// 检查群组是否存在
group, err := s.groupRepo.GetByID(groupID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrGroupNotFound
}
return err
}
// 检查权限:只有群主可以解散群
if group.OwnerID != userID {
return ErrNotGroupOwner
}
// 先删除群组对应的会话(包括参与者、消息)
if s.messageRepo != nil {
if err := s.messageRepo.DeleteConversationByGroupID(groupID); err != nil {
zap.L().Warn("删除会话失败",
zap.String("groupID", groupID),
zap.Error(err),
)
// 继续删除群组,不因为会话删除失败而中断
}
}
return s.groupRepo.Delete(groupID)
}
// TransferOwner 转让群主
func (s *groupService) TransferOwner(userID string, groupID string, newOwnerID string) error {
// 检查群组是否存在
group, err := s.groupRepo.GetByID(groupID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrGroupNotFound
}
return err
}
// 检查权限:只有群主可以转让群主
if group.OwnerID != userID {
return ErrNotGroupOwner
}
// 检查新群主是否是群成员
isMember, err := s.groupRepo.IsMember(groupID, newOwnerID)
if err != nil {
return err
}
if !isMember {
return ErrNotGroupMember
}
// 使用仓储的 TransferOwner 方法(在事务中更新)
return s.groupRepo.TransferOwner(groupID, userID, newOwnerID)
}
// GetUserGroups 获取用户加入的群组列表
func (s *groupService) GetUserGroups(userID string, page, pageSize int) ([]model.Group, int64, error) {
return s.groupRepo.GetUserGroups(userID, page, pageSize)
}
// ==================== 成员管理 ====================
func (s *groupService) newRequestFlag() string {
id, err := utils.GetSnowflake().GenerateID()
if err != nil {
return strconv.FormatInt(time.Now().UnixNano(), 10)
}
return strconv.FormatInt(id, 10)
}
func (s *groupService) createSystemNotification(receiverID string, notifyType model.SystemNotificationType, content string, extra *model.SystemNotificationExtra) {
if s.notifyRepo == nil {
return
}
notification := &model.SystemNotification{
ReceiverID: receiverID,
Type: notifyType,
Content: content,
ExtraData: extra,
}
if err := s.notifyRepo.Create(notification); err != nil {
zap.L().Warn("create system notification failed",
zap.String("component", "groupService"),
zap.String("receiverID", receiverID),
zap.String("type", string(notifyType)),
zap.Error(err),
)
}
}
func (s *groupService) broadcastMemberJoinNotice(groupID string, targetUserID string, operatorID string) {
if groupID == "" || targetUserID == "" {
return
}
targetUserName := "用户"
if targetUser, err := s.userRepo.GetByID(targetUserID); err == nil && targetUser != nil && targetUser.Nickname != "" {
targetUserName = targetUser.Nickname
}
noticeContent := "\"" + targetUserName + "\" 加入了群聊"
var savedMessage *model.Message
if s.messageRepo != nil {
conv, err := s.messageRepo.GetConversationByGroupID(groupID)
if err == nil && conv != nil {
msg := &model.Message{
ConversationID: conv.ID,
SenderID: model.SystemSenderIDStr,
Segments: model.MessageSegments{
{Type: "text", Data: map[string]any{"text": noticeContent}},
},
Status: model.MessageStatusNormal,
Category: model.CategoryNotification,
}
// 从 Redis 获取下一个 seq
if s.conversationCache != nil {
seq, seqErr := s.conversationCache.GetNextSeqWithGroup(context.Background(), conv.ID, true)
if seqErr != nil {
zap.L().Warn("get next seq failed, skipping system message",
zap.String("convID", conv.ID),
zap.Error(seqErr),
)
return
}
msg.Seq = seq
}
if err := s.messageRepo.CreateMessageWithSeq(msg); err != nil {
zap.L().Warn("保存入群提示消息失败",
zap.String("component", "broadcastMemberJoinNotice"),
zap.String("groupID", groupID),
zap.String("userID", targetUserID),
zap.Error(err),
)
} else {
savedMessage = msg
s.invalidateConversationCachesAfterSystemMessage(conv.ID)
}
} else {
zap.L().Warn("获取群组会话失败",
zap.String("component", "broadcastMemberJoinNotice"),
zap.String("groupID", groupID),
zap.Error(err),
)
}
}
noticeMsg := groupNoticeMessage{
NoticeType: "member_join",
GroupID: groupID,
Data: groupNoticeData{
UserID: targetUserID,
Username: targetUserName,
OperatorID: operatorID,
},
Timestamp: time.Now().UnixMilli(),
}
if savedMessage != nil {
noticeMsg.MessageID = savedMessage.ID
noticeMsg.Seq = savedMessage.Seq
}
s.publishGroupNotice(groupID, noticeMsg)
}
func (s *groupService) addMemberToGroupAndConversation(group *model.Group, userID string, operatorID string) error {
if group == nil {
return ErrGroupNotFound
}
isMember, err := s.groupRepo.IsMember(group.ID, userID)
if err != nil {
return err
}
if isMember {
return nil
}
memberCount, err := s.groupRepo.GetMemberCount(group.ID)
if err != nil {
return err
}
if int(memberCount) >= group.MaxMembers {
return ErrGroupFull
}
member := &model.GroupMember{
GroupID: group.ID,
UserID: userID,
Role: model.GroupRoleMember,
JoinTime: time.Now(),
}
if err := s.groupRepo.AddMember(member); err != nil {
return err
}
if s.messageRepo != nil {
conv, err := s.messageRepo.GetConversationByGroupID(group.ID)
if err == nil && conv != nil {
if err := s.messageRepo.AddParticipant(conv.ID, userID); err != nil {
zap.L().Warn("添加会话参与者失败",
zap.String("component", "addMemberToGroupAndConversation"),
zap.String("groupID", group.ID),
zap.String("userID", userID),
zap.Error(err),
)
}
s.invalidateConversationCachesAfterMembershipChange(conv.ID, userID)
}
}
cache.InvalidateGroupMembers(s.cache, group.ID)
s.broadcastMemberJoinNotice(group.ID, userID, operatorID)
return nil
}
func (s *groupService) collectGroupReviewerIDs(groupID string, ownerID string, skipUserID string) []string {
reviewerSet := make(map[string]struct{})
if ownerID != "" && ownerID != skipUserID {
reviewerSet[ownerID] = struct{}{}
}
if reviewers, err := s.groupRepo.GetAdmins(groupID); err == nil {
for _, reviewer := range reviewers {
if reviewer.UserID == "" || reviewer.UserID == skipUserID {
continue
}
reviewerSet[reviewer.UserID] = struct{}{}
}
}
return slices.Collect(maps.Keys(reviewerSet))
}
func (s *groupService) notifyJoinApplyReviewers(
group *model.Group,
req *model.GroupJoinRequest,
applicantName string,
applicantAvatar string,
targetUserID string,
targetUserName string,
targetUserAvatar string,
) {
if group == nil || req == nil {
return
}
reviewerIDs := s.collectGroupReviewerIDs(group.ID, group.OwnerID, req.InitiatorID)
for _, reviewerID := range reviewerIDs {
s.createSystemNotification(reviewerID, model.SysNotifyGroupJoinApply, applicantName+" 申请加入群聊 "+group.Name, &model.SystemNotificationExtra{
ActorIDStr: req.InitiatorID,
ActorName: applicantName,
AvatarURL: applicantAvatar,
TargetID: req.Flag,
TargetTitle: group.Name,
TargetType: string(model.GroupJoinRequestTypeJoinApply),
GroupID: group.ID,
GroupName: group.Name,
GroupAvatar: group.Avatar,
GroupDescription: group.Description,
Flag: req.Flag,
RequestType: string(req.RequestType),
RequestStatus: string(req.Status),
TargetUserID: targetUserID,
TargetUserName: targetUserName,
TargetUserAvatar: targetUserAvatar,
})
}
}
func (s *groupService) sendGroupInviteToTarget(group *model.Group, req *model.GroupJoinRequest, inviterName, inviterAvatar string) {
if group == nil || req == nil {
return
}
s.createSystemNotification(req.TargetUserID, model.SysNotifyGroupInvite, inviterName+" 邀请你加入群聊 "+group.Name, &model.SystemNotificationExtra{
ActorIDStr: req.InitiatorID,
ActorName: inviterName,
AvatarURL: inviterAvatar,
TargetID: req.Flag,
TargetTitle: group.Name,
TargetType: string(model.GroupJoinRequestTypeInvite),
GroupID: group.ID,
GroupName: group.Name,
GroupAvatar: group.Avatar,
GroupDescription: group.Description,
Flag: req.Flag,
RequestType: string(req.RequestType),
RequestStatus: string(req.Status),
TargetUserID: req.TargetUserID,
TargetUserName: "",
TargetUserAvatar: "",
})
}
// InviteMembers 邀请成员
func (s *groupService) InviteMembers(userID string, groupID string, memberIDs []string) error {
group, err := s.groupRepo.GetByID(groupID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrGroupNotFound
}
return err
}
isInviterMember, err := s.groupRepo.IsMember(groupID, userID)
if err != nil {
return err
}
if !isInviterMember {
return ErrNotGroupMember
}
inviter, _ := s.userRepo.GetByID(userID)
inviterName := "群成员"
inviterAvatar := ""
if inviter != nil {
inviterName = cmp.Or(inviter.Nickname, "群成员")
inviterAvatar = inviter.Avatar
}
createdCount := 0
for _, memberID := range memberIDs {
if memberID == "" || memberID == userID {
continue
}
isMember, err := s.groupRepo.IsMember(groupID, memberID)
if err != nil {
continue
}
if isMember {
continue
}
isFollowing, err := s.userRepo.IsFollowing(userID, memberID)
if err != nil || !isFollowing {
continue
}
isFollowedBack, err := s.userRepo.IsFollowing(memberID, userID)
if err != nil || !isFollowedBack {
continue
}
if _, err := s.requestRepo.GetPendingByGroupAndTarget(groupID, memberID, model.GroupJoinRequestTypeInvite); err == nil {
continue
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
continue
}
expireAt := time.Now().Add(72 * time.Hour)
req := &model.GroupJoinRequest{
Flag: s.newRequestFlag(),
GroupID: groupID,
InitiatorID: userID,
TargetUserID: memberID,
RequestType: model.GroupJoinRequestTypeInvite,
Status: model.GroupJoinRequestStatusPending,
ExpireAt: &expireAt,
}
if err := s.requestRepo.Create(req); err != nil {
continue
}
// 无论是否需要审批,都先发送邀请卡片给被邀请人
// 被邀请人同意后,再根据群的策略决定是否需要管理员审批
s.sendGroupInviteToTarget(group, req, inviterName, inviterAvatar)
createdCount++
}
if createdCount == 0 {
return ErrNoEligibleInvitee
}
return nil
}
// JoinGroup 加入群组
func (s *groupService) JoinGroup(userID string, groupID string) error {
group, err := s.groupRepo.GetByID(groupID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrGroupNotFound
}
return err
}
// 检查是否已经是群成员
isMember, err := s.groupRepo.IsMember(groupID, userID)
if err != nil {
return err
}
if isMember {
return ErrAlreadyMember
}
if group.JoinType == model.JoinTypeForbidden {
return ErrCannotJoin
}
if group.JoinType == model.JoinTypeApproval {
if pendingReq, err := s.requestRepo.GetPendingByGroupAndTarget(groupID, userID, model.GroupJoinRequestTypeJoinApply); err == nil {
applicant, _ := s.userRepo.GetByID(userID)
applicantName := "用户"
applicantAvatar := ""
if applicant != nil {
applicantName = cmp.Or(applicant.Nickname, "用户")
applicantAvatar = applicant.Avatar
}
// 已有待审批单时补发一次提醒,避免管理端漏看
s.notifyJoinApplyReviewers(
group,
pendingReq,
applicantName,
applicantAvatar,
userID,
applicantName,
applicantAvatar,
)
return ErrJoinRequestPending
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
applicant, _ := s.userRepo.GetByID(userID)
applicantName := "用户"
applicantAvatar := ""
if applicant != nil {
applicantName = cmp.Or(applicant.Nickname, "用户")
applicantAvatar = applicant.Avatar
}
expireAt := time.Now().Add(72 * time.Hour)
req := &model.GroupJoinRequest{
Flag: s.newRequestFlag(),
GroupID: groupID,
InitiatorID: userID,
TargetUserID: userID,
RequestType: model.GroupJoinRequestTypeJoinApply,
Status: model.GroupJoinRequestStatusPending,
ExpireAt: &expireAt,
}
if err := s.requestRepo.Create(req); err != nil {
return err
}
s.notifyJoinApplyReviewers(
group,
req,
applicantName,
applicantAvatar,
userID,
applicantName,
applicantAvatar,
)
return ErrJoinRequestPending
}
return s.addMemberToGroupAndConversation(group, userID, userID)
}
func (s *groupService) RespondInvite(userID string, flag string, approve bool, reason string) error {
req, err := s.requestRepo.GetByFlag(flag)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrGroupRequestNotFound
}
return err
}
if req.RequestType != model.GroupJoinRequestTypeInvite {
return ErrGroupRequestNotFound
}
if req.Status != model.GroupJoinRequestStatusPending {
return ErrGroupRequestHandled
}
if req.TargetUserID != userID {
return ErrNotRequestTarget
}
group, err := s.groupRepo.GetByID(req.GroupID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrGroupNotFound
}
return err
}
now := time.Now()
req.Reason = reason
if approve {
// 检查群的加群策略
if group.JoinType == model.JoinTypeAnyone {
// 允许任何人加入,直接进群
if err := s.addMemberToGroupAndConversation(group, userID, userID); err != nil {
return err
}
req.Status = model.GroupJoinRequestStatusAccepted
s.createSystemNotification(req.InitiatorID, model.SysNotifySystem, "你发出的群邀请已被接受", &model.SystemNotificationExtra{
GroupID: group.ID,
GroupName: group.Name,
Flag: req.Flag,
RequestType: string(req.RequestType),
RequestStatus: string(model.GroupJoinRequestStatusAccepted),
})
} else {
// 需要审批或不允许加入时,被邀请人同意后通知管理员审批
// 设置 ReviewerID 为被邀请人表示被邀请人已同意
req.ReviewerID = userID
req.ReviewedAt = &now
// 获取被邀请人信息
targetUserName := "用户"
targetUserAvatar := ""
if u, e := s.userRepo.GetByID(userID); e == nil && u != nil {
targetUserName = cmp.Or(u.Nickname, "用户")
targetUserAvatar = u.Avatar
}
// 获取邀请人信息
inviterName := "群成员"
inviterAvatar := ""
if u, e := s.userRepo.GetByID(req.InitiatorID); e == nil && u != nil {
inviterName = cmp.Or(u.Nickname, "群成员")
inviterAvatar = u.Avatar
}
// 通知管理员审批
reviewerIDs := s.collectGroupReviewerIDs(group.ID, group.OwnerID, req.InitiatorID)
for _, reviewerID := range reviewerIDs {
s.createSystemNotification(reviewerID, model.SysNotifyGroupJoinApply, inviterName+" 邀请 "+targetUserName+" 加入群聊 "+group.Name+",被邀请人已同意,请审批", &model.SystemNotificationExtra{
ActorIDStr: req.InitiatorID,
ActorName: inviterName,
AvatarURL: inviterAvatar,
TargetID: req.Flag,
TargetTitle: group.Name,
TargetType: string(model.GroupJoinRequestTypeInvite),
GroupID: group.ID,
GroupName: group.Name,
GroupAvatar: group.Avatar,
GroupDescription: group.Description,
Flag: req.Flag,
RequestType: string(req.RequestType),
RequestStatus: string(req.Status),
TargetUserID: userID,
TargetUserName: targetUserName,
TargetUserAvatar: targetUserAvatar,
})
}
// 通知邀请人:被邀请人已同意,等待管理员审批
s.createSystemNotification(req.InitiatorID, model.SysNotifySystem, targetUserName+" 已接受你的群邀请,等待管理员审批", &model.SystemNotificationExtra{
GroupID: group.ID,
GroupName: group.Name,
Flag: req.Flag,
RequestType: string(req.RequestType),
RequestStatus: string(req.Status),
})
}
} else {
req.Status = model.GroupJoinRequestStatusRejected
s.createSystemNotification(req.InitiatorID, model.SysNotifySystem, "你发出的群邀请已被拒绝", &model.SystemNotificationExtra{
GroupID: group.ID,
GroupName: group.Name,
Flag: req.Flag,
RequestType: string(req.RequestType),
RequestStatus: string(model.GroupJoinRequestStatusRejected),
Reason: reason,
})
}
return s.requestRepo.Update(req)
}
func (s *groupService) SetGroupAddRequest(userID string, flag string, approve bool, reason string) error {
req, err := s.requestRepo.GetByFlag(flag)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrGroupRequestNotFound
}
return err
}
if req.RequestType != model.GroupJoinRequestTypeJoinApply && req.RequestType != model.GroupJoinRequestTypeInvite {
return ErrGroupRequestNotFound
}
if req.Status != model.GroupJoinRequestStatusPending {
return ErrGroupRequestHandled
}
// 检查 invite 类型的请求:
// - ReviewerID 非空表示被邀请人已同意,等待管理员审批
// - ReviewerID 为空表示被邀请人还未同意,管理员不能提前审批
if req.RequestType == model.GroupJoinRequestTypeInvite {
if req.ReviewerID == "" {
// 被邀请人还未同意,管理员不能提前审批
return ErrInviteNotAccepted
}
if req.ReviewerID != req.TargetUserID {
// ReviewerID 不是被邀请人,说明已经被其他人处理过
return ErrGroupRequestHandled
}
}
if !s.IsGroupAdmin(userID, req.GroupID) {
return ErrNotGroupAdmin
}
group, err := s.groupRepo.GetByID(req.GroupID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrGroupNotFound
}
return err
}
now := time.Now()
// 保存管理员ID到ReviewerID覆盖被邀请人的ID
req.Reason = reason
targetUserName := "用户"
if u, e := s.userRepo.GetByID(req.TargetUserID); e == nil && u != nil {
targetUserName = cmp.Or(u.Nickname, "用户")
}
reviewerName := "管理员"
reviewerAvatar := ""
if u, e := s.userRepo.GetByID(userID); e == nil && u != nil {
reviewerName = cmp.Or(u.Nickname, "管理员")
reviewerAvatar = u.Avatar
}
if approve {
if req.RequestType == model.GroupJoinRequestTypeInvite {
// 被邀请人已同意,管理员审批通过后直接进群
if err := s.addMemberToGroupAndConversation(group, req.TargetUserID, userID); err != nil {
return err
}
req.Status = model.GroupJoinRequestStatusAccepted
req.ReviewerID = userID
req.ReviewedAt = &now
s.createSystemNotification(req.TargetUserID, model.SysNotifyGroupJoinApproved, "你接受的群邀请已通过审批,你已加入群聊 "+group.Name, &model.SystemNotificationExtra{
GroupID: group.ID,
GroupName: group.Name,
GroupAvatar: group.Avatar,
Flag: req.Flag,
RequestType: string(req.RequestType),
RequestStatus: string(model.GroupJoinRequestStatusAccepted),
})
s.createSystemNotification(req.InitiatorID, model.SysNotifySystem, "你邀请 "+targetUserName+" 入群已通过审批", &model.SystemNotificationExtra{
GroupID: group.ID,
GroupName: group.Name,
GroupAvatar: group.Avatar,
Flag: req.Flag,
RequestType: string(req.RequestType),
RequestStatus: string(model.GroupJoinRequestStatusAccepted),
})
} else {
if err := s.addMemberToGroupAndConversation(group, req.TargetUserID, userID); err != nil {
return err
}
req.Status = model.GroupJoinRequestStatusAccepted
req.ReviewerID = userID
req.ReviewedAt = &now
s.createSystemNotification(req.TargetUserID, model.SysNotifyGroupJoinApproved, "你申请加入群聊 "+group.Name+" 已通过", &model.SystemNotificationExtra{
GroupID: group.ID,
GroupName: group.Name,
GroupAvatar: group.Avatar,
Flag: req.Flag,
RequestType: string(req.RequestType),
RequestStatus: string(model.GroupJoinRequestStatusAccepted),
})
}
// 同步通知其他可审批人:该请求已被处理
reviewerIDs := s.collectGroupReviewerIDs(group.ID, group.OwnerID, "")
for _, reviewerID := range reviewerIDs {
if reviewerID == userID {
continue
}
s.createSystemNotification(reviewerID, model.SysNotifyGroupJoinApply, reviewerName+" 已同意该入群请求", &model.SystemNotificationExtra{
ActorIDStr: userID,
ActorName: reviewerName,
AvatarURL: reviewerAvatar,
GroupID: group.ID,
GroupName: group.Name,
GroupAvatar: group.Avatar,
GroupDescription: group.Description,
Flag: req.Flag,
RequestType: string(req.RequestType),
RequestStatus: string(model.GroupJoinRequestStatusAccepted),
TargetUserID: req.TargetUserID,
TargetUserName: targetUserName,
})
}
} else {
req.Status = model.GroupJoinRequestStatusRejected
req.ReviewerID = userID
req.ReviewedAt = &now
if req.RequestType == model.GroupJoinRequestTypeInvite {
// 被邀请人已同意但管理员拒绝,通知被邀请人和邀请人
s.createSystemNotification(req.TargetUserID, model.SysNotifyGroupJoinRejected, "你接受的群邀请未通过审批", &model.SystemNotificationExtra{
GroupID: group.ID,
GroupName: group.Name,
GroupAvatar: group.Avatar,
Flag: req.Flag,
RequestType: string(req.RequestType),
RequestStatus: string(model.GroupJoinRequestStatusRejected),
Reason: reason,
})
s.createSystemNotification(req.InitiatorID, model.SysNotifySystem, "你邀请 "+targetUserName+" 入群未通过审批", &model.SystemNotificationExtra{
GroupID: group.ID,
GroupName: group.Name,
GroupAvatar: group.Avatar,
Flag: req.Flag,
RequestType: string(req.RequestType),
RequestStatus: string(model.GroupJoinRequestStatusRejected),
Reason: reason,
})
} else {
s.createSystemNotification(req.TargetUserID, model.SysNotifyGroupJoinRejected, "你申请加入群聊 "+group.Name+" 被拒绝", &model.SystemNotificationExtra{
GroupID: group.ID,
GroupName: group.Name,
GroupAvatar: group.Avatar,
Flag: req.Flag,
RequestType: string(req.RequestType),
RequestStatus: string(model.GroupJoinRequestStatusRejected),
Reason: reason,
})
}
// 同步通知其他可审批人:该请求已被处理
reviewerIDs := s.collectGroupReviewerIDs(group.ID, group.OwnerID, "")
for _, reviewerID := range reviewerIDs {
if reviewerID == userID {
continue
}
s.createSystemNotification(reviewerID, model.SysNotifyGroupJoinApply, reviewerName+" 已拒绝该入群请求", &model.SystemNotificationExtra{
ActorIDStr: userID,
ActorName: reviewerName,
AvatarURL: reviewerAvatar,
GroupID: group.ID,
GroupName: group.Name,
GroupAvatar: group.Avatar,
GroupDescription: group.Description,
Flag: req.Flag,
RequestType: string(req.RequestType),
RequestStatus: string(model.GroupJoinRequestStatusRejected),
Reason: reason,
TargetUserID: req.TargetUserID,
TargetUserName: targetUserName,
})
}
}
return s.requestRepo.Update(req)
}
// LeaveGroup 退出群组
func (s *groupService) LeaveGroup(userID string, groupID string) error {
// 检查群组是否存在
group, err := s.groupRepo.GetByID(groupID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrGroupNotFound
}
return err
}
// 群主不能退出群
if group.OwnerID == userID {
return ErrCannotRemoveOwner
}
// 移除群成员
if err := s.groupRepo.RemoveMember(groupID, userID); err != nil {
return err
}
// 移除会话参与者记录
// 根据群组ID查找对应的会话
conv, err := s.messageRepo.GetConversationByGroupID(groupID)
if err != nil {
// 如果找不到会话,记录日志但不阻塞退出群流程
fmt.Printf("[WARN] LeaveGroup: conversation not found for group %s, error: %v\n", groupID, err)
} else {
// 移除该用户在会话中的参与者记录
if err := s.messageRepo.RemoveParticipant(conv.ID, userID); err != nil {
// 如果移除参与者失败,记录日志但不阻塞退出群流程
fmt.Printf("[WARN] LeaveGroup: failed to remove participant %s from conversation %s, error: %v\n", userID, conv.ID, err)
}
s.invalidateConversationCachesAfterMembershipChange(conv.ID, userID)
}
// 失效群组成员缓存
cache.InvalidateGroupMembers(s.cache, groupID)
return nil
}
// RemoveMember 移除成员
func (s *groupService) RemoveMember(userID string, groupID string, targetUserID string) error {
// 检查群组是否存在
group, err := s.groupRepo.GetByID(groupID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrGroupNotFound
}
return err
}
// 不能移除群主
if targetUserID == group.OwnerID {
return ErrCannotRemoveOwner
}
// 检查权限
targetRole, err := s.groupRepo.GetMemberRole(groupID, targetUserID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrNotGroupMember
}
return err
}
// 群主可以移除任何人
// 管理员只能移除普通成员
if group.OwnerID != userID {
if targetRole == model.GroupRoleOwner {
return ErrNotGroupOwner
}
if targetRole == model.GroupRoleAdmin && !s.IsGroupOwner(userID, groupID) {
return ErrNotGroupAdmin
}
}
// 移除群成员
if err := s.groupRepo.RemoveMember(groupID, targetUserID); err != nil {
return err
}
// 同时移除会话参与者
if s.messageRepo != nil {
conv, err := s.messageRepo.GetConversationByGroupID(groupID)
if err == nil && conv != nil {
if err := s.messageRepo.RemoveParticipant(conv.ID, targetUserID); err != nil {
zap.L().Warn("移除会话参与者失败",
zap.String("component", "RemoveMember"),
zap.String("groupID", groupID),
zap.String("userID", targetUserID),
zap.Error(err),
)
}
s.invalidateConversationCachesAfterMembershipChange(conv.ID, targetUserID)
}
}
// 失效群组成员缓存
cache.InvalidateGroupMembers(s.cache, groupID)
return nil
}
// GetMembers 获取群成员列表(带缓存)
func (s *groupService) GetMembers(groupID string, page, pageSize int) ([]model.GroupMember, int64, error) {
cacheSettings := cache.GetSettings()
groupMembersTTL := cacheSettings.GroupMembersTTL
if groupMembersTTL <= 0 {
groupMembersTTL = GroupMembersTTL
}
nullTTL := cacheSettings.NullTTL
if nullTTL <= 0 {
nullTTL = GroupMembersNullTTL
}
jitter := cacheSettings.JitterRatio
if jitter <= 0 {
jitter = GroupCacheJitter
}
// 生成缓存键
cacheKey := cache.GroupMembersKey(groupID, page, pageSize)
result, err := cache.GetOrLoadTyped[*GroupMembersResult](
s.cache,
cacheKey,
groupMembersTTL,
jitter,
nullTTL,
func() (*GroupMembersResult, error) {
members, total, err := s.groupRepo.GetMembers(groupID, page, pageSize)
if err != nil {
return nil, err
}
return &GroupMembersResult{
Members: members,
Total: total,
}, nil
},
)
if err != nil {
return nil, 0, err
}
if result == nil {
return []model.GroupMember{}, 0, nil
}
return result.Members, result.Total, nil
}
// SetMemberRole 设置成员角色
func (s *groupService) SetMemberRole(userID string, groupID string, targetUserID string, role string) error {
// 检查群组是否存在
group, err := s.groupRepo.GetByID(groupID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrGroupNotFound
}
return err
}
// 检查权限:只有群主可以设置角色
if group.OwnerID != userID {
return ErrNotGroupOwner
}
// 不能修改群主的角色
if targetUserID == group.OwnerID {
return ErrCannotRemoveOwner
}
// 验证角色值
if role != model.GroupRoleAdmin && role != model.GroupRoleMember {
return errors.New("无效的角色")
}
err = s.groupRepo.SetMemberRole(groupID, targetUserID, role)
if err != nil {
return err
}
// 失效群组成员缓存
cache.InvalidateGroupMembers(s.cache, groupID)
return nil
}
// SetMemberNickname 设置群内昵称
func (s *groupService) SetMemberNickname(userID string, groupID string, nickname string) error {
// 获取成员信息
member, err := s.groupRepo.GetMember(groupID, userID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrNotGroupMember
}
return err
}
member.Nickname = nickname
err = s.groupRepo.UpdateMember(member)
if err != nil {
return err
}
// 失效群组成员缓存
cache.InvalidateGroupMembers(s.cache, groupID)
return nil
}
// MuteMember 禁言成员
func (s *groupService) MuteMember(userID string, groupID string, targetUserID string, muted bool) error {
// 检查群组是否存在
group, err := s.groupRepo.GetByID(groupID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrGroupNotFound
}
return err
}
// 不能禁言群主
if targetUserID == group.OwnerID {
return ErrCannotMuteOwner
}
// 检查权限:群主可以禁言任何人,管理员只能禁言普通成员
if group.OwnerID != userID {
targetRole, err := s.groupRepo.GetMemberRole(groupID, targetUserID)
if err != nil {
return err
}
if targetRole == model.GroupRoleAdmin {
return ErrNotGroupAdmin
}
}
member, err := s.groupRepo.GetMember(groupID, targetUserID)
if err != nil {
zap.L().Warn("获取成员失败",
zap.String("component", "MuteMember"),
zap.Error(err),
)
return err
}
zap.L().Debug("禁言前状态",
zap.String("component", "MuteMember"),
zap.Bool("member.Muted", member.Muted),
zap.Bool("muted", muted),
)
member.Muted = muted
if err := s.groupRepo.UpdateMember(member); err != nil {
zap.L().Warn("更新成员失败",
zap.String("component", "MuteMember"),
zap.Error(err),
)
return err
}
zap.L().Debug("禁言状态已更新到数据库", zap.String("component", "MuteMember"))
// 验证更新结果
updatedMember, _ := s.groupRepo.GetMember(groupID, targetUserID)
if updatedMember != nil {
zap.L().Debug("验证成员状态",
zap.String("component", "MuteMember"),
zap.Bool("member.Muted", updatedMember.Muted),
)
}
// 获取被禁言用户的显示名称
targetUser, _ := s.userRepo.GetByID(targetUserID)
targetUserName := "用户"
if targetUser != nil {
targetUserName = targetUser.Nickname
}
// 构建通知内容
noticeType := "muted"
noticeContent := "\"" + targetUserName + "\" 已被管理员禁言"
if !muted {
noticeType = "unmuted"
noticeContent = "\"" + targetUserName + "\" 已被管理员解除禁言"
}
// 保存禁言/解禁消息到数据库
var savedMessage *model.Message
if s.messageRepo != nil {
// 获取群组会话
conv, err := s.messageRepo.GetConversationByGroupID(groupID)
if err == nil && conv != nil {
// 创建系统消息
msg := &model.Message{
ConversationID: conv.ID,
SenderID: model.SystemSenderIDStr,
Segments: model.MessageSegments{
{Type: "text", Data: map[string]any{"text": noticeContent}},
},
Status: model.MessageStatusNormal,
Category: model.CategoryNotification,
}
// 从 Redis 获取下一个 seq
if s.conversationCache != nil {
seq, seqErr := s.conversationCache.GetNextSeqWithGroup(context.Background(), conv.ID, true)
if seqErr != nil {
zap.L().Warn("get next seq failed, skipping system message",
zap.String("convID", conv.ID),
zap.Error(seqErr),
)
return nil
}
msg.Seq = seq
}
if err := s.messageRepo.CreateMessageWithSeq(msg); err != nil {
zap.L().Warn("保存禁言消息失败",
zap.String("component", "MuteMember"),
zap.Error(err),
)
} else {
savedMessage = msg
zap.L().Debug("禁言消息已保存",
zap.String("component", "MuteMember"),
zap.String("msgID", msg.ID),
zap.Int64("seq", msg.Seq),
)
s.invalidateConversationCachesAfterSystemMessage(conv.ID)
}
} else {
zap.L().Warn("获取群组会话失败",
zap.String("component", "MuteMember"),
zap.Error(err),
)
}
}
noticeMsg := groupNoticeMessage{
NoticeType: noticeType,
GroupID: groupID,
Data: groupNoticeData{
UserID: targetUserID,
OperatorID: userID,
},
Timestamp: time.Now().UnixMilli(),
}
if savedMessage != nil {
noticeMsg.MessageID = savedMessage.ID
noticeMsg.Seq = savedMessage.Seq
}
s.publishGroupNotice(groupID, noticeMsg)
// 失效群组成员缓存
cache.InvalidateGroupMembers(s.cache, groupID)
return nil
}
// ==================== 群设置 ====================
// SetMuteAll 全员禁言
func (s *groupService) SetMuteAll(userID string, groupID string, muteAll bool) error {
// 检查群组是否存在
group, err := s.groupRepo.GetByID(groupID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrGroupNotFound
}
return err
}
// 检查权限:只有群主可以设置全员禁言
if group.OwnerID != userID {
return ErrNotGroupOwner
}
group.MuteAll = muteAll
return s.groupRepo.Update(group)
}
// SetJoinType 设置加群方式
func (s *groupService) SetJoinType(userID string, groupID string, joinType int) error {
// 检查群组是否存在
group, err := s.groupRepo.GetByID(groupID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrGroupNotFound
}
return err
}
// 检查权限:只有群主可以设置加群方式
if group.OwnerID != userID {
return ErrNotGroupOwner
}
// 验证加群方式
if joinType < 0 || joinType > 2 {
return errors.New("无效的加群方式")
}
group.JoinType = model.JoinType(joinType)
return s.groupRepo.Update(group)
}
// ==================== 群公告 ====================
// CreateAnnouncement 创建群公告
func (s *groupService) CreateAnnouncement(userID string, groupID string, content string) (*model.GroupAnnouncement, error) {
// 检查群组是否存在
_, err := s.groupRepo.GetByID(groupID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrGroupNotFound
}
return nil, err
}
// 检查权限:只有群主和管理员可以发布公告
if !s.IsGroupAdmin(userID, groupID) {
return nil, ErrNotGroupAdmin
}
announcement := &model.GroupAnnouncement{
GroupID: groupID,
Content: content,
AuthorID: userID,
}
if err := s.groupRepo.CreateAnnouncement(announcement); err != nil {
return nil, err
}
return announcement, nil
}
// GetAnnouncements 获取群公告列表
func (s *groupService) GetAnnouncements(groupID string, page, pageSize int) ([]model.GroupAnnouncement, int64, error) {
return s.groupRepo.GetAnnouncements(groupID, page, pageSize)
}
// DeleteAnnouncement 删除群公告
func (s *groupService) DeleteAnnouncement(userID string, announcementID string) error {
// 获取公告
announcement, err := s.groupRepo.GetAnnouncementByID(announcementID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New("公告不存在")
}
return err
}
// 检查群组是否存在
group, err := s.groupRepo.GetByID(announcement.GroupID)
if err != nil {
return err
}
// 检查权限:只有群主和管理员可以删除公告
if !s.IsGroupAdmin(userID, group.ID) {
return ErrNotGroupAdmin
}
return s.groupRepo.DeleteAnnouncement(announcementID)
}
// ==================== 权限检查 ====================
// CanSendGroupMessage 检查是否可以发送群消息
func (s *groupService) CanSendGroupMessage(userID string, groupID string) error {
// 检查是否是群成员
member, err := s.groupRepo.GetMember(groupID, userID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrNotGroupMember
}
return err
}
// 检查是否被禁言
if member.Muted {
return ErrMuted
}
// 检查是否全员禁言
group, err := s.groupRepo.GetByID(groupID)
if err != nil {
return err
}
if group.MuteAll {
return ErrMuteAllEnabled
}
return nil
}
// IsGroupAdmin 检查是否是群管理员
func (s *groupService) IsGroupAdmin(userID string, groupID string) bool {
role, err := s.groupRepo.GetMemberRole(groupID, userID)
if err != nil {
return false
}
return role == model.GroupRoleOwner || role == model.GroupRoleAdmin
}
// IsGroupOwner 检查是否是群主
func (s *groupService) IsGroupOwner(userID string, groupID string) bool {
group, err := s.groupRepo.GetByID(groupID)
if err != nil {
return false
}
return group.OwnerID == userID
}
// GetMember 获取指定用户在群组中的成员信息
func (s *groupService) GetMember(groupID string, userID string) (*model.GroupMember, error) {
return s.groupRepo.GetMember(groupID, userID)
}
// ==================== 游标分页方法 ====================
// GetGroupsByCursor 游标分页获取群组列表
func (s *groupService) GetGroupsByCursor(ctx context.Context, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Group], error) {
return s.groupRepo.GetGroupsByCursor(ctx, req)
}
// GetUserGroupsByCursor 游标分页获取用户加入的群组列表
func (s *groupService) GetUserGroupsByCursor(ctx context.Context, userID string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Group], error) {
return s.groupRepo.GetUserGroupsByCursor(ctx, userID, req)
}
// GetMembersByCursor 游标分页获取群成员列表
func (s *groupService) GetMembersByCursor(ctx context.Context, groupID string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.GroupMember], error) {
return s.groupRepo.GetMembersByCursor(ctx, groupID, req)
}
// GetAnnouncementsByCursor 游标分页获取群公告列表
func (s *groupService) GetAnnouncementsByCursor(ctx context.Context, groupID string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.GroupAnnouncement], error) {
return s.groupRepo.GetAnnouncementsByCursor(ctx, groupID, req)
}