package service import ( "errors" "fmt" "log" "strconv" "time" "carrot_bbs/internal/cache" "carrot_bbs/internal/model" "carrot_bbs/internal/pkg/sse" "carrot_bbs/internal/pkg/utils" "carrot_bbs/internal/repository" "gorm.io/gorm" ) // 缓存TTL常量 const ( GroupMembersTTL = 120 * time.Second // 群组成员缓存120秒 GroupMembersNullTTL = 5 * time.Second GroupCacheJitter = 0.1 ) // 群组服务错误定义 var ( ErrGroupNotFound = errors.New("群组不存在") ErrNotGroupMember = errors.New("不是群成员") ErrNotGroupAdmin = errors.New("不是群管理员") ErrNotGroupOwner = errors.New("不是群主") ErrGroupFull = errors.New("群已满") ErrAlreadyMember = errors.New("已经是群成员") ErrCannotRemoveOwner = errors.New("不能移除群主") ErrCannotMuteOwner = errors.New("不能禁言群主") ErrMuted = errors.New("你已被禁言") ErrMuteAllEnabled = errors.New("全员禁言中") ErrCannotJoin = errors.New("该群不允许加入") ErrJoinRequestPending = errors.New("加群申请已提交") ErrGroupRequestNotFound = errors.New("加群请求不存在") ErrGroupRequestHandled = errors.New("该加群请求已处理") ErrNotRequestTarget = errors.New("不是邀请目标用户") ErrNoEligibleInvitee = errors.New("没有可邀请的用户") ErrNotMutualFollow = errors.New("仅支持邀请互相关注用户") ) // 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]interface{}) 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) // 成员管理 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) } // GroupMembersResult 群组成员缓存结果 type GroupMembersResult struct { Members []model.GroupMember Total int64 } // groupService 群组服务实现 type groupService struct { db *gorm.DB groupRepo repository.GroupRepository userRepo *repository.UserRepository messageRepo *repository.MessageRepository requestRepo repository.GroupJoinRequestRepository notifyRepo *repository.SystemNotificationRepository sseHub *sse.Hub cache cache.Cache } // NewGroupService 创建群组服务 func NewGroupService(db *gorm.DB, groupRepo repository.GroupRepository, userRepo *repository.UserRepository, messageRepo *repository.MessageRepository, sseHub *sse.Hub) GroupService { return &groupService{ db: db, groupRepo: groupRepo, userRepo: userRepo, messageRepo: messageRepo, requestRepo: repository.NewGroupJoinRequestRepository(db), notifyRepo: repository.NewSystemNotificationRepository(db), sseHub: sseHub, cache: cache.GetCache(), } } 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 { log.Printf("[groupService] 获取群成员失败: groupID=%s, err=%v", groupID, err) return } if s.sseHub != nil { for _, m := range members { s.sseHub.PublishToUser(m.UserID, "group_notice", notice) } } } // ==================== 群组管理 ==================== // 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: 1, // 群主 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, } // 在事务中创建会话和参与者 err = s.db.Transaction(func(tx *gorm.DB) error { // 创建会话 if err := tx.Create(conversation).Error; err != nil { return err } // 添加群主为会话参与者 ownerParticipant := model.ConversationParticipant{ ConversationID: conversation.ID, UserID: ownerID, LastReadSeq: 0, } if err := tx.Create(&ownerParticipant).Error; err != nil { return err } // 添加被邀请的成员为会话参与者 for _, memberID := range memberIDs { if memberID == ownerID { continue } participant := model.ConversationParticipant{ ConversationID: conversation.ID, UserID: memberID, LastReadSeq: 0, } if err := tx.Create(&participant).Error; err != nil { // 单个参与者添加失败继续处理其他成员 continue } } return nil }) if 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 } // UpdateGroup 更新群组信息 func (s *groupService) UpdateGroup(userID string, groupID string, updates map[string]interface{}) 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 { log.Printf("[DissolveGroup] 删除会话失败: groupID=%s, err=%v", groupID, 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 } // 在事务中更新 return s.db.Transaction(func(tx *gorm.DB) error { // 更新群组的群主 group.OwnerID = newOwnerID if err := tx.Save(group).Error; err != nil { return err } // 更新原群主为管理员 if err := tx.Model(&model.GroupMember{}). Where("group_id = ? AND user_id = ?", groupID, userID). 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 }) } // 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 { log.Printf("[groupService] create system notification failed: receiverID=%s type=%s err=%v", receiverID, notifyType, 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]interface{}{"text": noticeContent}}, }, Status: model.MessageStatusNormal, Category: model.CategoryNotification, } if err := s.messageRepo.CreateMessageWithSeq(msg); err != nil { log.Printf("[broadcastMemberJoinNotice] 保存入群提示消息失败: groupID=%s, userID=%s, err=%v", groupID, targetUserID, err) } else { savedMessage = msg } } else { log.Printf("[broadcastMemberJoinNotice] 获取群组会话失败: groupID=%s, err=%v", groupID, 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 { log.Printf("[addMemberToGroupAndConversation] 添加会话参与者失败: groupID=%s, userID=%s, err=%v", group.ID, userID, err) } } } 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{}{} } var reviewers []model.GroupMember if err := s.db.Where("group_id = ? AND role IN ?", groupID, []string{model.GroupRoleOwner, model.GroupRoleAdmin}).Find(&reviewers).Error; err == nil { for _, reviewer := range reviewers { if reviewer.UserID == "" || reviewer.UserID == skipUserID { continue } reviewerSet[reviewer.UserID] = struct{}{} } } result := make([]string, 0, len(reviewerSet)) for id := range reviewerSet { result = append(result, id) } return result } 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 } isInviterAdmin := s.IsGroupAdmin(userID, groupID) inviter, _ := s.userRepo.GetByID(userID) inviterName := "群成员" inviterAvatar := "" if inviter != nil && inviter.Nickname != "" { inviterName = inviter.Nickname } if inviter != nil { 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 err != nil && !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 } if isInviterAdmin { // 群主/管理员邀请:直接发送邀请卡片,等待被邀请人确认 s.sendGroupInviteToTarget(group, req, inviterName, inviterAvatar) createdCount++ continue } inviteeName := "用户" inviteeAvatar := "" if invitee, e := s.userRepo.GetByID(memberID); e == nil && invitee != nil { if invitee.Nickname != "" { inviteeName = invitee.Nickname } inviteeAvatar = invitee.Avatar } reviewerIDs := s.collectGroupReviewerIDs(group.ID, group.OwnerID, userID) for _, reviewerID := range reviewerIDs { s.createSystemNotification(reviewerID, model.SysNotifyGroupJoinApply, inviterName+" 邀请 "+inviteeName+" 加入群聊 "+group.Name+",请审批", &model.SystemNotificationExtra{ ActorIDStr: userID, 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: memberID, TargetUserName: inviteeName, TargetUserAvatar: inviteeAvatar, }) } 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 && applicant.Nickname != "" { applicantName = applicant.Nickname } if applicant != nil { applicantAvatar = applicant.Avatar } // 已有待审批单时补发一次提醒,避免管理端漏看 s.notifyJoinApplyReviewers( group, pendingReq, applicantName, applicantAvatar, userID, applicantName, applicantAvatar, ) return ErrJoinRequestPending } else if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return err } applicant, _ := s.userRepo.GetByID(userID) applicantName := "用户" applicantAvatar := "" if applicant != nil && applicant.Nickname != "" { applicantName = applicant.Nickname } if applicant != nil { 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.ReviewerID = userID req.ReviewedAt = &now req.Reason = reason if approve { 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 { 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 } if req.RequestType == model.GroupJoinRequestTypeInvite && req.ReviewerID != "" { // invite 类型中 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() req.ReviewerID = userID req.ReviewedAt = &now req.Reason = reason targetUserName := "用户" if u, e := s.userRepo.GetByID(req.TargetUserID); e == nil && u != nil && u.Nickname != "" { targetUserName = u.Nickname } reviewerName := "管理员" reviewerAvatar := "" if u, e := s.userRepo.GetByID(userID); e == nil && u != nil { if u.Nickname != "" { reviewerName = u.Nickname } reviewerAvatar = u.Avatar } if approve { if req.RequestType == model.GroupJoinRequestTypeInvite { // 管理员审批通过后,不直接拉人;发送邀请卡片等待对方确认 inviterName := "群成员" inviterAvatar := "" if u, e := s.userRepo.GetByID(req.InitiatorID); e == nil && u != nil { if u.Nickname != "" { inviterName = u.Nickname } inviterAvatar = u.Avatar } s.sendGroupInviteToTarget(group, req, inviterName, inviterAvatar) 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.GroupJoinRequestStatusPending), }) } else { if err := s.addMemberToGroupAndConversation(group, req.TargetUserID, userID); err != nil { return err } req.Status = model.GroupJoinRequestStatusAccepted 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 if req.RequestType == model.GroupJoinRequestTypeInvite { // 成员邀请被管理员拒绝,仅通知邀请人 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) } } // 失效群组成员缓存 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 { log.Printf("[RemoveMember] 移除会话参与者失败: groupID=%s, userID=%s, err=%v", groupID, targetUserID, err) } } } // 失效群组成员缓存 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 { log.Printf("[MuteMember] 获取成员失败: %v", err) return err } log.Printf("[MuteMember] 禁言前状态: member.Muted=%v, 即将设置 muted=%v", member.Muted, muted) member.Muted = muted if err := s.groupRepo.UpdateMember(member); err != nil { log.Printf("[MuteMember] 更新成员失败: %v", err) return err } log.Printf("[MuteMember] 禁言状态已更新到数据库") // 验证更新结果 updatedMember, _ := s.groupRepo.GetMember(groupID, targetUserID) if updatedMember != nil { log.Printf("[MuteMember] 验证: member.Muted=%v", 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]interface{}{"text": noticeContent}}, }, Status: model.MessageStatusNormal, Category: model.CategoryNotification, } // 保存消息并获取 seq if err := s.messageRepo.CreateMessageWithSeq(msg); err != nil { log.Printf("[MuteMember] 保存禁言消息失败: %v", err) } else { savedMessage = msg log.Printf("[MuteMember] 禁言消息已保存, ID=%s, Seq=%d", msg.ID, msg.Seq) } } else { log.Printf("[MuteMember] 获取群组会话失败: %v", 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) }