diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupRequestController.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupRequestController.java index d6a0489..440e30b 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupRequestController.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupRequestController.java @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.im.controller.admin.group; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ObjUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.util.collection.MapUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; @@ -117,10 +118,14 @@ public class ImGroupRequestController { return success(CollUtil.getFirst(buildVOList(Collections.singletonList(request)))); } - /** 当前用户是否该群的群主 / 管理员 */ + /** + * 当前用户是否该群的有效群主 / 管理员 + */ private boolean isGroupOwnerOrAdmin(Long groupId, Long userId) { ImGroupMemberDO member = groupMemberService.getGroupMember(groupId, userId); - return member != null && ImGroupMemberRoleEnum.isOwnerOrAdmin(member.getRole()); + return member != null + && !CommonStatusEnum.DISABLE.getStatus().equals(member.getStatus()) + && ImGroupMemberRoleEnum.isOwnerOrAdmin(member.getRole()); } /** 申请记录列表批量转 VO + 关联回填用户 / 群信息 */ diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/group/ImGroupRequestMapper.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/group/ImGroupRequestMapper.java index bd10db0..ff93f3b 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/group/ImGroupRequestMapper.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/group/ImGroupRequestMapper.java @@ -25,16 +25,25 @@ public interface ImGroupRequestMapper extends BaseMapperX { .eq(ImGroupRequestDO::getUserId, userId)); } + default List selectListByGroupIdAndUserIds(Long groupId, Collection userIds) { + return selectList(new LambdaQueryWrapperX() + .eq(ImGroupRequestDO::getGroupId, groupId) + .in(ImGroupRequestDO::getUserId, userIds)); + } + default List selectListByGroupIdsAndHandleResult(Collection groupIds, Integer handleResult) { return selectList(new LambdaQueryWrapperX() .in(ImGroupRequestDO::getGroupId, groupIds) .eq(ImGroupRequestDO::getHandleResult, handleResult) + .orderByDesc(ImGroupRequestDO::getUpdateTime) .orderByDesc(ImGroupRequestDO::getId)); } default List selectListByGroupId(Long groupId) { + // 同上,update_time 倒序优先于 id return selectList(new LambdaQueryWrapperX() .eq(ImGroupRequestDO::getGroupId, groupId) + .orderByDesc(ImGroupRequestDO::getUpdateTime) .orderByDesc(ImGroupRequestDO::getId)); } diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupRequestServiceImpl.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupRequestServiceImpl.java index 7136d13..366a5fd 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupRequestServiceImpl.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupRequestServiceImpl.java @@ -11,6 +11,7 @@ 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.message.ImMessageTypeEnum; @@ -99,6 +100,9 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService { // TODO @AI:看看是不是在 basemapperx 里,增加一个 updateXXXX;可以根据传递的 DO,深度更新的方法?应该匹配这个场景的对哇?【主要希望 service 不要出现 mapper 相关的类】 ImGroupRequestDO request = groupRequestMapper.selectByGroupIdAndUserId(groupId, userId); if (request != null) { + // update_time 显式置当前时间:update(null, wrapper) 不会触发 MetaObjectHandler.updateFill; + // 列表查询按 update_time 倒序排,复用旧记录时必须刷新这一列才能让本次重新提交排到最前 + LocalDateTime now = LocalDateTime.now(); groupRequestMapper.update(null, new LambdaUpdateWrapper() .eq(ImGroupRequestDO::getId, request.getId()) .set(ImGroupRequestDO::getApplyContent, reqVO.getApplyContent()) @@ -107,11 +111,12 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService { .set(ImGroupRequestDO::getInviterUserId, null) .set(ImGroupRequestDO::getHandleUserId, null) .set(ImGroupRequestDO::getHandleContent, null) - .set(ImGroupRequestDO::getHandleTime, null)); + .set(ImGroupRequestDO::getHandleTime, null) + .set(ImGroupRequestDO::getUpdateTime, now)); request.setApplyContent(reqVO.getApplyContent()).setAddSource(reqVO.getAddSource()) .setHandleResult(ImGroupRequestHandleResultEnum.UNHANDLED.getResult()) .setInviterUserId(null).setHandleUserId(null) - .setHandleContent(null).setHandleTime(null); + .setHandleContent(null).setHandleTime(null).setUpdateTime(now); } else { request = BeanUtils.toBean(reqVO, ImGroupRequestDO.class) .setUserId(userId).setInviterUserId(null) @@ -206,27 +211,35 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService { ImGroupDO group = groupService.validateGroupExists(groupId); // 1. 每个被邀请人 upsert 一条 inviter_user_id=邀请人 的记录 - // 已有记录(同一对 group_id, user_id 唯一):覆盖 inviter_user_id + 重置 handle_result + 清空旧处理痕迹(走 LambdaUpdateWrapper.set 显式置 null) + // 已有记录(同一对 group_id, user_id 唯一):覆盖 inviter_user_id / add_source + 重置 handle_result + 清空旧处理痕迹(走 LambdaUpdateWrapper.set 显式置 null) + // add_source 强制覆写为 INVITE,避免审批通过后把残留的旧来源回写进群成员留痕 + Integer inviteSource = ImGroupAddSourceEnum.INVITE.getSource(); List requests = new ArrayList<>(invitedUserIds.size()); // TODO @AI:看看是不是在 basemapperx 里,增加一个 updateXXXX;可以根据传递的 DO,深度更新的方法?应该匹配这个场景的对哇?【主要希望 service 不要出现 mapper 相关的类】 + // 同 applyJoinGroup:update(null, wrapper) 不走 MetaObjectHandler.updateFill,复用旧记录必须显式刷 update_time,否则按 update_time 倒序排不到最前 + LocalDateTime now = LocalDateTime.now(); for (Long invitedUserId : invitedUserIds) { ImGroupRequestDO existing = groupRequestMapper.selectByGroupIdAndUserId(groupId, invitedUserId); if (existing != null) { groupRequestMapper.update(null, new LambdaUpdateWrapper() .eq(ImGroupRequestDO::getId, existing.getId()) .set(ImGroupRequestDO::getInviterUserId, inviterUserId) + .set(ImGroupRequestDO::getAddSource, inviteSource) .set(ImGroupRequestDO::getHandleResult, ImGroupRequestHandleResultEnum.UNHANDLED.getResult()) .set(ImGroupRequestDO::getHandleUserId, null) .set(ImGroupRequestDO::getHandleContent, null) - .set(ImGroupRequestDO::getHandleTime, null)); - existing.setInviterUserId(inviterUserId) + .set(ImGroupRequestDO::getHandleTime, null) + .set(ImGroupRequestDO::getUpdateTime, now)); + existing.setInviterUserId(inviterUserId).setAddSource(inviteSource) .setHandleResult(ImGroupRequestHandleResultEnum.UNHANDLED.getResult()) - .setHandleUserId(null).setHandleContent(null).setHandleTime(null); + .setHandleUserId(null).setHandleContent(null).setHandleTime(null) + .setUpdateTime(now); requests.add(existing); continue; } ImGroupRequestDO insert = new ImGroupRequestDO() .setGroupId(groupId).setUserId(invitedUserId).setInviterUserId(inviterUserId) + .setAddSource(inviteSource) .setHandleResult(ImGroupRequestHandleResultEnum.UNHANDLED.getResult()); groupRequestMapper.insert(insert); requests.add(insert); diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImpl.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImpl.java index 8bddb6c..722d88e 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImpl.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImpl.java @@ -194,14 +194,15 @@ public class ImGroupServiceImpl implements ImGroupService { // 1.1 校验群存在 + 当前用户是群成员;同时拿到 role 供下面审批分支判断 ImGroupDO group = validateGroupExists(groupId); ImGroupMemberDO operator = groupMemberService.validateMemberInGroup(groupId, userId); - // 1.2 排除已在群中的用户 + // 1.2 入参去重 + 排除已在群中的用户 List activeMembers = groupMemberService.getActiveGroupMemberListByGroupId(groupId); - activeMembers.forEach(member -> inviteReqVO.getMemberUserIds().remove(member.getUserId())); - if (CollUtil.isEmpty(inviteReqVO.getMemberUserIds())) { + Set activeMemberUserIds = convertSet(activeMembers, ImGroupMemberDO::getUserId); + List memberUserIds = new ArrayList<>(new LinkedHashSet<>(inviteReqVO.getMemberUserIds())); + memberUserIds.removeAll(activeMemberUserIds); + if (CollUtil.isEmpty(memberUserIds)) { return; } // 1.3 校验被邀请人都是当前用户的好友 - List memberUserIds = inviteReqVO.getMemberUserIds(); List friends = friendService.getActiveFriendList(userId, memberUserIds); Set friendUserIds = convertSet(friends, ImFriendDO::getFriendUserId); Collection notFriendUserIds = CollUtil.subtract(memberUserIds, friendUserIds); @@ -214,7 +215,7 @@ public class ImGroupServiceImpl implements ImGroupService { } // 2. 情况一:群开启审批 + 邀请人是普通成员,落 group_request 等群主 / 管理员处理 - // 群主 / 管理员邀请视同已审批通过,绕过审批直接拉人进群(对齐 OpenIM / 微信) + // 群主 / 管理员邀请,直接拉人进群 if (Boolean.TRUE.equals(group.getJoinApproval()) && !ImGroupMemberRoleEnum.isOwnerOrAdmin(operator.getRole())) { groupRequestService.createInviteRequestList(groupId, userId, memberUserIds); @@ -227,7 +228,7 @@ public class ImGroupServiceImpl implements ImGroupService { // 4. 发 GROUP_MEMBER_INVITE 通知给全员;本地拼 receivers(已查的 active + 新邀请)避免缓存刚 evict 后强制走 DB Set allReceivers = new HashSet<>(memberUserIds); - activeMembers.forEach(member -> allReceivers.add(member.getUserId())); + allReceivers.addAll(activeMemberUserIds); groupMessageService.sendGroupMessage(userId, allReceivers, ImGroupMessageSendDTO.ofGroupMemberInvite(groupId, userId, memberUserIds)); } diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupRequestApprovedNotification.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupRequestApprovedNotification.java index 4bb5892..7965192 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupRequestApprovedNotification.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupRequestApprovedNotification.java @@ -6,8 +6,7 @@ import lombok.EqualsAndHashCode; /** * 入群申请被同意通知 *

- * 走私聊通道定向推送给申请人 + 群主 + 全部管理员; - * 申请人侧据此弹 toast、列表项更新;admin 侧据此把 unhandledCount 减 1 并从未处理列表移除 + * 走私聊通道定向推送给申请人 + 群主 + 全部管理员;admin 侧据此把 unhandledCount 减 1 并从未处理列表移除 */ @EqualsAndHashCode(callSuper = true) @Data diff --git a/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/group/ImGroupRequestServiceImplTest.java b/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/group/ImGroupRequestServiceImplTest.java index 09f95f5..5b7672f 100644 --- a/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/group/ImGroupRequestServiceImplTest.java +++ b/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/group/ImGroupRequestServiceImplTest.java @@ -265,8 +265,14 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest { groupRequestService.createInviteRequestList(10L, 1L, List.of(2L, 3L)); // 断言:插入 2 条申请记录 + 推 1503 给 owner(每条 1 帧)共 2 帧 - verify(groupRequestMapper, times(2)).insert(any(ImGroupRequestDO.class)); + ArgumentCaptor captor = ArgumentCaptor.forClass(ImGroupRequestDO.class); + verify(groupRequestMapper, times(2)).insert(captor.capture()); verify(websocketService, times(2)).sendPrivateMessageAsync(anyLong(), any(ImPrivateMessageDTO.class)); + // 断言:每条记录 inviterUserId=1 + addSource=INVITE,避免审批通过后回写群成员留痕的来源为空 / 脏带旧值 + captor.getAllValues().forEach(insert -> { + assertEquals(1L, insert.getInviterUserId()); + assertEquals(ImGroupAddSourceEnum.INVITE.getSource(), insert.getAddSource()); + }); } private AdminUserRespDTO buildUser(Long id, String nickname) {