Commit ded120902a0ff67e7c13d0c56ea485500405eaf8
1 parent
3b0abfb2
✨ feat(im): 初始化群申请 v0.4:第五把 review(多轮 finding 修复 + 通知静默化)
- 邀请路径写 addSource=INVITE;群主 / 管理员邀请绕过审批;inviteGroupMember 入参去重 - getGroupRequest 越权校验加成员有效状态判断;新增 list-by-group 接口 - 申请列表按 update_time 倒序,update(null, wrapper) 路径手动刷 updateTime - addByRequestId 不再 skip 同 id,复用记录刷新并置顶 - GroupRequestListDialog 单群模式订阅 store 增量同步;GroupMemberAddDialog 审批分支文案区分 - ConversationItem 增加 [X 条进群申请] 红字前缀;MessagePanel 顶部胶囊横幅 - 1505 / 1506 通知改静默:同意走群事件渲染系统提示,拒绝不再打扰;清掉 dead inviterUserId 字段
Showing
6 changed files
with
50 additions
and
17 deletions
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; |
| 2 | 2 | |
| 3 | 3 | import cn.hutool.core.collection.CollUtil; |
| 4 | 4 | import cn.hutool.core.util.ObjUtil; |
| 5 | +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; | |
| 5 | 6 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| 6 | 7 | import cn.iocoder.yudao.framework.common.util.collection.MapUtils; |
| 7 | 8 | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
| ... | ... | @@ -117,10 +118,14 @@ public class ImGroupRequestController { |
| 117 | 118 | return success(CollUtil.getFirst(buildVOList(Collections.singletonList(request)))); |
| 118 | 119 | } |
| 119 | 120 | |
| 120 | - /** 当前用户是否该群的群主 / 管理员 */ | |
| 121 | + /** | |
| 122 | + * 当前用户是否该群的有效群主 / 管理员 | |
| 123 | + */ | |
| 121 | 124 | private boolean isGroupOwnerOrAdmin(Long groupId, Long userId) { |
| 122 | 125 | ImGroupMemberDO member = groupMemberService.getGroupMember(groupId, userId); |
| 123 | - return member != null && ImGroupMemberRoleEnum.isOwnerOrAdmin(member.getRole()); | |
| 126 | + return member != null | |
| 127 | + && !CommonStatusEnum.DISABLE.getStatus().equals(member.getStatus()) | |
| 128 | + && ImGroupMemberRoleEnum.isOwnerOrAdmin(member.getRole()); | |
| 124 | 129 | } |
| 125 | 130 | |
| 126 | 131 | /** 申请记录列表批量转 VO + 关联回填用户 / 群信息 */ | ... | ... |
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<ImGroupRequestDO> { |
| 25 | 25 | .eq(ImGroupRequestDO::getUserId, userId)); |
| 26 | 26 | } |
| 27 | 27 | |
| 28 | + default List<ImGroupRequestDO> selectListByGroupIdAndUserIds(Long groupId, Collection<Long> userIds) { | |
| 29 | + return selectList(new LambdaQueryWrapperX<ImGroupRequestDO>() | |
| 30 | + .eq(ImGroupRequestDO::getGroupId, groupId) | |
| 31 | + .in(ImGroupRequestDO::getUserId, userIds)); | |
| 32 | + } | |
| 33 | + | |
| 28 | 34 | default List<ImGroupRequestDO> selectListByGroupIdsAndHandleResult(Collection<Long> groupIds, Integer handleResult) { |
| 29 | 35 | return selectList(new LambdaQueryWrapperX<ImGroupRequestDO>() |
| 30 | 36 | .in(ImGroupRequestDO::getGroupId, groupIds) |
| 31 | 37 | .eq(ImGroupRequestDO::getHandleResult, handleResult) |
| 38 | + .orderByDesc(ImGroupRequestDO::getUpdateTime) | |
| 32 | 39 | .orderByDesc(ImGroupRequestDO::getId)); |
| 33 | 40 | } |
| 34 | 41 | |
| 35 | 42 | default List<ImGroupRequestDO> selectListByGroupId(Long groupId) { |
| 43 | + // 同上,update_time 倒序优先于 id | |
| 36 | 44 | return selectList(new LambdaQueryWrapperX<ImGroupRequestDO>() |
| 37 | 45 | .eq(ImGroupRequestDO::getGroupId, groupId) |
| 46 | + .orderByDesc(ImGroupRequestDO::getUpdateTime) | |
| 38 | 47 | .orderByDesc(ImGroupRequestDO::getId)); |
| 39 | 48 | } |
| 40 | 49 | ... | ... |
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; |
| 11 | 11 | import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; |
| 12 | 12 | import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupRequestDO; |
| 13 | 13 | import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupRequestMapper; |
| 14 | +import cn.iocoder.yudao.module.im.enums.group.ImGroupAddSourceEnum; | |
| 14 | 15 | import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum; |
| 15 | 16 | import cn.iocoder.yudao.module.im.enums.group.ImGroupRequestHandleResultEnum; |
| 16 | 17 | import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum; |
| ... | ... | @@ -99,6 +100,9 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService { |
| 99 | 100 | // TODO @AI:看看是不是在 basemapperx 里,增加一个 updateXXXX;可以根据传递的 DO,深度更新的方法?应该匹配这个场景的对哇?【主要希望 service 不要出现 mapper 相关的类】 |
| 100 | 101 | ImGroupRequestDO request = groupRequestMapper.selectByGroupIdAndUserId(groupId, userId); |
| 101 | 102 | if (request != null) { |
| 103 | + // update_time 显式置当前时间:update(null, wrapper) 不会触发 MetaObjectHandler.updateFill; | |
| 104 | + // 列表查询按 update_time 倒序排,复用旧记录时必须刷新这一列才能让本次重新提交排到最前 | |
| 105 | + LocalDateTime now = LocalDateTime.now(); | |
| 102 | 106 | groupRequestMapper.update(null, new LambdaUpdateWrapper<ImGroupRequestDO>() |
| 103 | 107 | .eq(ImGroupRequestDO::getId, request.getId()) |
| 104 | 108 | .set(ImGroupRequestDO::getApplyContent, reqVO.getApplyContent()) |
| ... | ... | @@ -107,11 +111,12 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService { |
| 107 | 111 | .set(ImGroupRequestDO::getInviterUserId, null) |
| 108 | 112 | .set(ImGroupRequestDO::getHandleUserId, null) |
| 109 | 113 | .set(ImGroupRequestDO::getHandleContent, null) |
| 110 | - .set(ImGroupRequestDO::getHandleTime, null)); | |
| 114 | + .set(ImGroupRequestDO::getHandleTime, null) | |
| 115 | + .set(ImGroupRequestDO::getUpdateTime, now)); | |
| 111 | 116 | request.setApplyContent(reqVO.getApplyContent()).setAddSource(reqVO.getAddSource()) |
| 112 | 117 | .setHandleResult(ImGroupRequestHandleResultEnum.UNHANDLED.getResult()) |
| 113 | 118 | .setInviterUserId(null).setHandleUserId(null) |
| 114 | - .setHandleContent(null).setHandleTime(null); | |
| 119 | + .setHandleContent(null).setHandleTime(null).setUpdateTime(now); | |
| 115 | 120 | } else { |
| 116 | 121 | request = BeanUtils.toBean(reqVO, ImGroupRequestDO.class) |
| 117 | 122 | .setUserId(userId).setInviterUserId(null) |
| ... | ... | @@ -206,27 +211,35 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService { |
| 206 | 211 | ImGroupDO group = groupService.validateGroupExists(groupId); |
| 207 | 212 | |
| 208 | 213 | // 1. 每个被邀请人 upsert 一条 inviter_user_id=邀请人 的记录 |
| 209 | - // 已有记录(同一对 group_id, user_id 唯一):覆盖 inviter_user_id + 重置 handle_result + 清空旧处理痕迹(走 LambdaUpdateWrapper.set 显式置 null) | |
| 214 | + // 已有记录(同一对 group_id, user_id 唯一):覆盖 inviter_user_id / add_source + 重置 handle_result + 清空旧处理痕迹(走 LambdaUpdateWrapper.set 显式置 null) | |
| 215 | + // add_source 强制覆写为 INVITE,避免审批通过后把残留的旧来源回写进群成员留痕 | |
| 216 | + Integer inviteSource = ImGroupAddSourceEnum.INVITE.getSource(); | |
| 210 | 217 | List<ImGroupRequestDO> requests = new ArrayList<>(invitedUserIds.size()); |
| 211 | 218 | // TODO @AI:看看是不是在 basemapperx 里,增加一个 updateXXXX;可以根据传递的 DO,深度更新的方法?应该匹配这个场景的对哇?【主要希望 service 不要出现 mapper 相关的类】 |
| 219 | + // 同 applyJoinGroup:update(null, wrapper) 不走 MetaObjectHandler.updateFill,复用旧记录必须显式刷 update_time,否则按 update_time 倒序排不到最前 | |
| 220 | + LocalDateTime now = LocalDateTime.now(); | |
| 212 | 221 | for (Long invitedUserId : invitedUserIds) { |
| 213 | 222 | ImGroupRequestDO existing = groupRequestMapper.selectByGroupIdAndUserId(groupId, invitedUserId); |
| 214 | 223 | if (existing != null) { |
| 215 | 224 | groupRequestMapper.update(null, new LambdaUpdateWrapper<ImGroupRequestDO>() |
| 216 | 225 | .eq(ImGroupRequestDO::getId, existing.getId()) |
| 217 | 226 | .set(ImGroupRequestDO::getInviterUserId, inviterUserId) |
| 227 | + .set(ImGroupRequestDO::getAddSource, inviteSource) | |
| 218 | 228 | .set(ImGroupRequestDO::getHandleResult, ImGroupRequestHandleResultEnum.UNHANDLED.getResult()) |
| 219 | 229 | .set(ImGroupRequestDO::getHandleUserId, null) |
| 220 | 230 | .set(ImGroupRequestDO::getHandleContent, null) |
| 221 | - .set(ImGroupRequestDO::getHandleTime, null)); | |
| 222 | - existing.setInviterUserId(inviterUserId) | |
| 231 | + .set(ImGroupRequestDO::getHandleTime, null) | |
| 232 | + .set(ImGroupRequestDO::getUpdateTime, now)); | |
| 233 | + existing.setInviterUserId(inviterUserId).setAddSource(inviteSource) | |
| 223 | 234 | .setHandleResult(ImGroupRequestHandleResultEnum.UNHANDLED.getResult()) |
| 224 | - .setHandleUserId(null).setHandleContent(null).setHandleTime(null); | |
| 235 | + .setHandleUserId(null).setHandleContent(null).setHandleTime(null) | |
| 236 | + .setUpdateTime(now); | |
| 225 | 237 | requests.add(existing); |
| 226 | 238 | continue; |
| 227 | 239 | } |
| 228 | 240 | ImGroupRequestDO insert = new ImGroupRequestDO() |
| 229 | 241 | .setGroupId(groupId).setUserId(invitedUserId).setInviterUserId(inviterUserId) |
| 242 | + .setAddSource(inviteSource) | |
| 230 | 243 | .setHandleResult(ImGroupRequestHandleResultEnum.UNHANDLED.getResult()); |
| 231 | 244 | groupRequestMapper.insert(insert); |
| 232 | 245 | requests.add(insert); | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImpl.java
| ... | ... | @@ -194,14 +194,15 @@ public class ImGroupServiceImpl implements ImGroupService { |
| 194 | 194 | // 1.1 校验群存在 + 当前用户是群成员;同时拿到 role 供下面审批分支判断 |
| 195 | 195 | ImGroupDO group = validateGroupExists(groupId); |
| 196 | 196 | ImGroupMemberDO operator = groupMemberService.validateMemberInGroup(groupId, userId); |
| 197 | - // 1.2 排除已在群中的用户 | |
| 197 | + // 1.2 入参去重 + 排除已在群中的用户 | |
| 198 | 198 | List<ImGroupMemberDO> activeMembers = groupMemberService.getActiveGroupMemberListByGroupId(groupId); |
| 199 | - activeMembers.forEach(member -> inviteReqVO.getMemberUserIds().remove(member.getUserId())); | |
| 200 | - if (CollUtil.isEmpty(inviteReqVO.getMemberUserIds())) { | |
| 199 | + Set<Long> activeMemberUserIds = convertSet(activeMembers, ImGroupMemberDO::getUserId); | |
| 200 | + List<Long> memberUserIds = new ArrayList<>(new LinkedHashSet<>(inviteReqVO.getMemberUserIds())); | |
| 201 | + memberUserIds.removeAll(activeMemberUserIds); | |
| 202 | + if (CollUtil.isEmpty(memberUserIds)) { | |
| 201 | 203 | return; |
| 202 | 204 | } |
| 203 | 205 | // 1.3 校验被邀请人都是当前用户的好友 |
| 204 | - List<Long> memberUserIds = inviteReqVO.getMemberUserIds(); | |
| 205 | 206 | List<ImFriendDO> friends = friendService.getActiveFriendList(userId, memberUserIds); |
| 206 | 207 | Set<Long> friendUserIds = convertSet(friends, ImFriendDO::getFriendUserId); |
| 207 | 208 | Collection<Long> notFriendUserIds = CollUtil.subtract(memberUserIds, friendUserIds); |
| ... | ... | @@ -214,7 +215,7 @@ public class ImGroupServiceImpl implements ImGroupService { |
| 214 | 215 | } |
| 215 | 216 | |
| 216 | 217 | // 2. 情况一:群开启审批 + 邀请人是普通成员,落 group_request 等群主 / 管理员处理 |
| 217 | - // 群主 / 管理员邀请视同已审批通过,绕过审批直接拉人进群(对齐 OpenIM / 微信) | |
| 218 | + // 群主 / 管理员邀请,直接拉人进群 | |
| 218 | 219 | if (Boolean.TRUE.equals(group.getJoinApproval()) |
| 219 | 220 | && !ImGroupMemberRoleEnum.isOwnerOrAdmin(operator.getRole())) { |
| 220 | 221 | groupRequestService.createInviteRequestList(groupId, userId, memberUserIds); |
| ... | ... | @@ -227,7 +228,7 @@ public class ImGroupServiceImpl implements ImGroupService { |
| 227 | 228 | |
| 228 | 229 | // 4. 发 GROUP_MEMBER_INVITE 通知给全员;本地拼 receivers(已查的 active + 新邀请)避免缓存刚 evict 后强制走 DB |
| 229 | 230 | Set<Long> allReceivers = new HashSet<>(memberUserIds); |
| 230 | - activeMembers.forEach(member -> allReceivers.add(member.getUserId())); | |
| 231 | + allReceivers.addAll(activeMemberUserIds); | |
| 231 | 232 | groupMessageService.sendGroupMessage(userId, allReceivers, |
| 232 | 233 | ImGroupMessageSendDTO.ofGroupMemberInvite(groupId, userId, memberUserIds)); |
| 233 | 234 | } | ... | ... |
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; |
| 6 | 6 | /** |
| 7 | 7 | * 入群申请被同意通知 |
| 8 | 8 | * <p> |
| 9 | - * 走私聊通道定向推送给申请人 + 群主 + 全部管理员; | |
| 10 | - * 申请人侧据此弹 toast、列表项更新;admin 侧据此把 unhandledCount 减 1 并从未处理列表移除 | |
| 9 | + * 走私聊通道定向推送给申请人 + 群主 + 全部管理员;admin 侧据此把 unhandledCount 减 1 并从未处理列表移除 | |
| 11 | 10 | */ |
| 12 | 11 | @EqualsAndHashCode(callSuper = true) |
| 13 | 12 | @Data | ... | ... |
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/group/ImGroupRequestServiceImplTest.java
| ... | ... | @@ -265,8 +265,14 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest { |
| 265 | 265 | groupRequestService.createInviteRequestList(10L, 1L, List.of(2L, 3L)); |
| 266 | 266 | |
| 267 | 267 | // 断言:插入 2 条申请记录 + 推 1503 给 owner(每条 1 帧)共 2 帧 |
| 268 | - verify(groupRequestMapper, times(2)).insert(any(ImGroupRequestDO.class)); | |
| 268 | + ArgumentCaptor<ImGroupRequestDO> captor = ArgumentCaptor.forClass(ImGroupRequestDO.class); | |
| 269 | + verify(groupRequestMapper, times(2)).insert(captor.capture()); | |
| 269 | 270 | verify(websocketService, times(2)).sendPrivateMessageAsync(anyLong(), any(ImPrivateMessageDTO.class)); |
| 271 | + // 断言:每条记录 inviterUserId=1 + addSource=INVITE,避免审批通过后回写群成员留痕的来源为空 / 脏带旧值 | |
| 272 | + captor.getAllValues().forEach(insert -> { | |
| 273 | + assertEquals(1L, insert.getInviterUserId()); | |
| 274 | + assertEquals(ImGroupAddSourceEnum.INVITE.getSource(), insert.getAddSource()); | |
| 275 | + }); | |
| 270 | 276 | } |
| 271 | 277 | |
| 272 | 278 | private AdminUserRespDTO buildUser(Long id, String nickname) { | ... | ... |