package cn.iocoder.yudao.module.im.service.group; import cn.hutool.core.collection.CollUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.im.controller.admin.group.vo.request.ImGroupRequestApplyReqVO; import cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.ImGroupRequestManagerPageReqVO; 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.group.ImGroupRequestDO; import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupRequestMapper; 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.group.ImGroupRequestHandleResultEnum; import cn.iocoder.yudao.module.im.enums.ImContentTypeEnum; import cn.iocoder.yudao.module.im.enums.ImConversationTypeEnum; import cn.iocoder.yudao.module.im.service.message.ImGroupMessageService; import cn.iocoder.yudao.module.im.service.message.dto.ImGroupMessageSendDTO; import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService; import cn.iocoder.yudao.module.im.service.websocket.notification.group.BaseGroupNotification; import cn.iocoder.yudao.module.im.service.websocket.notification.group.GroupRequestApprovedNotification; import cn.iocoder.yudao.module.im.service.websocket.notification.group.GroupRequestReceivedNotification; import cn.iocoder.yudao.module.im.service.websocket.notification.group.GroupRequestRejectedNotification; import cn.iocoder.yudao.module.system.api.user.AdminUserApi; import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Lazy; import org.springframework.dao.DuplicateKeyException; 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 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.convertSet; import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*; /** * IM 加群申请 Service 实现类 * * @author 芋道源码 */ @Slf4j @Service @Validated public class ImGroupRequestServiceImpl implements ImGroupRequestService { @Resource private ImGroupRequestMapper groupRequestMapper; @Resource @Lazy // 避免循环依赖 private ImGroupService groupService; @Resource @Lazy // 避免循环依赖 private ImGroupMemberService groupMemberService; @Resource @Lazy // 避免循环依赖 private ImGroupMessageService groupMessageService; @Resource private ImWebSocketService websocketService; @Resource private AdminUserApi adminUserApi; @Override @Transactional(rollbackFor = Exception.class) public ImGroupRequestDO applyJoinGroup(Long userId, ImGroupRequestApplyReqVO reqVO) { Long groupId = reqVO.getGroupId(); // 1.1 校验群存在 + 未封禁 / 未解散 ImGroupDO group = groupService.validateGroupExists(groupId); // 1.2 校验未在群中 ImGroupMemberDO member = groupMemberService.getGroupMember(groupId, userId); if (member != null && !CommonStatusEnum.DISABLE.getStatus().equals(member.getStatus())) { throw exception(GROUP_REQUEST_ALREADY_MEMBER); } // 2. 情况一:群未开启审批,直接入群;写群成员留痕 + 推 1510 全员广播;不落申请记录 if (!Boolean.TRUE.equals(group.getJoinApproval())) { // 入群前校验人数上限 groupService.validateMemberCountLimit(groupId, 1); // 写群成员;addSource 来自前端入口(搜索 / 二维码 / 分享链接),inviterUserId=null(主动申请) groupMemberService.addGroupMember(groupId, userId, ImGroupMemberRoleEnum.NORMAL.getRole(), reqVO.getAddSource(), null); // 推 1510 给全员;payload 含进群者 + 来源,前端按 entrantUserId 局部插入新成员 groupMessageService.sendGroupMessage(userId, ImGroupMessageSendDTO.ofGroupMemberEnter(groupId, userId, reqVO.getAddSource())); return null; } // 3. 情况二:群开启了审批,创建或复用一条主动申请记录 ImGroupRequestDO request = createOrResetApplyRequest(groupId, userId, reqVO); // 4. 1503 定向推群主 + 全部管理员(多端同步);payload 携带申请方昵称 / 头像 AdminUserRespDTO applyUser = adminUserApi.getUser(userId); GroupRequestReceivedNotification payload = buildRequestNotification(group, request, applyUser); for (Long receiverUserId : getGroupMemberListByOwnerAndAdminUserIds(group)) { websocketService.sendNotificationAsync(receiverUserId, ImConversationTypeEnum.NONE.getType(), ImContentTypeEnum.GROUP_REQUEST_RECEIVED.getType(), payload); } return request; } @Override @Transactional(rollbackFor = Exception.class) public void agreeGroupRequest(Long userId, Long requestId) { // 1.1 校验申请存在 + 未处理 + 操作人是 owner / admin ImGroupRequestDO request = validateRequestForHandle(userId, requestId); // 1.2 复核群当前状态:拒绝在封禁 / 解散的群继续放人 groupService.validateGroupExists(request.getGroupId()); // 1.3 复核申请人是否已在群中;幂等避免重复广播 1509 / 1510 入群事件 ImGroupMemberDO applicant = groupMemberService.getGroupMember(request.getGroupId(), request.getUserId()); if (applicant != null && CommonStatusEnum.ENABLE.getStatus().equals(applicant.getStatus())) { throw exception(GROUP_REQUEST_ALREADY_MEMBER); } // 2. 入群前校验人数上限;群已满抛错让操作人选择拒绝 groupService.validateMemberCountLimit(request.getGroupId(), 1); // 3. 乐观锁推进状态 LocalDateTime now = LocalDateTime.now(); ImGroupRequestDO updateObj = new ImGroupRequestDO() .setHandleResult(ImGroupRequestHandleResultEnum.AGREED.getResult()) .setHandleUserId(userId).setHandleTime(now); int affected = groupRequestMapper.updateByIdAndHandleResult(request.getId(), ImGroupRequestHandleResultEnum.UNHANDLED.getResult(), updateObj); if (affected == 0) { throw exception(GROUP_REQUEST_HANDLED); } request.setHandleResult(ImGroupRequestHandleResultEnum.AGREED.getResult()) .setHandleUserId(userId).setHandleTime(now); // 4. 写群成员;addSource / inviterUserId 沿用申请记录上的来源信息 groupMemberService.addGroupMember(request.getGroupId(), request.getUserId(), ImGroupMemberRoleEnum.NORMAL.getRole(), request.getAddSource(), request.getInviterUserId()); // 5.1 1505 定向推送给申请人 + 群主 + 全部管理员(每端单推) GroupRequestApprovedNotification payload = (GroupRequestApprovedNotification) new GroupRequestApprovedNotification() .setRequestId(request.getId()).setGroupId(request.getGroupId()).setUserId(request.getUserId()) .setOperatorUserId(userId); broadcastToOwnerAdminsAndApplicant(request.getGroupId(), request.getUserId(), payload, ImContentTypeEnum.GROUP_REQUEST_APPROVED.getType(), userId); // 5.2 群事件:主动申请 → 1510 自由进群;被邀请 → 1509 成员加入 if (request.getInviterUserId() == null) { groupMessageService.sendGroupMessage(userId, ImGroupMessageSendDTO.ofGroupMemberEnter(request.getGroupId(), request.getUserId(), request.getAddSource())); } else { groupMessageService.sendGroupMessage(userId, ImGroupMessageSendDTO.ofGroupMemberInvite(request.getGroupId(), request.getInviterUserId(), Collections.singleton(request.getUserId()))); } } @Override @Transactional(rollbackFor = Exception.class) public void refuseGroupRequest(Long userId, Long requestId, String handleContent) { // 1. 校验 ImGroupRequestDO request = validateRequestForHandle(userId, requestId); // 2. 乐观锁推进 ImGroupRequestDO updateObj = new ImGroupRequestDO() .setHandleResult(ImGroupRequestHandleResultEnum.REFUSED.getResult()) .setHandleContent(handleContent) .setHandleUserId(userId).setHandleTime(LocalDateTime.now()); int affected = groupRequestMapper.updateByIdAndHandleResult(request.getId(), ImGroupRequestHandleResultEnum.UNHANDLED.getResult(), updateObj); if (affected == 0) { throw exception(GROUP_REQUEST_HANDLED); } // 3. 1506 定向推送给申请人 + 群主 + 全部管理员 GroupRequestRejectedNotification payload = (GroupRequestRejectedNotification) new GroupRequestRejectedNotification() .setRequestId(request.getId()).setGroupId(request.getGroupId()).setUserId(request.getUserId()) .setHandleContent(handleContent).setOperatorUserId(userId); broadcastToOwnerAdminsAndApplicant(request.getGroupId(), request.getUserId(), payload, ImContentTypeEnum.GROUP_REQUEST_REJECTED.getType(), userId); } @Override @Transactional(rollbackFor = Exception.class) public void createInviteRequestList(Long groupId, Long inviterUserId, Collection invitedUserIds) { if (CollUtil.isEmpty(invitedUserIds)) { return; } ImGroupDO group = groupService.validateGroupExists(groupId); Integer inviteSource = ImGroupAddSourceEnum.INVITE.getSource(); // 1. 逐条创建或复用邀请申请 List requests = convertList(invitedUserIds, userId -> createOrResetInviteRequest(groupId, inviterUserId, userId, inviteSource)); // 2. 推 1503 给群主 + 全部管理员;多端同步;每条申请单独推一帧 Map userMap = adminUserApi.getUserMap(invitedUserIds); List ownerAndAdmins = getGroupMemberListByOwnerAndAdminUserIds(group); for (ImGroupRequestDO request : requests) { AdminUserRespDTO applyUser = userMap.get(request.getUserId()); GroupRequestReceivedNotification payload = buildRequestNotification(group, request, applyUser); for (Long receiverUserId : ownerAndAdmins) { websocketService.sendNotificationAsync(receiverUserId, ImConversationTypeEnum.NONE.getType(), ImContentTypeEnum.GROUP_REQUEST_RECEIVED.getType(), payload); } } } @Override public List getUnhandledRequestListByOwnerOrAdmin(Long userId) { // 1. 找出当前用户作为 OWNER / ADMIN 的所有群 List myMembers = groupMemberService.getActiveGroupMemberListByUserId(userId); Set ownerOrAdminGroupIds = convertSet(myMembers, ImGroupMemberDO::getGroupId, member -> ImGroupMemberRoleEnum.isOwnerOrAdmin(member.getRole())); if (CollUtil.isEmpty(ownerOrAdminGroupIds)) { return Collections.emptyList(); } // 2. 一次拉所有群的未处理申请 return groupRequestMapper.selectListByGroupIdsAndHandleResult( ownerOrAdminGroupIds, ImGroupRequestHandleResultEnum.UNHANDLED.getResult()); } @Override public List pullGroupRequestList(Long userId, Long lastUpdateTime, Long lastId, Integer limit) { // 1. 找出当前用户作为 OWNER / ADMIN 的所有群 List myMembers = groupMemberService.getActiveGroupMemberListByUserId(userId); Set ownerOrAdminGroupIds = convertSet(myMembers, ImGroupMemberDO::getGroupId, member -> ImGroupMemberRoleEnum.isOwnerOrAdmin(member.getRole())); if (CollUtil.isEmpty(ownerOrAdminGroupIds)) { return Collections.emptyList(); } // 2. 按游标增量拉取这些群下的申请 return groupRequestMapper.selectPullListByGroupIds(ownerOrAdminGroupIds, lastUpdateTime, lastId, limit); } @Override public List getGroupRequestListByGroupId(Long userId, Long groupId) { // 1. 校验群存在 + 当前用户是群主 / 管理员 groupService.validateGroupExists(groupId); ImGroupMemberDO operator = groupMemberService.validateMemberInGroup(groupId, userId); if (!ImGroupMemberRoleEnum.isOwnerOrAdmin(operator.getRole())) { throw exception(GROUP_REQUEST_NOT_TO_ME); } // 2. 拉取该群下全部申请(含已处理);按 id 倒序,前端首条卡片化展示 return groupRequestMapper.selectListByGroupId(groupId); } @Override public ImGroupRequestDO getGroupRequest(Long id) { return groupRequestMapper.selectById(id); } @Override public PageResult getGroupRequestPage(ImGroupRequestManagerPageReqVO reqVO) { return groupRequestMapper.selectPage(reqVO); } /** * 创建或重置主动加群申请 * * @param groupId 群编号 * @param userId 申请人用户编号 * @param reqVO 申请请求 * @return 申请记录 */ private ImGroupRequestDO createOrResetApplyRequest(Long groupId, Long userId, ImGroupRequestApplyReqVO reqVO) { // 1. 已有申请:覆盖本次申请内容,并重置为未处理 ImGroupRequestDO request = groupRequestMapper.selectByGroupIdAndUserId(groupId, userId); if (request != null) { resetApplyRequest(request, reqVO); return request; } // 2. 无旧申请:创建主动申请记录 request = BeanUtils.toBean(reqVO, ImGroupRequestDO.class) .setUserId(userId).setInviterUserId(null) .setHandleResult(ImGroupRequestHandleResultEnum.UNHANDLED.getResult()); try { groupRequestMapper.insert(request); return request; } catch (DuplicateKeyException ex) { // 3. 唯一键冲突:回查并复用并发写入的记录 request = groupRequestMapper.selectByGroupIdAndUserId(groupId, userId); if (request == null) { throw ex; } resetApplyRequest(request, reqVO); return request; } } /** * 创建或重置邀请加群申请 * * @param groupId 群编号 * @param inviterUserId 邀请人用户编号 * @param userId 被邀请人用户编号 * @param inviteSource 邀请来源 * @return 申请记录 */ private ImGroupRequestDO createOrResetInviteRequest(Long groupId, Long inviterUserId, Long userId, Integer inviteSource) { // 1. 已有申请:覆盖邀请人和来源,并重置为未处理 ImGroupRequestDO request = groupRequestMapper.selectByGroupIdAndUserId(groupId, userId); if (request != null) { resetInviteRequest(request, inviterUserId, inviteSource); return request; } // 2. 无旧申请:创建邀请申请记录 request = new ImGroupRequestDO().setGroupId(groupId).setUserId(userId).setInviterUserId(inviterUserId) .setAddSource(inviteSource).setHandleResult(ImGroupRequestHandleResultEnum.UNHANDLED.getResult()); try { groupRequestMapper.insert(request); return request; } catch (DuplicateKeyException ex) { // 3. 唯一键冲突:回查并复用并发写入的记录 request = groupRequestMapper.selectByGroupIdAndUserId(groupId, userId); if (request == null) { throw ex; } resetInviteRequest(request, inviterUserId, inviteSource); return request; } } /** * 重置主动加群申请 * * @param request 申请记录 * @param reqVO 申请请求 */ private void resetApplyRequest(ImGroupRequestDO request, ImGroupRequestApplyReqVO reqVO) { // 1. 更新申请内容、来源和处理状态 LocalDateTime now = LocalDateTime.now(); groupRequestMapper.updateApplyByIdReset(request.getId(), reqVO.getApplyContent(), reqVO.getAddSource(), now); // 2. 同步内存对象,后续通知构建直接复用 request.setApplyContent(reqVO.getApplyContent()).setAddSource(reqVO.getAddSource()) .setHandleResult(ImGroupRequestHandleResultEnum.UNHANDLED.getResult()) .setInviterUserId(null).setHandleUserId(null) .setHandleContent(null).setHandleTime(null).setUpdateTime(now); } /** * 重置邀请加群申请 * * @param request 申请记录 * @param inviterUserId 邀请人用户编号 * @param inviteSource 邀请来源 */ private void resetInviteRequest(ImGroupRequestDO request, Long inviterUserId, Integer inviteSource) { // 1. 更新邀请人、来源和处理状态 LocalDateTime now = LocalDateTime.now(); groupRequestMapper.updateInviteByIdReset(request.getId(), inviterUserId, inviteSource, now); // 2. 同步内存对象,后续通知构建直接复用 request.setInviterUserId(inviterUserId).setAddSource(inviteSource) .setHandleResult(ImGroupRequestHandleResultEnum.UNHANDLED.getResult()) .setHandleUserId(null).setHandleContent(null).setHandleTime(null) .setUpdateTime(now); } /** * 校验申请可被「当前用户」处理:申请存在 + 未处理 + 操作人是群主 / 管理员 */ private ImGroupRequestDO validateRequestForHandle(Long userId, Long requestId) { ImGroupRequestDO request = groupRequestMapper.selectById(requestId); if (request == null) { throw exception(GROUP_REQUEST_NOT_EXISTS); } if (!ImGroupRequestHandleResultEnum.isUnhandled(request.getHandleResult())) { throw exception(GROUP_REQUEST_HANDLED); } ImGroupMemberDO operator = groupMemberService.validateMemberInGroup(request.getGroupId(), userId); if (!ImGroupMemberRoleEnum.isOwnerOrAdmin(operator.getRole())) { throw exception(GROUP_REQUEST_NOT_TO_ME); } return request; } /** * 构建 1503 通知 payload;聚合申请方昵称 / 头像供前端直接渲染 */ private GroupRequestReceivedNotification buildRequestNotification(ImGroupDO group, ImGroupRequestDO request, AdminUserRespDTO applyUser) { Long operatorUserId = request.getInviterUserId() != null ? request.getInviterUserId() : request.getUserId(); GroupRequestReceivedNotification payload = (GroupRequestReceivedNotification) new GroupRequestReceivedNotification() .setRequestId(request.getId()).setGroupId(group.getId()).setUserId(request.getUserId()) .setInviterUserId(request.getInviterUserId()) .setApplyContent(request.getApplyContent()).setAddSource(request.getAddSource()) .setOperatorUserId(operatorUserId); if (applyUser != null) { payload.setUserNickname(applyUser.getNickname()).setUserAvatar(applyUser.getAvatar()); } return payload; } /** * 1505 / 1506 受众:申请人 + 群主 + 全部管理员 */ private void broadcastToOwnerAdminsAndApplicant(Long groupId, Long applicantUserId, BaseGroupNotification payload, Integer messageType, Long operatorUserId) { ImGroupDO group = groupService.getGroup(groupId); if (group == null) { return; } Set receivers = new LinkedHashSet<>(getGroupMemberListByOwnerAndAdminUserIds(group)); receivers.add(applicantUserId); for (Long receiverUserId : receivers) { websocketService.sendNotificationAsync(receiverUserId, ImConversationTypeEnum.NONE.getType(), messageType, payload); } } /** * 列出群主 + 全部管理员的用户编号 * * @param group 群信息 * @return 群主 + 全部管理员的用户编号列表 */ private List getGroupMemberListByOwnerAndAdminUserIds(ImGroupDO group) { return convertList(groupMemberService.getGroupMemberListByOwnerAndAdmin(group.getId()), ImGroupMemberDO::getUserId); } }