package cn.iocoder.yudao.module.im.service.group; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.ListUtil; import cn.hutool.core.util.ObjUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberUpdateReqVO; import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupMemberMapper; import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum; import cn.iocoder.yudao.module.im.service.message.ImGroupMessageService; import cn.iocoder.yudao.module.im.service.message.dto.ImGroupMessageSendDTO; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.context.annotation.Lazy; import org.springframework.dao.DuplicateKeyException; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import java.time.LocalDateTime; import java.util.*; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; import static cn.iocoder.yudao.module.im.dal.redis.RedisKeyConstants.GROUP_MEMBER_IDS; import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.GROUP_MEMBER_NOT_IN_GROUP; /** * 群成员 Service 实现类 * * @author 芋道源码 */ @Slf4j @Service @Validated public class ImGroupMemberServiceImpl implements ImGroupMemberService { @Resource private ImGroupMemberMapper groupMemberMapper; @Resource @Lazy // 避免循环依赖 private ImGroupMessageService groupMessageService; @Override public ImGroupMemberDO getGroupMember(Long id) { return groupMemberMapper.selectById(id); } @Override public ImGroupMemberDO getGroupMember(Long groupId, Long userId) { return groupMemberMapper.selectByGroupIdAndUserId(groupId, userId); } @Override public List getGroupMembers(Long groupId, Collection userIds) { if (CollUtil.isEmpty(userIds)) { return Collections.emptyList(); } return groupMemberMapper.selectListByGroupIdAndUserIds(groupId, userIds); } @Override public List getGroupMemberListByGroupId(Long groupId) { return groupMemberMapper.selectListByGroupId(groupId); } @Override public List getActiveGroupMemberListByGroupId(Long groupId) { return groupMemberMapper.selectListByGroupIdAndStatus(groupId, CommonStatusEnum.ENABLE.getStatus()); } @Override public List getGroupMemberListByOwnerAndAdmin(Long groupId) { return groupMemberMapper.selectListByGroupIdAndStatusAndRoles(groupId, CommonStatusEnum.ENABLE.getStatus(), ListUtil.of(ImGroupMemberRoleEnum.OWNER.getRole(), ImGroupMemberRoleEnum.ADMIN.getRole())); } /** * 只缓存 userId 列表而非整个 {@link ImGroupMemberDO},理由: *
    *
  • 体积小:500 人群约 4KB,失效/序列化成本低;
  • *
  • 失效面窄:仅 {@link #addGroupMember}/{@link #addGroupMembers}/ * {@link #removeGroupMember}/{@link #removeGroupMembers}/{@link #removeGroupMembersByGroupId} * 这类影响成员集合的写操作需要失效; * {@link #updateGroupMember}(昵称/备注/免打扰)不修改集合成员,不需要失效。
  • *
*/ @Override @Cacheable(cacheNames = GROUP_MEMBER_IDS, key = "#groupId") public List getActiveGroupMemberUserIdsByGroupId(Long groupId) { List members = groupMemberMapper.selectListByGroupIdAndStatus( groupId, CommonStatusEnum.ENABLE.getStatus()); return convertList(members, ImGroupMemberDO::getUserId); } @Override public List getActiveGroupMemberListByUserId(Long userId) { return groupMemberMapper.selectListByUserIdAndStatus(userId, CommonStatusEnum.ENABLE.getStatus()); } @Override public List getGroupMemberListByUserId(Long userId) { return groupMemberMapper.selectListByUserId(userId); } @Override public ImGroupMemberDO addGroupMember(Long groupId, Long userId) { return addGroupMember(groupId, userId, ImGroupMemberRoleEnum.NORMAL.getRole(), null, null); } @Override public ImGroupMemberDO addGroupMember(Long groupId, Long userId, Integer role) { return addGroupMember(groupId, userId, role, null, null); } /** * 并发安全:依靠 im_group_member 表的唯一索引 uk_im_group_member_group_user(group_id, user_id) 保证幂等, * 当并发 insert 触发 {@link DuplicateKeyException} 时降级为 select + update。 *

* 重置旧成员行时强制重置 role / addSource / inviterUserId / quitTime / muteEndTime */ @Override @CacheEvict(cacheNames = GROUP_MEMBER_IDS, key = "#groupId") public ImGroupMemberDO addGroupMember(Long groupId, Long userId, Integer role, Integer addSource, Long inviterUserId) { LocalDateTime now = LocalDateTime.now(); // 情况一:已存在记录 → 重置或跳过 ImGroupMemberDO exists = groupMemberMapper.selectByGroupIdAndUserId(groupId, userId); if (exists != null) { if (CommonStatusEnum.isDisable(exists.getStatus())) { groupMemberMapper.updateRejoinFields(exists.getId(), CommonStatusEnum.ENABLE.getStatus(), now, role, addSource, inviterUserId); exists.setStatus(CommonStatusEnum.ENABLE.getStatus()).setJoinTime(now).setRole(role) .setAddSource(addSource).setInviterUserId(inviterUserId) .setQuitTime(null).setMuteEndTime(null); } return exists; } // 情况二:新增成员 ImGroupMemberDO member = new ImGroupMemberDO() .setGroupId(groupId).setUserId(userId) .setStatus(CommonStatusEnum.ENABLE.getStatus()).setJoinTime(now) .setRole(role).setAddSource(addSource).setInviterUserId(inviterUserId); try { groupMemberMapper.insert(member); return member; } catch (DuplicateKeyException e) { // 并发场景:另一个请求已先一步插入,且其插入的必然是 ENABLE 状态(DISABLE 场景在上方分支已处理),查询返回 log.warn("[addGroupMember][groupId({}) userId({}) 并发插入冲突,查询返回]", groupId, userId); return groupMemberMapper.selectByGroupIdAndUserId(groupId, userId); } } @Override public void addGroupMembers(Long groupId, Collection userIds) { addGroupMembers(groupId, userIds, null, null); } @Override @CacheEvict(cacheNames = GROUP_MEMBER_IDS, key = "#groupId") public void addGroupMembers(Long groupId, Collection userIds, Integer addSource, Long inviterUserId) { LocalDateTime now = LocalDateTime.now(); Integer role = ImGroupMemberRoleEnum.NORMAL.getRole(); // 1.1 查询已有记录(含已退群的 DISABLE 记录) List existMembers = groupMemberMapper.selectListByGroupIdAndUserIds(groupId, userIds); Map existMap = convertMap(existMembers, ImGroupMemberDO::getUserId); // 1.2 分类:已有记录 → UPDATE,新成员 → INSERT List inserts = new ArrayList<>(); List updates = new ArrayList<>(); for (Long userId : userIds) { ImGroupMemberDO exist = existMap.get(userId); if (exist == null) { inserts.add(new ImGroupMemberDO().setGroupId(groupId).setUserId(userId) .setStatus(CommonStatusEnum.ENABLE.getStatus()).setRole(role).setJoinTime(now) .setAddSource(addSource).setInviterUserId(inviterUserId)); } else if (CommonStatusEnum.DISABLE.getStatus().equals(exist.getStatus())) { updates.add(new ImGroupMemberDO().setId(exist.getId()) .setStatus(CommonStatusEnum.ENABLE.getStatus()).setRole(role).setJoinTime(now) .setAddSource(addSource).setInviterUserId(inviterUserId)); } } // 2.1 先做 update,update 没有并发冲突风险 if (CollUtil.isNotEmpty(updates)) { for (ImGroupMemberDO update : updates) { groupMemberMapper.updateRejoinFields(update.getId(), update.getStatus(), update.getJoinTime(), update.getRole(), update.getAddSource(), update.getInviterUserId()); } } // 2.2 批量 insert。并发场景下若其它请求已先一步插入同一 (groupId, userId), // 会触发唯一索引冲突,此时降级为逐个 addGroupMember(利用其兜底逻辑幂等处理)。 if (CollUtil.isNotEmpty(inserts)) { try { groupMemberMapper.insertBatch(inserts); } catch (DuplicateKeyException e) { log.warn("[addGroupMembers][groupId({}) userIds({}) 批量插入冲突,降级为逐个处理]", groupId, userIds); for (ImGroupMemberDO insert : inserts) { addGroupMember(groupId, insert.getUserId(), role, addSource, inviterUserId); } } } } @Override public ImGroupMemberDO validateMemberInGroup(Long groupId, Long userId) { ImGroupMemberDO member = groupMemberMapper.selectByGroupIdAndUserId(groupId, userId); if (member == null || CommonStatusEnum.DISABLE.getStatus().equals(member.getStatus())) { throw exception(GROUP_MEMBER_NOT_IN_GROUP); } return member; } @Override public void validateMembersInGroup(Long groupId, Collection userIds) { if (CollUtil.isEmpty(userIds)) { return; } // 一次性拉取目标 userId 的成员记录,仅保留活跃状态 List members = groupMemberMapper.selectListByGroupIdAndUserIds(groupId, userIds); Set activeUserIds = convertSet(members, ImGroupMemberDO::getUserId, member -> CommonStatusEnum.ENABLE.getStatus().equals(member.getStatus())); // 任一 userId 不在活跃集合即抛 for (Long userId : userIds) { if (!activeUserIds.contains(userId)) { throw exception(GROUP_MEMBER_NOT_IN_GROUP); } } } @Override public int updateGroupMemberRole(Long groupId, Collection userIds, Integer role) { if (CollUtil.isEmpty(userIds)) { return 0; } return groupMemberMapper.updateListByGroupIdAndUserIds(groupId, userIds, new ImGroupMemberDO().setRole(role)); } @Override public Long getGroupMemberCountByRole(Long groupId, Integer role) { return groupMemberMapper.selectCountByGroupIdAndRoleAndStatus( groupId, role, CommonStatusEnum.ENABLE.getStatus()); } @Override public void updateGroupMember(Long userId, ImGroupMemberUpdateReqVO updateReqVO) { Long groupId = updateReqVO.getGroupId(); // 1. 校验是群的有效成员 ImGroupMemberDO member = validateMemberInGroup(groupId, userId); // 2. 更新群成员信息 ImGroupMemberDO updateObj = BeanUtils.toBean(updateReqVO, ImGroupMemberDO.class) .setId(member.getId()); groupMemberMapper.updateById(updateObj); // 3.1 displayUserName 是公开字段,单独走 GROUP_MEMBER_NICKNAME_UPDATE 在线同步给全员;空串视为「清空昵称」也要同步;与旧值相同跳过 if (updateReqVO.getDisplayUserName() != null && ObjUtil.notEqual(updateReqVO.getDisplayUserName(), member.getDisplayUserName())) { groupMessageService.sendGroupMessage(userId, ImGroupMessageSendDTO.ofGroupMemberNicknameUpdate( groupId, userId, updateReqVO.getDisplayUserName())); } // 3.2 silent / groupRemark 是个人字段,仅推自己做多端同步;与旧值都相同跳过 boolean silentChanged = updateReqVO.getSilent() != null && ObjUtil.notEqual(updateReqVO.getSilent(), member.getSilent()); boolean groupRemarkChanged = updateReqVO.getGroupRemark() != null && ObjUtil.notEqual(updateReqVO.getGroupRemark(), member.getGroupRemark()); if (silentChanged || groupRemarkChanged) { groupMessageService.sendGroupMessage(userId, ListUtil.of(userId), ImGroupMessageSendDTO.ofGroupMemberSettingUpdate( groupId, userId, updateReqVO.getSilent(), updateReqVO.getGroupRemark())); } } @Override @CacheEvict(cacheNames = GROUP_MEMBER_IDS, key = "#groupId") public void removeGroupMember(Long groupId, Long userId) { // 1. 校验是群的有效成员 ImGroupMemberDO member = validateMemberInGroup(groupId, userId); // 2. 更新为退群状态 groupMemberMapper.updateById(new ImGroupMemberDO().setId(member.getId()) .setStatus(CommonStatusEnum.DISABLE.getStatus()) .setQuitTime(LocalDateTime.now())); } @Override @CacheEvict(cacheNames = GROUP_MEMBER_IDS, key = "#groupId") public void removeGroupMembers(Long groupId, Collection userIds) { groupMemberMapper.updateByGroupIdAndUserIdsAndStatus(groupId, userIds, CommonStatusEnum.ENABLE.getStatus(), new ImGroupMemberDO().setStatus(CommonStatusEnum.DISABLE.getStatus()) .setQuitTime(LocalDateTime.now())); } @Override @CacheEvict(cacheNames = GROUP_MEMBER_IDS, key = "#groupId") public void removeGroupMembersByGroupId(Long groupId) { groupMemberMapper.updateByGroupIdAndStatus(groupId, CommonStatusEnum.ENABLE.getStatus(), new ImGroupMemberDO().setStatus(CommonStatusEnum.DISABLE.getStatus()) .setQuitTime(LocalDateTime.now())); } @Override public Map getActiveMemberCountMap(Collection groupIds) { return groupMemberMapper.selectCountMapByGroupIdsAndStatus(groupIds, CommonStatusEnum.ENABLE.getStatus()); } @Override public void updateGroupMemberMuteEndTime(Long groupId, Long userId, LocalDateTime muteEndTime) { ImGroupMemberDO member = validateMemberInGroup(groupId, userId); if (muteEndTime != null) { // 禁言:直接更新到期时间 groupMemberMapper.updateById(new ImGroupMemberDO().setId(member.getId()).setMuteEndTime(muteEndTime)); } else { // 取消禁言 groupMemberMapper.updateMuteEndTimeNull(member.getId()); } } }