Files
backend/internal/service/group_service.go
lan 4d8f2ec997 Initial backend repository commit.
Set up project files and add .gitignore to exclude local build/runtime artifacts.

Made-with: Cursor
2026-03-09 21:28:58 +08:00

1492 lines
43 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 (
"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)
}