Set up project files and add .gitignore to exclude local build/runtime artifacts. Made-with: Cursor
1492 lines
43 KiB
Go
1492 lines
43 KiB
Go
package service
|
||
|
||
import (
|
||
"errors"
|
||
"fmt"
|
||
"log"
|
||
"strconv"
|
||
"time"
|
||
|
||
"carrot_bbs/internal/cache"
|
||
"carrot_bbs/internal/model"
|
||
"carrot_bbs/internal/pkg/utils"
|
||
"carrot_bbs/internal/pkg/websocket"
|
||
"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
|
||
wsManager *websocket.WebSocketManager
|
||
cache cache.Cache
|
||
}
|
||
|
||
// NewGroupService 创建群组服务
|
||
func NewGroupService(db *gorm.DB, groupRepo repository.GroupRepository, userRepo *repository.UserRepository, messageRepo *repository.MessageRepository, wsManager *websocket.WebSocketManager) GroupService {
|
||
return &groupService{
|
||
db: db,
|
||
groupRepo: groupRepo,
|
||
userRepo: userRepo,
|
||
messageRepo: messageRepo,
|
||
requestRepo: repository.NewGroupJoinRequestRepository(db),
|
||
notifyRepo: repository.NewSystemNotificationRepository(db),
|
||
wsManager: wsManager,
|
||
cache: cache.GetCache(),
|
||
}
|
||
}
|
||
|
||
// ==================== 群组管理 ====================
|
||
|
||
// 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)
|
||
}
|
||
}
|
||
|
||
if s.wsManager == nil {
|
||
return
|
||
}
|
||
|
||
noticeMsg := websocket.GroupNoticeMessage{
|
||
NoticeType: "member_join",
|
||
GroupID: groupID,
|
||
Data: websocket.GroupNoticeData{
|
||
UserID: targetUserID,
|
||
Username: targetUserName,
|
||
OperatorID: operatorID,
|
||
},
|
||
Timestamp: time.Now().UnixMilli(),
|
||
}
|
||
if savedMessage != nil {
|
||
noticeMsg.MessageID = savedMessage.ID
|
||
noticeMsg.Seq = savedMessage.Seq
|
||
}
|
||
|
||
wsMsg := websocket.CreateWSMessage(websocket.MessageTypeGroupNotice, noticeMsg)
|
||
members, _, err := s.groupRepo.GetMembers(groupID, 1, 1000)
|
||
if err != nil {
|
||
log.Printf("[broadcastMemberJoinNotice] 获取群成员失败: groupID=%s, err=%v", groupID, err)
|
||
return
|
||
}
|
||
for _, m := range members {
|
||
if s.wsManager.IsUserOnline(m.UserID) {
|
||
s.wsManager.SendToUser(m.UserID, wsMsg)
|
||
}
|
||
}
|
||
}
|
||
|
||
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)
|
||
}
|
||
}
|
||
|
||
// 发送WebSocket通知给群成员
|
||
if s.wsManager != nil {
|
||
log.Printf("[MuteMember] 准备发送禁言通知: groupID=%s, targetUserID=%s, noticeType=%s, operatorID=%s", groupID, targetUserID, noticeType, userID)
|
||
|
||
// 构建通知消息,包含保存的消息信息
|
||
noticeMsg := websocket.GroupNoticeMessage{
|
||
NoticeType: noticeType,
|
||
GroupID: groupID,
|
||
Data: websocket.GroupNoticeData{
|
||
UserID: targetUserID,
|
||
OperatorID: userID,
|
||
},
|
||
Timestamp: time.Now().UnixMilli(),
|
||
}
|
||
|
||
// 如果消息已保存,添加消息ID和seq
|
||
if savedMessage != nil {
|
||
noticeMsg.MessageID = savedMessage.ID
|
||
noticeMsg.Seq = savedMessage.Seq
|
||
}
|
||
|
||
wsMsg := websocket.CreateWSMessage(websocket.MessageTypeGroupNotice, noticeMsg)
|
||
log.Printf("[MuteMember] 创建的WebSocket消息: Type=%s, Data=%+v", wsMsg.Type, wsMsg.Data)
|
||
|
||
// 获取所有群成员并发送通知
|
||
members, _, err := s.groupRepo.GetMembers(groupID, 1, 1000)
|
||
if err == nil {
|
||
log.Printf("[MuteMember] 获取到群成员数量: %d", len(members))
|
||
for _, m := range members {
|
||
isOnline := s.wsManager.IsUserOnline(m.UserID)
|
||
log.Printf("[MuteMember] 成员 %s 在线状态: %v", m.UserID, isOnline)
|
||
if isOnline {
|
||
s.wsManager.SendToUser(m.UserID, wsMsg)
|
||
log.Printf("[MuteMember] 已发送通知给成员: %s", m.UserID)
|
||
}
|
||
}
|
||
} else {
|
||
log.Printf("[MuteMember] 获取群成员失败: %v", err)
|
||
}
|
||
}
|
||
|
||
// 失效群组成员缓存
|
||
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)
|
||
}
|