Commit f360040a3d144733ba2d14845557eec32789a6c6
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
Showing
6 changed files
with
101 additions
and
68 deletions
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<ImGroupRequestDO> { | @@ -52,6 +54,42 @@ public interface ImGroupRequestMapper extends BaseMapperX<ImGroupRequestDO> { | ||
| 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 | }); |