Commit f360040a3d144733ba2d14845557eec32789a6c6

Authored by YunaiV
1 parent ded12090

✨ feat(im): 初始化群申请 v0.5:第六把 review(性能 / 健壮性 / 简洁度收口)

后端
- createInviteRequestList N+1 → 3 SQL:批量 select IN + update IN + insertBatch;20 人邀请从 40 RTT 降到 3 RTT
- service 不再出现 mybatis:复用记录的 update(null, wrapper) 下沉到 Im{Group,Friend}RequestMapper.update*Reset helper
- inviteGroupMember 入参去重切 hutool:CollUtil.subtractToList(CollUtil.distinct(...), activeMemberUserIds)
- 删除 dead 字段 inviterUserId(GroupRequestApprovedNotification / GroupRequestRejectedNotification):前端不再消费

前端
- 1505 / 1506 通知改静默:同意走群事件 1509 / 1510 渲染系统提示,拒绝不再打扰
- 修竞态:addByRequestId 校验 handleResult === UNHANDLED,避免 1503 在途时被 1505 / 1506 抢先后又把已处理记录塞回未处理列表
- 修复 dialog 复用记录刷新:watch key 含 inviterUserId / applyContent,同 id 不同内容也触发 refetch;actingId 期间跳过避免本端动作多余 RTT
- 修复 willGoApproval 误报:group.ownerUserId 兜底群主;members 未到位时保守按非审批处理
- unhandledCountMap memoized getter:O(N) 扫一次缓存到 Map,ConversationItem 直读 Map 消除 O(N×M) 重复 filter
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/friend/ImFriendRequestMapper.java
@@ -5,6 +5,7 @@ import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; @@ -5,6 +5,7 @@ import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
5 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; 5 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
6 import cn.iocoder.yudao.module.im.controller.admin.manager.friend.vo.ImFriendRequestManagerPageReqVO; 6 import cn.iocoder.yudao.module.im.controller.admin.manager.friend.vo.ImFriendRequestManagerPageReqVO;
7 import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendRequestDO; 7 import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendRequestDO;
  8 +import cn.iocoder.yudao.module.im.enums.friend.ImFriendRequestHandleResultEnum;
8 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; 9 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
9 import org.apache.ibatis.annotations.Mapper; 10 import org.apache.ibatis.annotations.Mapper;
10 11
@@ -43,6 +44,22 @@ public interface ImFriendRequestMapper extends BaseMapperX<ImFriendRequestDO> { @@ -43,6 +44,22 @@ public interface ImFriendRequestMapper extends BaseMapperX<ImFriendRequestDO> {
43 .eq(ImFriendRequestDO::getId, id).eq(ImFriendRequestDO::getHandleResult, handleResult)); 44 .eq(ImFriendRequestDO::getId, id).eq(ImFriendRequestDO::getHandleResult, handleResult));
44 } 45 }
45 46
  47 + /**
  48 + * 复用 (fromUserId, toUserId) 旧申请记录:覆盖申请理由 / 备注 / 来源,重置为未处理 + 清空旧处理痕迹
  49 + * <p>
  50 + * handleContent / handleTime 走 LambdaUpdateWrapper.set 显式置 null,updateById 默认会忽略 null 字段
  51 + */
  52 + default int updateByIdReset(Long id, String applyContent, String displayName, Integer addSource) {
  53 + return update(null, new LambdaUpdateWrapper<ImFriendRequestDO>()
  54 + .eq(ImFriendRequestDO::getId, id)
  55 + .set(ImFriendRequestDO::getApplyContent, applyContent)
  56 + .set(ImFriendRequestDO::getDisplayName, displayName)
  57 + .set(ImFriendRequestDO::getAddSource, addSource)
  58 + .set(ImFriendRequestDO::getHandleResult, ImFriendRequestHandleResultEnum.UNHANDLED.getResult())
  59 + .set(ImFriendRequestDO::getHandleContent, null)
  60 + .set(ImFriendRequestDO::getHandleTime, null));
  61 + }
  62 +
46 default PageResult<ImFriendRequestDO> selectPage(ImFriendRequestManagerPageReqVO reqVO) { 63 default PageResult<ImFriendRequestDO> selectPage(ImFriendRequestManagerPageReqVO reqVO) {
47 return selectPage(reqVO, new LambdaQueryWrapperX<ImFriendRequestDO>() 64 return selectPage(reqVO, new LambdaQueryWrapperX<ImFriendRequestDO>()
48 .eqIfPresent(ImFriendRequestDO::getFromUserId, reqVO.getFromUserId()) 65 .eqIfPresent(ImFriendRequestDO::getFromUserId, reqVO.getFromUserId())
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/group/ImGroupRequestMapper.java
@@ -5,9 +5,11 @@ import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; @@ -5,9 +5,11 @@ import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
5 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; 5 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
6 import cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.ImGroupRequestManagerPageReqVO; 6 import cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.ImGroupRequestManagerPageReqVO;
7 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupRequestDO; 7 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupRequestDO;
  8 +import cn.iocoder.yudao.module.im.enums.group.ImGroupRequestHandleResultEnum;
8 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; 9 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
9 import org.apache.ibatis.annotations.Mapper; 10 import org.apache.ibatis.annotations.Mapper;
10 11
  12 +import java.time.LocalDateTime;
11 import java.util.Collection; 13 import java.util.Collection;
12 import java.util.List; 14 import java.util.List;
13 15
@@ -52,6 +54,42 @@ public interface ImGroupRequestMapper extends BaseMapperX&lt;ImGroupRequestDO&gt; { @@ -52,6 +54,42 @@ public interface ImGroupRequestMapper extends BaseMapperX&lt;ImGroupRequestDO&gt; {
52 .eq(ImGroupRequestDO::getId, id).eq(ImGroupRequestDO::getHandleResult, expectedHandleResult)); 54 .eq(ImGroupRequestDO::getId, id).eq(ImGroupRequestDO::getHandleResult, expectedHandleResult));
53 } 55 }
54 56
  57 + /**
  58 + * 复用主动申请的旧记录:覆盖申请理由 / 来源,重置为未处理 + 清空旧处理痕迹 + 刷 update_time
  59 + * <p>
  60 + * update_time 显式 set,因为 update(null, wrapper) 不会触发 MetaObjectHandler.updateFill;
  61 + * 列表查询按 update_time 倒序,复用记录必须刷这一列才会排到最前
  62 + */
  63 + default int updateApplyByIdReset(Long id, String applyContent, Integer addSource, LocalDateTime updateTime) {
  64 + return update(null, new LambdaUpdateWrapper<ImGroupRequestDO>()
  65 + .eq(ImGroupRequestDO::getId, id)
  66 + .set(ImGroupRequestDO::getApplyContent, applyContent)
  67 + .set(ImGroupRequestDO::getAddSource, addSource)
  68 + .set(ImGroupRequestDO::getHandleResult, ImGroupRequestHandleResultEnum.UNHANDLED.getResult())
  69 + .set(ImGroupRequestDO::getInviterUserId, null)
  70 + .set(ImGroupRequestDO::getHandleUserId, null)
  71 + .set(ImGroupRequestDO::getHandleContent, null)
  72 + .set(ImGroupRequestDO::getHandleTime, null)
  73 + .set(ImGroupRequestDO::getUpdateTime, updateTime));
  74 + }
  75 +
  76 + /**
  77 + * 复用邀请审批的旧记录(批量):盖写 inviterUserId / addSource,重置为未处理 + 清空旧处理痕迹 + 刷 update_time
  78 + */
  79 + default int updateInviteByGroupIdAndUserIdsReset(Long groupId, Collection<Long> userIds,
  80 + Long inviterUserId, Integer addSource, LocalDateTime updateTime) {
  81 + return update(null, new LambdaUpdateWrapper<ImGroupRequestDO>()
  82 + .eq(ImGroupRequestDO::getGroupId, groupId)
  83 + .in(ImGroupRequestDO::getUserId, userIds)
  84 + .set(ImGroupRequestDO::getInviterUserId, inviterUserId)
  85 + .set(ImGroupRequestDO::getAddSource, addSource)
  86 + .set(ImGroupRequestDO::getHandleResult, ImGroupRequestHandleResultEnum.UNHANDLED.getResult())
  87 + .set(ImGroupRequestDO::getHandleUserId, null)
  88 + .set(ImGroupRequestDO::getHandleContent, null)
  89 + .set(ImGroupRequestDO::getHandleTime, null)
  90 + .set(ImGroupRequestDO::getUpdateTime, updateTime));
  91 + }
  92 +
55 default PageResult<ImGroupRequestDO> selectPage(ImGroupRequestManagerPageReqVO reqVO) { 93 default PageResult<ImGroupRequestDO> selectPage(ImGroupRequestManagerPageReqVO reqVO) {
56 return selectPage(reqVO, new LambdaQueryWrapperX<ImGroupRequestDO>() 94 return selectPage(reqVO, new LambdaQueryWrapperX<ImGroupRequestDO>()
57 .eqIfPresent(ImGroupRequestDO::getGroupId, reqVO.getGroupId()) 95 .eqIfPresent(ImGroupRequestDO::getGroupId, reqVO.getGroupId())
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendRequestServiceImpl.java
1 package cn.iocoder.yudao.module.im.service.friend; 1 package cn.iocoder.yudao.module.im.service.friend;
2 2
3 import cn.hutool.core.util.ObjUtil; 3 import cn.hutool.core.util.ObjUtil;
4 -import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;  
5 import cn.hutool.extra.spring.SpringUtil; 4 import cn.hutool.extra.spring.SpringUtil;
6 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; 5 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
7 import cn.iocoder.yudao.framework.common.pojo.PageResult; 6 import cn.iocoder.yudao.framework.common.pojo.PageResult;
@@ -88,18 +87,10 @@ public class ImFriendRequestServiceImpl implements ImFriendRequestService { @@ -88,18 +87,10 @@ public class ImFriendRequestServiceImpl implements ImFriendRequestService {
88 } 87 }
89 88
90 // 2. 落库:upsert 语义;同一对 (from, to) 唯一,已有记录覆盖申请理由 / 备注 / 来源 + 重置为未处理 + 清空旧处理痕迹 89 // 2. 落库:upsert 语义;同一对 (from, to) 唯一,已有记录覆盖申请理由 / 备注 / 来源 + 重置为未处理 + 清空旧处理痕迹
91 - // null 字段(handleContent / handleTime)走 LambdaUpdateWrapper.set 显式清空,updateById 默认会忽略 null  
92 - // TODO @AI:看看是不是在 basemapperx 里,增加一个 updateXXXX;可以根据传递的 DO,深度更新的方法?应该匹配这个场景的对哇?【主要希望 service 不要出现 mapper 相关的类】  
93 ImFriendRequestDO request = friendRequestMapper.selectByFromUserIdAndToUserId(fromUserId, toUserId); 90 ImFriendRequestDO request = friendRequestMapper.selectByFromUserIdAndToUserId(fromUserId, toUserId);
94 if (request != null) { 91 if (request != null) {
95 - friendRequestMapper.update(null, new LambdaUpdateWrapper<ImFriendRequestDO>()  
96 - .eq(ImFriendRequestDO::getId, request.getId())  
97 - .set(ImFriendRequestDO::getApplyContent, reqVO.getApplyContent())  
98 - .set(ImFriendRequestDO::getDisplayName, reqVO.getDisplayName())  
99 - .set(ImFriendRequestDO::getAddSource, reqVO.getAddSource())  
100 - .set(ImFriendRequestDO::getHandleResult, ImFriendRequestHandleResultEnum.UNHANDLED.getResult())  
101 - .set(ImFriendRequestDO::getHandleContent, null)  
102 - .set(ImFriendRequestDO::getHandleTime, null)); 92 + friendRequestMapper.updateByIdReset(request.getId(),
  93 + reqVO.getApplyContent(), reqVO.getDisplayName(), reqVO.getAddSource());
103 request.setApplyContent(reqVO.getApplyContent()).setDisplayName(reqVO.getDisplayName()) 94 request.setApplyContent(reqVO.getApplyContent()).setDisplayName(reqVO.getDisplayName())
104 .setAddSource(reqVO.getAddSource()) 95 .setAddSource(reqVO.getAddSource())
105 .setHandleResult(ImFriendRequestHandleResultEnum.UNHANDLED.getResult()) 96 .setHandleResult(ImFriendRequestHandleResultEnum.UNHANDLED.getResult())
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupRequestServiceImpl.java
1 package cn.iocoder.yudao.module.im.service.group; 1 package cn.iocoder.yudao.module.im.service.group;
2 2
3 import cn.hutool.core.collection.CollUtil; 3 import cn.hutool.core.collection.CollUtil;
4 -import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;  
5 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; 4 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
6 import cn.iocoder.yudao.framework.common.pojo.PageResult; 5 import cn.iocoder.yudao.framework.common.pojo.PageResult;
7 import cn.iocoder.yudao.framework.common.util.object.BeanUtils; 6 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
@@ -94,25 +93,12 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService { @@ -94,25 +93,12 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService {
94 return null; 93 return null;
95 } 94 }
96 95
97 - // 3. 情况二:群开启了审批;upsert 一条「主动申请」记录,inviterUserId=null  
98 - // 已有记录(无论之前是未处理 / 已同意 / 已拒绝),覆盖申请理由 + 来源 + 重置为未处理 + 清空旧处理痕迹  
99 - // null 字段(inviterUserId / handleUserId / handleContent / handleTime)走 LambdaUpdateWrapper.set 显式清空,updateById 默认会忽略 null  
100 - // TODO @AI:看看是不是在 basemapperx 里,增加一个 updateXXXX;可以根据传递的 DO,深度更新的方法?应该匹配这个场景的对哇?【主要希望 service 不要出现 mapper 相关的类】 96 + // 3. 情况二:群开启了审批;upsert 一条主动申请记录(inviterUserId=null)
101 ImGroupRequestDO request = groupRequestMapper.selectByGroupIdAndUserId(groupId, userId); 97 ImGroupRequestDO request = groupRequestMapper.selectByGroupIdAndUserId(groupId, userId);
102 if (request != null) { 98 if (request != null) {
103 - // update_time 显式置当前时间:update(null, wrapper) 不会触发 MetaObjectHandler.updateFill;  
104 - // 列表查询按 update_time 倒序排,复用旧记录时必须刷新这一列才能让本次重新提交排到最前  
105 LocalDateTime now = LocalDateTime.now(); 99 LocalDateTime now = LocalDateTime.now();
106 - groupRequestMapper.update(null, new LambdaUpdateWrapper<ImGroupRequestDO>()  
107 - .eq(ImGroupRequestDO::getId, request.getId())  
108 - .set(ImGroupRequestDO::getApplyContent, reqVO.getApplyContent())  
109 - .set(ImGroupRequestDO::getAddSource, reqVO.getAddSource())  
110 - .set(ImGroupRequestDO::getHandleResult, ImGroupRequestHandleResultEnum.UNHANDLED.getResult())  
111 - .set(ImGroupRequestDO::getInviterUserId, null)  
112 - .set(ImGroupRequestDO::getHandleUserId, null)  
113 - .set(ImGroupRequestDO::getHandleContent, null)  
114 - .set(ImGroupRequestDO::getHandleTime, null)  
115 - .set(ImGroupRequestDO::getUpdateTime, now)); 100 + groupRequestMapper.updateApplyByIdReset(request.getId(),
  101 + reqVO.getApplyContent(), reqVO.getAddSource(), now);
116 request.setApplyContent(reqVO.getApplyContent()).setAddSource(reqVO.getAddSource()) 102 request.setApplyContent(reqVO.getApplyContent()).setAddSource(reqVO.getAddSource())
117 .setHandleResult(ImGroupRequestHandleResultEnum.UNHANDLED.getResult()) 103 .setHandleResult(ImGroupRequestHandleResultEnum.UNHANDLED.getResult())
118 .setInviterUserId(null).setHandleUserId(null) 104 .setInviterUserId(null).setHandleUserId(null)
@@ -209,45 +195,40 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService { @@ -209,45 +195,40 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService {
209 return; 195 return;
210 } 196 }
211 ImGroupDO group = groupService.validateGroupExists(groupId); 197 ImGroupDO group = groupService.validateGroupExists(groupId);
212 -  
213 - // 1. 每个被邀请人 upsert 一条 inviter_user_id=邀请人 的记录  
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(); 198 Integer inviteSource = ImGroupAddSourceEnum.INVITE.getSource();
217 - List<ImGroupRequestDO> requests = new ArrayList<>(invitedUserIds.size());  
218 - // TODO @AI:看看是不是在 basemapperx 里,增加一个 updateXXXX;可以根据传递的 DO,深度更新的方法?应该匹配这个场景的对哇?【主要希望 service 不要出现 mapper 相关的类】  
219 - // 同 applyJoinGroup:update(null, wrapper) 不走 MetaObjectHandler.updateFill,复用旧记录必须显式刷 update_time,否则按 update_time 倒序排不到最前 199 + Integer unhandled = ImGroupRequestHandleResultEnum.UNHANDLED.getResult();
220 LocalDateTime now = LocalDateTime.now(); 200 LocalDateTime now = LocalDateTime.now();
221 - for (Long invitedUserId : invitedUserIds) {  
222 - ImGroupRequestDO existing = groupRequestMapper.selectByGroupIdAndUserId(groupId, invitedUserId);  
223 - if (existing != null) {  
224 - groupRequestMapper.update(null, new LambdaUpdateWrapper<ImGroupRequestDO>()  
225 - .eq(ImGroupRequestDO::getId, existing.getId())  
226 - .set(ImGroupRequestDO::getInviterUserId, inviterUserId)  
227 - .set(ImGroupRequestDO::getAddSource, inviteSource)  
228 - .set(ImGroupRequestDO::getHandleResult, ImGroupRequestHandleResultEnum.UNHANDLED.getResult())  
229 - .set(ImGroupRequestDO::getHandleUserId, null)  
230 - .set(ImGroupRequestDO::getHandleContent, null)  
231 - .set(ImGroupRequestDO::getHandleTime, null)  
232 - .set(ImGroupRequestDO::getUpdateTime, now));  
233 - existing.setInviterUserId(inviterUserId).setAddSource(inviteSource)  
234 - .setHandleResult(ImGroupRequestHandleResultEnum.UNHANDLED.getResult())  
235 - .setHandleUserId(null).setHandleContent(null).setHandleTime(null)  
236 - .setUpdateTime(now);  
237 - requests.add(existing);  
238 - continue;  
239 - }  
240 - ImGroupRequestDO insert = new ImGroupRequestDO()  
241 - .setGroupId(groupId).setUserId(invitedUserId).setInviterUserId(inviterUserId)  
242 - .setAddSource(inviteSource)  
243 - .setHandleResult(ImGroupRequestHandleResultEnum.UNHANDLED.getResult());  
244 - groupRequestMapper.insert(insert);  
245 - requests.add(insert); 201 +
  202 + // 1. 批量查现有记录,分流为「复用旧记录」+「新增」
  203 + List<ImGroupRequestDO> existingList = groupRequestMapper.selectListByGroupIdAndUserIds(groupId, invitedUserIds);
  204 + Set<Long> existingUserIds = convertSet(existingList, ImGroupRequestDO::getUserId);
  205 + Collection<Long> newUserIds = CollUtil.subtract(invitedUserIds, existingUserIds);
  206 +
  207 + // 2. 复用旧记录:一条 update WHERE IN 完成
  208 + if (CollUtil.isNotEmpty(existingList)) {
  209 + groupRequestMapper.updateInviteByGroupIdAndUserIdsReset(groupId, existingUserIds,
  210 + inviterUserId, inviteSource, now);
  211 + // 同步内存对象,下面 buildRequestNotification 要读 inviterUserId / addSource
  212 + existingList.forEach(r -> r.setInviterUserId(inviterUserId).setAddSource(inviteSource)
  213 + .setHandleResult(unhandled)
  214 + .setHandleUserId(null).setHandleContent(null).setHandleTime(null)
  215 + .setUpdateTime(now));
  216 + }
  217 +
  218 + // 3. 新增:批量 insert
  219 + List<ImGroupRequestDO> newList = convertList(newUserIds, userId -> new ImGroupRequestDO()
  220 + .setGroupId(groupId).setUserId(userId).setInviterUserId(inviterUserId)
  221 + .setAddSource(inviteSource).setHandleResult(unhandled));
  222 + if (CollUtil.isNotEmpty(newList)) {
  223 + groupRequestMapper.insertBatch(newList);
246 } 224 }
247 225
248 - // 2. 推 1503 给群主 + 全部管理员;多端同步;每条申请单独推一帧 226 + // 4. 推 1503 给群主 + 全部管理员;多端同步;每条申请单独推一帧
249 Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(invitedUserIds); 227 Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(invitedUserIds);
250 List<Long> ownerAndAdmins = getGroupMemberListByOwnerAndAdminUserIds(group); 228 List<Long> ownerAndAdmins = getGroupMemberListByOwnerAndAdminUserIds(group);
  229 + List<ImGroupRequestDO> requests = new ArrayList<>(existingList.size() + newList.size());
  230 + requests.addAll(existingList);
  231 + requests.addAll(newList);
251 for (ImGroupRequestDO request : requests) { 232 for (ImGroupRequestDO request : requests) {
252 AdminUserRespDTO applyUser = userMap.get(request.getUserId()); 233 AdminUserRespDTO applyUser = userMap.get(request.getUserId());
253 GroupRequestReceivedNotification payload = buildRequestNotification(group, request, applyUser); 234 GroupRequestReceivedNotification payload = buildRequestNotification(group, request, applyUser);
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImpl.java
@@ -197,8 +197,8 @@ public class ImGroupServiceImpl implements ImGroupService { @@ -197,8 +197,8 @@ public class ImGroupServiceImpl implements ImGroupService {
197 // 1.2 入参去重 + 排除已在群中的用户 197 // 1.2 入参去重 + 排除已在群中的用户
198 List<ImGroupMemberDO> activeMembers = groupMemberService.getActiveGroupMemberListByGroupId(groupId); 198 List<ImGroupMemberDO> activeMembers = groupMemberService.getActiveGroupMemberListByGroupId(groupId);
199 Set<Long> activeMemberUserIds = convertSet(activeMembers, ImGroupMemberDO::getUserId); 199 Set<Long> activeMemberUserIds = convertSet(activeMembers, ImGroupMemberDO::getUserId);
200 - List<Long> memberUserIds = new ArrayList<>(new LinkedHashSet<>(inviteReqVO.getMemberUserIds()));  
201 - memberUserIds.removeAll(activeMemberUserIds); 200 + List<Long> memberUserIds = CollUtil.subtractToList(
  201 + CollUtil.distinct(inviteReqVO.getMemberUserIds()), activeMemberUserIds);
202 if (CollUtil.isEmpty(memberUserIds)) { 202 if (CollUtil.isEmpty(memberUserIds)) {
203 return; 203 return;
204 } 204 }
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/group/ImGroupRequestServiceImplTest.java
@@ -23,6 +23,7 @@ import org.mockito.ArgumentCaptor; @@ -23,6 +23,7 @@ import org.mockito.ArgumentCaptor;
23 import org.mockito.InjectMocks; 23 import org.mockito.InjectMocks;
24 import org.mockito.Mock; 24 import org.mockito.Mock;
25 25
  26 +import java.util.Collection;
26 import java.util.List; 27 import java.util.List;
27 28
28 import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*; 29 import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*;
@@ -253,6 +254,8 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest { @@ -253,6 +254,8 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest {
253 .joinApproval(true) 254 .joinApproval(true)
254 .status(CommonStatusEnum.ENABLE.getStatus()).build(); 255 .status(CommonStatusEnum.ENABLE.getStatus()).build();
255 when(groupService.validateGroupExists(10L)).thenReturn(group); 256 when(groupService.validateGroupExists(10L)).thenReturn(group);
  257 + when(groupRequestMapper.selectListByGroupIdAndUserIds(eq(10L), anyCollection()))
  258 + .thenReturn(List.of());
256 when(groupMemberService.getGroupMemberListByOwnerAndAdmin(10L)).thenReturn(List.of( 259 when(groupMemberService.getGroupMemberListByOwnerAndAdmin(10L)).thenReturn(List.of(
257 ImGroupMemberDO.builder().groupId(10L).userId(99L) 260 ImGroupMemberDO.builder().groupId(10L).userId(99L)
258 .role(ImGroupMemberRoleEnum.OWNER.getRole()) 261 .role(ImGroupMemberRoleEnum.OWNER.getRole())
@@ -261,15 +264,18 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest { @@ -261,15 +264,18 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest {
261 2L, buildUser(2L, "用户A"), 264 2L, buildUser(2L, "用户A"),
262 3L, buildUser(3L, "用户B"))); 265 3L, buildUser(3L, "用户B")));
263 266
264 - // 调用:邀请人 1L 邀请 2L、3L 267 + // 调用:邀请人 1L 邀请 2L、3L(都没有旧记录)
265 groupRequestService.createInviteRequestList(10L, 1L, List.of(2L, 3L)); 268 groupRequestService.createInviteRequestList(10L, 1L, List.of(2L, 3L));
266 269
267 - // 断言:插入 2 条申请记录 + 推 1503 给 owner(每条 1 帧)共 2 帧  
268 - ArgumentCaptor<ImGroupRequestDO> captor = ArgumentCaptor.forClass(ImGroupRequestDO.class);  
269 - verify(groupRequestMapper, times(2)).insert(captor.capture()); 270 + // 断言:批量插入 2 条 + 推 1503 给 owner(每条 1 帧)共 2 帧
  271 + @SuppressWarnings("unchecked")
  272 + ArgumentCaptor<Collection<ImGroupRequestDO>> captor = ArgumentCaptor.forClass(Collection.class);
  273 + verify(groupRequestMapper).insertBatch(captor.capture());
270 verify(websocketService, times(2)).sendPrivateMessageAsync(anyLong(), any(ImPrivateMessageDTO.class)); 274 verify(websocketService, times(2)).sendPrivateMessageAsync(anyLong(), any(ImPrivateMessageDTO.class));
271 // 断言:每条记录 inviterUserId=1 + addSource=INVITE,避免审批通过后回写群成员留痕的来源为空 / 脏带旧值 275 // 断言:每条记录 inviterUserId=1 + addSource=INVITE,避免审批通过后回写群成员留痕的来源为空 / 脏带旧值
272 - captor.getAllValues().forEach(insert -> { 276 + Collection<ImGroupRequestDO> inserted = captor.getValue();
  277 + assertEquals(2, inserted.size());
  278 + inserted.forEach(insert -> {
273 assertEquals(1L, insert.getInviterUserId()); 279 assertEquals(1L, insert.getInviterUserId());
274 assertEquals(ImGroupAddSourceEnum.INVITE.getSource(), insert.getAddSource()); 280 assertEquals(ImGroupAddSourceEnum.INVITE.getSource(), insert.getAddSource());
275 }); 281 });