package cn.iocoder.yudao.module.im.service.group; import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.bean.copier.CopyOptions; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupAdminAddReqVO; import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupAdminRemoveReqVO; import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupCancelMuteMemberReqVO; import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupCreateReqVO; import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupMuteAllReqVO; import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupMuteMemberReqVO; import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupTransferOwnerReqVO; import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupUpdateReqVO; import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberInviteReqVO; import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberRemoveReqVO; import cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.ImGroupManagerBanReqVO; import cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.ImGroupManagerPageReqVO; import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO; import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO; import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO; import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupMapper; import cn.iocoder.yudao.module.im.enums.group.ImGroupAddSourceEnum; import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum; import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum; import cn.iocoder.yudao.module.im.enums.ImContentTypeEnum; import cn.iocoder.yudao.module.im.framework.config.ImProperties; import cn.iocoder.yudao.module.im.service.friend.ImFriendService; import cn.iocoder.yudao.module.im.service.message.ImGroupMessageService; import cn.iocoder.yudao.module.im.service.message.dto.ImGroupMessageSendDTO; import cn.iocoder.yudao.module.system.api.user.AdminUserApi; import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; import jakarta.annotation.Resource; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import java.time.LocalDateTime; import java.util.*; import java.util.stream.Collectors; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; import static cn.iocoder.yudao.module.im.dal.redis.RedisKeyConstants.GROUP; import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*; /** * 用户群 Service 实现类 * * @author 芋道源码 */ @Service @Validated public class ImGroupServiceImpl implements ImGroupService { @Resource private ImGroupMapper groupMapper; @Resource @Lazy // 避免循环依赖 private ImGroupMemberService groupMemberService; @Resource @Lazy // 避免循环依赖 private ImGroupMessageService groupMessageService; @Resource @Lazy // 避免循环依赖 private ImGroupRequestService groupRequestService; @Resource private ImFriendService friendService; @Resource private AdminUserApi adminUserApi; @Resource private ImProperties imProperties; // ==================== 群的写操作 ==================== @Override @Transactional(rollbackFor = Exception.class) public ImGroupDO createGroup(ImGroupCreateReqVO createReqVO, Long userId) { // 1.1 处理初始成员列表(去重 + 排除创建者自己) Set initialMemberUserIds = createReqVO.getMemberUserIds() == null ? new HashSet<>() : new HashSet<>(createReqVO.getMemberUserIds()); initialMemberUserIds.remove(userId); // 1.2 校验初始成员都是创建者的好友 if (CollUtil.isNotEmpty(initialMemberUserIds)) { List friends = friendService.getActiveFriendList(userId, initialMemberUserIds); Set friendUserIds = convertSet(friends, ImFriendDO::getFriendUserId); Collection notFriendUserIds = CollUtil.subtract(initialMemberUserIds, friendUserIds); if (CollUtil.isNotEmpty(notFriendUserIds)) { throw exception(GROUP_INVITE_NOT_FRIEND, getUserNicknames(notFriendUserIds)); } } // 1.3 校验群人数上限(创建者 + 初始成员 ≤ 群成员上限) int maxMember = imProperties.getGroup().getMaxMember(); if (initialMemberUserIds.size() + 1 > maxMember) { throw exception(GROUP_MEMBER_EXCEED, maxMember); } // 2.1 插入群记录 ImGroupDO group = BeanUtils.toBean(createReqVO, ImGroupDO.class) .setOwnerUserId(userId).setStatus(CommonStatusEnum.ENABLE.getStatus()); groupMapper.insert(group); // 2.2 创建者作为 OWNER 入群 groupMemberService.addGroupMember(group.getId(), userId, ImGroupMemberRoleEnum.OWNER.getRole()); // 2.3 批量添加初始成员;标记 addSource=INVITE / inviter=创建者 if (CollUtil.isNotEmpty(initialMemberUserIds)) { groupMemberService.addGroupMembers(group.getId(), initialMemberUserIds, ImGroupAddSourceEnum.INVITE.getSource(), userId); } // 3. 推送 GROUP_CREATE 通知给全员(含创建者多端同步 + 初始成员) List allMemberUserIds = CollectionUtils.of(userId, initialMemberUserIds); groupMessageService.sendGroupMessage(userId, allMemberUserIds, ImGroupMessageSendDTO.ofGroupCreate(group.getId(), userId, allMemberUserIds)); return group; } @Override @CacheEvict(cacheNames = GROUP, key = "#updateReqVO.id") @Transactional(rollbackFor = Exception.class) public ImGroupDO updateGroup(ImGroupUpdateReqVO updateReqVO, Long userId) { // 1.1 校验群存在:group 留作老值备份,通知里 oldXXX 字段从这里取 ImGroupDO group = validateGroupExists(updateReqVO.getId()); // 1.2 校验操作人是群主 if (ObjUtil.notEqual(group.getOwnerUserId(), userId)) { throw exception(GROUP_NOT_OWNER); } // 2. 更新数据库(newGroup 仅含变更字段) ImGroupDO newGroup = BeanUtils.toBean(updateReqVO, ImGroupDO.class); groupMapper.updateById(newGroup); // 3. 按变更字段分别推送 GROUP_NAME / NOTICE / INFO_UPDATE 通知;活跃成员只查一次复用,避免 3 次 Redis GET // name / avatar 不允许空串(业务上必须非空),notice 允许空串(清空公告也是有效操作) Long groupId = group.getId(); boolean nameChanged = StrUtil.isNotEmpty(updateReqVO.getName()); boolean noticeChanged = updateReqVO.getNotice() != null; boolean avatarChanged = StrUtil.isNotEmpty(updateReqVO.getAvatar()); boolean joinApprovalChanged = updateReqVO.getJoinApproval() != null && ObjUtil.notEqual(group.getJoinApproval(), updateReqVO.getJoinApproval()); if (nameChanged || noticeChanged || avatarChanged || joinApprovalChanged) { List memberUserIds = groupMemberService.getActiveGroupMemberUserIdsByGroupId(groupId); if (nameChanged) { groupMessageService.sendGroupMessage(userId, memberUserIds, ImGroupMessageSendDTO.ofGroupNameUpdate( groupId, userId, group.getName(), updateReqVO.getName())); } if (noticeChanged) { groupMessageService.sendGroupMessage(userId, memberUserIds, ImGroupMessageSendDTO.ofGroupNoticeUpdate( groupId, userId, group.getNotice(), updateReqVO.getNotice())); } if (avatarChanged) { groupMessageService.sendGroupMessage(userId, memberUserIds, ImGroupMessageSendDTO.ofGroupInfoUpdate( groupId, userId, group.getAvatar(), updateReqVO.getAvatar(), null, null)); } if (joinApprovalChanged) { groupMessageService.sendGroupMessage(userId, memberUserIds, ImGroupMessageSendDTO.ofGroupInfoUpdate( groupId, userId, null, null, group.getJoinApproval(), updateReqVO.getJoinApproval())); } } // 4. 返回合并后的新群信息(updateReqVO 非空字段覆盖 group) BeanUtil.copyProperties(updateReqVO, group, CopyOptions.create().ignoreNullValue()); return group; } @Override @CacheEvict(cacheNames = GROUP, key = "#id") @Transactional(rollbackFor = Exception.class) public void dissolveGroup(Long id, Long userId) { // 1. 校验群存在 + 当前用户是群主 ImGroupDO group = validateGroupNotDissolved(id); if (ObjUtil.notEqual(group.getOwnerUserId(), userId)) { throw exception(GROUP_NOT_OWNER); } // 2. 解散群 dissolveGroup0(id, userId); } private void dissolveGroup0(Long id, Long userId) { // 1. 先发 GROUP_DISSOLVE 通知:放在成员移除前,sendGroupMessage 才能查到全员 groupMessageService.sendGroupMessage(userId, ImGroupMessageSendDTO.ofGroupDissolve(id, userId)); // 2.1 更新群状态为已解散 groupMapper.updateById(new ImGroupDO().setId(id) .setStatus(CommonStatusEnum.DISABLE.getStatus()).setDissolvedTime(LocalDateTime.now())); // 2.2 移除全部群成员 groupMemberService.removeGroupMembersByGroupId(id); } // ==================== 群成员的写操作 ==================== @Override @Transactional(rollbackFor = Exception.class) public void inviteGroupMember(Long userId, ImGroupMemberInviteReqVO inviteReqVO) { Long groupId = inviteReqVO.getGroupId(); // 1.1 校验群存在 + 当前用户是群成员;同时拿到 role 供下面审批分支判断 ImGroupDO group = validateGroupExists(groupId); ImGroupMemberDO operator = groupMemberService.validateMemberInGroup(groupId, userId); // 1.2 入参去重 + 排除已在群中的用户 List activeMembers = groupMemberService.getActiveGroupMemberListByGroupId(groupId); Set activeMemberUserIds = convertSet(activeMembers, ImGroupMemberDO::getUserId); List memberUserIds = CollUtil.subtractToList( CollUtil.distinct(inviteReqVO.getMemberUserIds()), activeMemberUserIds); if (CollUtil.isEmpty(memberUserIds)) { return; } // 1.3 校验被邀请人都是当前用户的好友 List friends = friendService.getActiveFriendList(userId, memberUserIds); Set friendUserIds = convertSet(friends, ImFriendDO::getFriendUserId); Collection notFriendUserIds = CollUtil.subtract(memberUserIds, friendUserIds); if (CollUtil.isNotEmpty(notFriendUserIds)) { throw exception(GROUP_INVITE_NOT_FRIEND, getUserNicknames(notFriendUserIds)); } // 1.4 校验群人数上限 int maxMember = imProperties.getGroup().getMaxMember(); if (activeMembers.size() + memberUserIds.size() > maxMember) { throw exception(GROUP_MEMBER_EXCEED, maxMember); } // 2. 情况一:群开启审批 + 邀请人是普通成员,落 group_request 等群主 / 管理员处理 // 群主 / 管理员邀请,直接拉人进群 if (Boolean.TRUE.equals(group.getJoinApproval()) && !ImGroupMemberRoleEnum.isOwnerOrAdmin(operator.getRole())) { groupRequestService.createInviteRequestList(groupId, userId, memberUserIds); return; } // 3. 情况二:未开审批 / 群主 / 管理员邀请,直进;批量添加群成员,写 addSource=INVITE / inviterUserId=操作人 留痕 groupMemberService.addGroupMembers(groupId, memberUserIds, ImGroupAddSourceEnum.INVITE.getSource(), userId); // 4. 发 GROUP_MEMBER_INVITE 通知给全员;本地拼 receivers(已查的 active + 新邀请)避免缓存刚 evict 后强制走 DB Set allReceivers = new HashSet<>(memberUserIds); allReceivers.addAll(activeMemberUserIds); groupMessageService.sendGroupMessage(userId, allReceivers, ImGroupMessageSendDTO.ofGroupMemberInvite(groupId, userId, memberUserIds)); } @Override @Transactional(rollbackFor = Exception.class) public void quitGroup(Long groupId, Long userId) { // 1.1 校验群存在且未解散 ImGroupDO group = getSelf().getGroup(groupId); if (group == null) { throw exception(GROUP_NOT_EXISTS); } if (CommonStatusEnum.DISABLE.getStatus().equals(group.getStatus())) { throw exception(GROUP_DISSOLVED); } // 1.2 群主不可退群 if (ObjUtil.equal(group.getOwnerUserId(), userId)) { throw exception(GROUP_OWNER_CANNOT_QUIT); } // 1.3 校验当前用户是有效群成员;防止非成员触发广播 + 后续 remove 失败时无法回滚已推送的事件 groupMemberService.validateMemberInGroup(groupId, userId); // 2. 先发广播,后移成员(见类 javadoc) groupMessageService.sendGroupMessage(userId, ImGroupMessageSendDTO.ofGroupMemberQuit(groupId, userId)); // 3. 移除群成员 groupMemberService.removeGroupMember(groupId, userId); } @Override @Transactional(rollbackFor = Exception.class) public void removeGroupMember(Long userId, ImGroupMemberRemoveReqVO removeReqVO) { Long groupId = removeReqVO.getGroupId(); Set targetUserIds = new HashSet<>(removeReqVO.getMemberUserIds()); // 1.1 校验群存在 + 操作者是群主或管理员 ImGroupMemberDO operator = validateGroupOwnerOrAdmin(groupId, userId); // 1.2 不能移除自己 if (targetUserIds.contains(userId)) { throw exception(GROUP_CANNOT_REMOVE_SELF); } // 1.3 仅保留仍有效的成员;已退群(DISABLE)/ 查无记录的目标直接跳过,只踢有效成员,不让整批失败 List targets = filterList(groupMemberService.getGroupMembers(groupId, targetUserIds), target -> CommonStatusEnum.ENABLE.getStatus().equals(target.getStatus())); Set validTargetUserIds = convertSet(targets, ImGroupMemberDO::getUserId); // 1.4 目标全部已不在群:无人可踢,直接返回 if (CollUtil.isEmpty(validTargetUserIds)) { return; } // 1.5 三档权限校验:群主不可被移出;管理员不能移出管理员 boolean operatorIsAdmin = ImGroupMemberRoleEnum.isAdmin(operator.getRole()); for (ImGroupMemberDO target : targets) { if (ImGroupMemberRoleEnum.isOwner(target.getRole())) { throw exception(GROUP_REMOVE_OWNER_DENIED); } if (operatorIsAdmin && ImGroupMemberRoleEnum.isAdmin(target.getRole())) { throw exception(GROUP_REMOVE_ADMIN_DENIED); } } // 2. 先发 GROUP_MEMBER_KICK 通知:放在被踢者移除前,sendGroupMessage 才能查到全员(含被踢者) groupMessageService.sendGroupMessage(userId, ImGroupMessageSendDTO.ofGroupMemberKick(groupId, userId, validTargetUserIds)); // 3. 批量移除群成员;不清理读位置(保留退群前历史已读,供离线补偿) groupMemberService.removeGroupMembers(groupId, validTargetUserIds); } @Override @Transactional(rollbackFor = Exception.class) public void addGroupAdmin(Long userId, ImGroupAdminAddReqVO reqVO) { Long groupId = reqVO.getId(); Set targetUserIds = new HashSet<>(reqVO.getUserIds()); // 1.1 仅群主可操作 validateGroupOwnerForUpdate(groupId, userId); // 1.2 校验目标都是有效成员且非群主 Map targetMap = convertMap( groupMemberService.getGroupMembers(groupId, targetUserIds), ImGroupMemberDO::getUserId); validateAdminTargets(targetUserIds, targetMap); // 1.3 幂等过滤:跳过已是 ADMIN Set changedUserIds = convertSet(targetUserIds, id -> id, id -> !ImGroupMemberRoleEnum.isAdmin(targetMap.get(id).getRole())); if (CollUtil.isEmpty(changedUserIds)) { return; } // 1.4 校验上限 Long existAdminCount = groupMemberService.getGroupMemberCountByRole( groupId, ImGroupMemberRoleEnum.ADMIN.getRole()); int adminMaxCount = imProperties.getGroup().getAdminMaxCount(); if (existAdminCount + changedUserIds.size() > adminMaxCount) { throw exception(GROUP_ADMIN_MAX_LIMIT, adminMaxCount); } // 2. 批量更新角色 int affected = groupMemberService.updateGroupMemberRole(groupId, changedUserIds, ImGroupMemberRoleEnum.ADMIN.getRole()); if (affected != changedUserIds.size()) { throw exception(GROUP_ADMIN_TARGET_NOT_IN_GROUP); } // 3. 推送 GROUP_ADMIN_ADD 通知给全员 groupMessageService.sendGroupMessage(userId, ImGroupMessageSendDTO.ofGroupAdminAdd(groupId, userId, changedUserIds)); } @Override @Transactional(rollbackFor = Exception.class) public void removeGroupAdmin(Long userId, ImGroupAdminRemoveReqVO reqVO) { Long groupId = reqVO.getId(); Set targetUserIds = new HashSet<>(reqVO.getUserIds()); // 1.1 仅群主可操作 validateGroupOwnerForUpdate(groupId, userId); // 1.2 校验目标都是有效成员且非群主 Map targetMap = convertMap( groupMemberService.getGroupMembers(groupId, targetUserIds), ImGroupMemberDO::getUserId); validateAdminTargets(targetUserIds, targetMap); // 1.3 幂等过滤:跳过已是 MEMBER Set changedUserIds = convertSet(targetUserIds, id -> id, id -> ImGroupMemberRoleEnum.isAdmin(targetMap.get(id).getRole())); if (CollUtil.isEmpty(changedUserIds)) { return; } // 2. 批量更新角色 int affected = groupMemberService.updateGroupMemberRole(groupId, changedUserIds, ImGroupMemberRoleEnum.NORMAL.getRole()); if (affected != changedUserIds.size()) { throw exception(GROUP_ADMIN_TARGET_NOT_IN_GROUP); } // 3. 推送 GROUP_ADMIN_REMOVE 通知给全员 groupMessageService.sendGroupMessage(userId, ImGroupMessageSendDTO.ofGroupAdminRemove(groupId, userId, changedUserIds)); } /** * 校验管理员变更目标都是当前群的有效成员(status=ENABLE)且非群主 */ private void validateAdminTargets(Set targetUserIds, Map targetMap) { for (Long targetUserId : targetUserIds) { ImGroupMemberDO target = targetMap.get(targetUserId); if (target == null || CommonStatusEnum.DISABLE.getStatus().equals(target.getStatus())) { throw exception(GROUP_ADMIN_TARGET_NOT_IN_GROUP); } if (ImGroupMemberRoleEnum.isOwner(target.getRole())) { throw exception(GROUP_ADMIN_TARGET_IS_OWNER); } } } @Override @CacheEvict(cacheNames = GROUP, key = "#transferReqVO.id") @Transactional(rollbackFor = Exception.class) public void transferGroupOwner(Long userId, ImGroupTransferOwnerReqVO transferReqVO) { Long groupId = transferReqVO.getId(); Long newOwnerUserId = transferReqVO.getNewOwnerUserId(); // 1.1 仅老群主可执行 validateGroupOwnerForUpdate(groupId, userId); // 1.2 不能转让给自己 if (ObjUtil.equal(userId, newOwnerUserId)) { throw exception(GROUP_TRANSFER_OWNER_TO_SELF); } // 1.3 新群主必须是群的有效成员 ImGroupMemberDO newOwner = groupMemberService.validateMemberInGroup(groupId, newOwnerUserId); // 2.1 更新成员角色 int newOwnerAffected = groupMemberService.updateGroupMemberRole(groupId, CollUtil.newHashSet(newOwner.getUserId()), ImGroupMemberRoleEnum.OWNER.getRole()); if (newOwnerAffected != 1) { throw exception(GROUP_MEMBER_NOT_IN_GROUP); } int oldOwnerAffected = groupMemberService.updateGroupMemberRole(groupId, CollUtil.newHashSet(userId), ImGroupMemberRoleEnum.NORMAL.getRole()); if (oldOwnerAffected != 1) { throw exception(GROUP_MEMBER_NOT_IN_GROUP); } // 2.2 更新群主编号 groupMapper.updateById(new ImGroupDO().setId(groupId).setOwnerUserId(newOwnerUserId)); // 3. 推送 GROUP_OWNER_TRANSFER 通知给全员 groupMessageService.sendGroupMessage(userId, ImGroupMessageSendDTO.ofGroupOwnerTransfer(groupId, userId, newOwnerUserId)); } @Override @CacheEvict(cacheNames = GROUP, key = "#groupId") @Transactional(rollbackFor = Exception.class) public void pinGroupMessage(Long userId, Long groupId, Long messageId) { // 1.1 校验群主 / 管理员;同时拿到 group 复用,避免再走一次 @Cacheable ImGroupDO group = validateOwnerOrAdminAndGetGroupForUpdate(groupId, userId); // 1.2 校验消息属于该群、是普通聊天消息(绕过前端菜单不允许置顶群事件 / 撤回事件)、且未被撤回 ImGroupMessageDO message = groupMessageService.getGroupMessage(messageId); if (message == null || ObjUtil.notEqual(message.getGroupId(), groupId)) { throw exception(MESSAGE_NOT_IN_GROUP); } if (!ImContentTypeEnum.validate(message.getType()).isNormal() || ImMessageStatusEnum.RECALL.getStatus().equals(message.getStatus())) { throw exception(MESSAGE_NOT_IN_GROUP); } // 1.3 定向消息(receiverUserIds 非空)不允许置顶:置顶会向全群广播,泄露原本仅部分人可见的内容 if (CollUtil.isNotEmpty(message.getReceiverUserIds())) { throw exception(GROUP_MESSAGE_PIN_DIRECTED_DENIED); } // 2. 幂等 + 上限校验 List pinned = new ArrayList<>(CollUtil.emptyIfNull(group.getPinnedMessageIds())); if (pinned.contains(messageId)) { throw exception(GROUP_MESSAGE_ALREADY_PINNED); } int pinMaxCount = imProperties.getGroup().getPinMaxCount(); if (pinned.size() >= pinMaxCount) { throw exception(GROUP_MESSAGE_PIN_MAX_LIMIT, pinMaxCount); } pinned.add(messageId); groupMapper.updateById(new ImGroupDO().setId(groupId).setPinnedMessageIds(pinned)); // 3. 推送 GROUP_MESSAGE_PIN 通知给全员 groupMessageService.sendGroupMessage(userId, ImGroupMessageSendDTO.ofGroupMessagePin(groupId, userId, message)); } @Override @CacheEvict(cacheNames = GROUP, key = "#groupId") @Transactional(rollbackFor = Exception.class) public void unpinGroupMessage(Long userId, Long groupId, Long messageId) { // 1. 校验群主 / 管理员;同时拿到 group 复用,避免再走一次 @Cacheable ImGroupDO group = validateOwnerOrAdminAndGetGroupForUpdate(groupId, userId); // 2. 幂等校验 List pinned = new ArrayList<>(CollUtil.emptyIfNull(group.getPinnedMessageIds())); if (!pinned.contains(messageId)) { throw exception(GROUP_MESSAGE_NOT_PINNED); } pinned.remove(messageId); groupMapper.updateById(new ImGroupDO().setId(groupId).setPinnedMessageIds(pinned)); // 3. 推送 GROUP_MESSAGE_UNPIN 通知给全员 groupMessageService.sendGroupMessage(userId, ImGroupMessageSendDTO.ofGroupMessageUnpin(groupId, userId, messageId)); } /** * 校验登录用户是群主或管理员,同时返回群信息 */ private ImGroupDO validateOwnerOrAdminAndGetGroup(Long groupId, Long userId) { ImGroupDO group = validateGroupExists(groupId); ImGroupMemberDO member = groupMemberService.validateMemberInGroup(groupId, userId); if (!ImGroupMemberRoleEnum.isOwnerOrAdmin(member.getRole())) { throw exception(GROUP_NOT_OWNER_OR_ADMIN); } return group; } /** * 校验登录用户是群主或管理员,同时锁定群信息 */ private ImGroupDO validateOwnerOrAdminAndGetGroupForUpdate(Long groupId, Long userId) { ImGroupDO group = validateGroupExistsForUpdate(groupId); ImGroupMemberDO member = groupMemberService.validateMemberInGroup(groupId, userId); if (!ImGroupMemberRoleEnum.isOwnerOrAdmin(member.getRole())) { throw exception(GROUP_NOT_OWNER_OR_ADMIN); } return group; } // ==================== 群的读操作 ==================== @Override public ImGroupDO validateGroupExists(Long id) { ImGroupDO group = getSelf().getGroup(id); if (group == null) { throw exception(GROUP_NOT_EXISTS); } if (Boolean.TRUE.equals(group.getBanned())) { throw exception(GROUP_BANNED); } if (CommonStatusEnum.DISABLE.getStatus().equals(group.getStatus())) { throw exception(GROUP_DISSOLVED); } return group; } @Override public void validateMemberCountLimit(Long groupId, int addCount) { // 1. 锁定群记录 validateGroupExistsForUpdate(groupId); // 2. 校验成员数量上限 int activeCount = groupMemberService.getActiveGroupMemberUserIdsByGroupId(groupId).size(); int maxMember = imProperties.getGroup().getMaxMember(); if (activeCount + addCount > maxMember) { throw exception(GROUP_MEMBER_EXCEED, maxMember); } } private ImGroupDO validateGroupOwnerForUpdate(Long groupId, Long userId) { ImGroupDO group = validateGroupExistsForUpdate(groupId); if (ObjUtil.notEqual(group.getOwnerUserId(), userId)) { throw exception(GROUP_NOT_OWNER); } return group; } private ImGroupDO validateGroupExistsForUpdate(Long groupId) { ImGroupDO group = groupMapper.selectByIdForUpdate(groupId); if (group == null) { throw exception(GROUP_NOT_EXISTS); } if (Boolean.TRUE.equals(group.getBanned())) { throw exception(GROUP_BANNED); } if (CommonStatusEnum.DISABLE.getStatus().equals(group.getStatus())) { throw exception(GROUP_DISSOLVED); } return group; } private ImGroupDO validateGroupNotDissolved(Long id) { ImGroupDO group = getSelf().getGroup(id); if (group == null) { throw exception(GROUP_NOT_EXISTS); } if (CommonStatusEnum.DISABLE.getStatus().equals(group.getStatus())) { throw exception(GROUP_DISSOLVED); } return group; } /** * 校验当前用户是群主或管理员,否则抛 GROUP_NOT_OWNER_OR_ADMIN */ private ImGroupMemberDO validateGroupOwnerOrAdmin(Long groupId, Long userId) { validateGroupExists(groupId); ImGroupMemberDO member = groupMemberService.validateMemberInGroup(groupId, userId); if (!ImGroupMemberRoleEnum.isOwnerOrAdmin(member.getRole())) { throw exception(GROUP_NOT_OWNER_OR_ADMIN); } return member; } @Override @Cacheable(cacheNames = GROUP, key = "#id", unless = "#result == null") public ImGroupDO getGroup(Long id) { return groupMapper.selectById(id); } @Override public Map getGroupMap(Collection ids) { if (CollUtil.isEmpty(ids)) { return Collections.emptyMap(); } return convertMap(groupMapper.selectByIds(ids), ImGroupDO::getId); } @Override public List getMyGroupList(Long userId) { // 1. 查用户曾经加入的所有群(含退群,前端按需过滤);退群前的离线消息也要能展示 List members = groupMemberService.getGroupMemberListByUserId(userId); if (CollUtil.isEmpty(members)) { return Collections.emptyList(); } // 2. 批量查询群信息(不按 status / banned 过滤,已解散 / 封禁的群也要返回,供前端展示历史消息的群名 / 头像) Set groupIds = convertSet(members, ImGroupMemberDO::getGroupId); return groupMapper.selectByIds(groupIds); } // ==================== 管理后台 ==================== @Override public PageResult getGroupPage(ImGroupManagerPageReqVO pageReqVO) { return groupMapper.selectPage(pageReqVO); } @Override @CacheEvict(cacheNames = GROUP, key = "#banReqVO.id") public void banGroup(Long operatorUserId, ImGroupManagerBanReqVO banReqVO) { // 1. 校验群存在且未解散 ImGroupDO group = getSelf().getGroup(banReqVO.getId()); if (group == null) { throw exception(GROUP_NOT_EXISTS); } if (CommonStatusEnum.DISABLE.getStatus().equals(group.getStatus())) { throw exception(GROUP_DISSOLVED); } // 2. 幂等:已封禁直接返回,避免重复广播封禁通知 if (Boolean.TRUE.equals(group.getBanned())) { return; } // 3. 更新封禁状态 groupMapper.updateById(new ImGroupDO().setId(banReqVO.getId()) .setBanned(true).setBannedReason(banReqVO.getReason()).setBannedTime(LocalDateTime.now())); // 4. 广播通知 groupMessageService.sendGroupMessage(operatorUserId, ImGroupMessageSendDTO.ofGroupBanned(banReqVO.getId(), operatorUserId, true)); } @Override @CacheEvict(cacheNames = GROUP, key = "#id") public void unbanGroup(Long operatorUserId, Long id) { // 1. 校验群存在 if (getSelf().getGroup(id) == null) { throw exception(GROUP_NOT_EXISTS); } // 2. 解封(保留 bannedReason / bannedTime 作为历史记录) groupMapper.updateById(new ImGroupDO().setId(id).setBanned(false)); // 3. 广播通知 groupMessageService.sendGroupMessage(operatorUserId, ImGroupMessageSendDTO.ofGroupBanned(id, operatorUserId, false)); } @Override @CacheEvict(cacheNames = GROUP, key = "#id") @Transactional(rollbackFor = Exception.class) public void dissolveGroupByManager(Long operatorUserId, Long id) { // 1. 校验群存在且未解散 validateGroupNotDissolved(id); // 2. 解散群 dissolveGroup0(id, operatorUserId); } // ==================== 群禁言 ==================== @Override @CacheEvict(cacheNames = GROUP, key = "#reqVO.id") @Transactional(rollbackFor = Exception.class) public void muteAll(Long userId, ImGroupMuteAllReqVO reqVO) { // 1. 校验群主或管理员 validateGroupOwnerOrAdmin(reqVO.getId(), userId); // 2. 更新 mutedAll groupMapper.updateById(new ImGroupDO().setId(reqVO.getId()).setMutedAll(reqVO.getMutedAll())); // 3. 广播通知 ImGroupMessageSendDTO messageSendDTO = Boolean.TRUE.equals(reqVO.getMutedAll()) ? ImGroupMessageSendDTO.ofGroupMuted(reqVO.getId(), userId) : ImGroupMessageSendDTO.ofGroupCancelMuted(reqVO.getId(), userId); groupMessageService.sendGroupMessage(userId, messageSendDTO); } @Override @Transactional(rollbackFor = Exception.class) public void muteMember(Long userId, ImGroupMuteMemberReqVO reqVO) { // 1.1 不能禁言自己 if (ObjUtil.equal(userId, reqVO.getUserId())) { throw exception(GROUP_MUTE_MEMBER_SELF); } // 1.2 校验群存在且未封禁 validateGroupExists(reqVO.getId()); // 1.3 校验操作人和目标都在群中 ImGroupMemberDO operatorMember = groupMemberService.validateMemberInGroup(reqVO.getId(), userId); ImGroupMemberDO targetMember = groupMemberService.validateMemberInGroup(reqVO.getId(), reqVO.getUserId()); // 1.4 三档权限校验 validateMutePermission(operatorMember, targetMember); // 2. 设置 muteEndTime LocalDateTime muteEndTime = reqVO.getMutedSeconds() == 0 ? ImGroupMemberDO.PERMANENT_MUTE_END_TIME : LocalDateTime.now().plusSeconds(reqVO.getMutedSeconds()); groupMemberService.updateGroupMemberMuteEndTime(reqVO.getId(), reqVO.getUserId(), muteEndTime); // 3. 广播通知 groupMessageService.sendGroupMessage(userId, ImGroupMessageSendDTO.ofGroupMemberMuted(reqVO.getId(), userId, reqVO.getUserId(), muteEndTime)); } @Override @Transactional(rollbackFor = Exception.class) public void cancelMuteMember(Long userId, ImGroupCancelMuteMemberReqVO reqVO) { // 1.1 校验群存在且未封禁 validateGroupExists(reqVO.getId()); // 1.2 校验操作人和目标都在群中 ImGroupMemberDO operatorMember = groupMemberService.validateMemberInGroup(reqVO.getId(), userId); ImGroupMemberDO targetMember = groupMemberService.validateMemberInGroup(reqVO.getId(), reqVO.getUserId()); // 1.3 三档权限校验 validateMutePermission(operatorMember, targetMember); // 2. 取消禁言(清空 muteEndTime) groupMemberService.updateGroupMemberMuteEndTime(reqVO.getId(), reqVO.getUserId(), null); // 3. 广播通知 groupMessageService.sendGroupMessage(userId, ImGroupMessageSendDTO.ofGroupMemberCancelMuted(reqVO.getId(), userId, reqVO.getUserId())); } /** * 三档分层禁言权限校验 */ private void validateMutePermission(ImGroupMemberDO operator, ImGroupMemberDO target) { // 普通成员不能禁言任何人 if (!ImGroupMemberRoleEnum.isOwnerOrAdmin(operator.getRole())) { throw exception(GROUP_NOT_OWNER_OR_ADMIN); } // 群主不可被禁言 if (ImGroupMemberRoleEnum.isOwner(target.getRole())) { throw exception(GROUP_MUTE_OWNER_DENIED); } // 管理员不能禁言其他管理员 if (ImGroupMemberRoleEnum.isAdmin(target.getRole()) && !ImGroupMemberRoleEnum.isOwner(operator.getRole())) { throw exception(GROUP_MUTE_ADMIN_DENIED); } } private ImGroupServiceImpl getSelf() { return SpringUtil.getBean(getClass()); } /** * 根据用户编号集合,拼接用户昵称字符串(逗号分隔) * * @param userIds 用户编号集合 * @return 昵称字符串,如 "张三,李四" */ private String getUserNicknames(Collection userIds) { Map userMap = adminUserApi.getUserMap(userIds); return userIds.stream() .map(id -> userMap.containsKey(id) ? userMap.get(id).getNickname() : String.valueOf(id)) .collect(Collectors.joining(",")); } }