diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/friend/ImFriendRequestMapper.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/friend/ImFriendRequestMapper.java index b03c21d..1f7c6a5 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/friend/ImFriendRequestMapper.java +++ b/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; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; import cn.iocoder.yudao.module.im.controller.admin.manager.friend.vo.ImFriendRequestManagerPageReqVO; import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendRequestDO; +import cn.iocoder.yudao.module.im.enums.friend.ImFriendRequestHandleResultEnum; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import org.apache.ibatis.annotations.Mapper; @@ -43,6 +44,22 @@ public interface ImFriendRequestMapper extends BaseMapperX { .eq(ImFriendRequestDO::getId, id).eq(ImFriendRequestDO::getHandleResult, handleResult)); } + /** + * 复用 (fromUserId, toUserId) 旧申请记录:覆盖申请理由 / 备注 / 来源,重置为未处理 + 清空旧处理痕迹 + *

+ * handleContent / handleTime 走 LambdaUpdateWrapper.set 显式置 null,updateById 默认会忽略 null 字段 + */ + default int updateByIdReset(Long id, String applyContent, String displayName, Integer addSource) { + return update(null, new LambdaUpdateWrapper() + .eq(ImFriendRequestDO::getId, id) + .set(ImFriendRequestDO::getApplyContent, applyContent) + .set(ImFriendRequestDO::getDisplayName, displayName) + .set(ImFriendRequestDO::getAddSource, addSource) + .set(ImFriendRequestDO::getHandleResult, ImFriendRequestHandleResultEnum.UNHANDLED.getResult()) + .set(ImFriendRequestDO::getHandleContent, null) + .set(ImFriendRequestDO::getHandleTime, null)); + } + default PageResult selectPage(ImFriendRequestManagerPageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .eqIfPresent(ImFriendRequestDO::getFromUserId, reqVO.getFromUserId()) 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 ff93f3b..ea43d9e 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 @@ -5,9 +5,11 @@ import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; import cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.ImGroupRequestManagerPageReqVO; import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupRequestDO; +import cn.iocoder.yudao.module.im.enums.group.ImGroupRequestHandleResultEnum; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import org.apache.ibatis.annotations.Mapper; +import java.time.LocalDateTime; import java.util.Collection; import java.util.List; @@ -52,6 +54,42 @@ public interface ImGroupRequestMapper extends BaseMapperX { .eq(ImGroupRequestDO::getId, id).eq(ImGroupRequestDO::getHandleResult, expectedHandleResult)); } + /** + * 复用主动申请的旧记录:覆盖申请理由 / 来源,重置为未处理 + 清空旧处理痕迹 + 刷 update_time + *

+ * update_time 显式 set,因为 update(null, wrapper) 不会触发 MetaObjectHandler.updateFill; + * 列表查询按 update_time 倒序,复用记录必须刷这一列才会排到最前 + */ + default int updateApplyByIdReset(Long id, String applyContent, Integer addSource, LocalDateTime updateTime) { + return update(null, new LambdaUpdateWrapper() + .eq(ImGroupRequestDO::getId, id) + .set(ImGroupRequestDO::getApplyContent, applyContent) + .set(ImGroupRequestDO::getAddSource, addSource) + .set(ImGroupRequestDO::getHandleResult, ImGroupRequestHandleResultEnum.UNHANDLED.getResult()) + .set(ImGroupRequestDO::getInviterUserId, null) + .set(ImGroupRequestDO::getHandleUserId, null) + .set(ImGroupRequestDO::getHandleContent, null) + .set(ImGroupRequestDO::getHandleTime, null) + .set(ImGroupRequestDO::getUpdateTime, updateTime)); + } + + /** + * 复用邀请审批的旧记录(批量):盖写 inviterUserId / addSource,重置为未处理 + 清空旧处理痕迹 + 刷 update_time + */ + default int updateInviteByGroupIdAndUserIdsReset(Long groupId, Collection userIds, + Long inviterUserId, Integer addSource, LocalDateTime updateTime) { + return update(null, new LambdaUpdateWrapper() + .eq(ImGroupRequestDO::getGroupId, groupId) + .in(ImGroupRequestDO::getUserId, userIds) + .set(ImGroupRequestDO::getInviterUserId, inviterUserId) + .set(ImGroupRequestDO::getAddSource, addSource) + .set(ImGroupRequestDO::getHandleResult, ImGroupRequestHandleResultEnum.UNHANDLED.getResult()) + .set(ImGroupRequestDO::getHandleUserId, null) + .set(ImGroupRequestDO::getHandleContent, null) + .set(ImGroupRequestDO::getHandleTime, null) + .set(ImGroupRequestDO::getUpdateTime, updateTime)); + } + default PageResult selectPage(ImGroupRequestManagerPageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .eqIfPresent(ImGroupRequestDO::getGroupId, reqVO.getGroupId()) diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendRequestServiceImpl.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendRequestServiceImpl.java index 84bf719..08592de 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendRequestServiceImpl.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendRequestServiceImpl.java @@ -1,7 +1,6 @@ package cn.iocoder.yudao.module.im.service.friend; import cn.hutool.core.util.ObjUtil; -import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.PageResult; @@ -88,18 +87,10 @@ public class ImFriendRequestServiceImpl implements ImFriendRequestService { } // 2. 落库:upsert 语义;同一对 (from, to) 唯一,已有记录覆盖申请理由 / 备注 / 来源 + 重置为未处理 + 清空旧处理痕迹 - // null 字段(handleContent / handleTime)走 LambdaUpdateWrapper.set 显式清空,updateById 默认会忽略 null - // TODO @AI:看看是不是在 basemapperx 里,增加一个 updateXXXX;可以根据传递的 DO,深度更新的方法?应该匹配这个场景的对哇?【主要希望 service 不要出现 mapper 相关的类】 ImFriendRequestDO request = friendRequestMapper.selectByFromUserIdAndToUserId(fromUserId, toUserId); if (request != null) { - friendRequestMapper.update(null, new LambdaUpdateWrapper() - .eq(ImFriendRequestDO::getId, request.getId()) - .set(ImFriendRequestDO::getApplyContent, reqVO.getApplyContent()) - .set(ImFriendRequestDO::getDisplayName, reqVO.getDisplayName()) - .set(ImFriendRequestDO::getAddSource, reqVO.getAddSource()) - .set(ImFriendRequestDO::getHandleResult, ImFriendRequestHandleResultEnum.UNHANDLED.getResult()) - .set(ImFriendRequestDO::getHandleContent, null) - .set(ImFriendRequestDO::getHandleTime, null)); + friendRequestMapper.updateByIdReset(request.getId(), + reqVO.getApplyContent(), reqVO.getDisplayName(), reqVO.getAddSource()); request.setApplyContent(reqVO.getApplyContent()).setDisplayName(reqVO.getDisplayName()) .setAddSource(reqVO.getAddSource()) .setHandleResult(ImFriendRequestHandleResultEnum.UNHANDLED.getResult()) 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 366a5fd..37b410c 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 @@ -1,7 +1,6 @@ package cn.iocoder.yudao.module.im.service.group; import cn.hutool.core.collection.CollUtil; -import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; 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; @@ -94,25 +93,12 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService { return null; } - // 3. 情况二:群开启了审批;upsert 一条「主动申请」记录,inviterUserId=null - // 已有记录(无论之前是未处理 / 已同意 / 已拒绝),覆盖申请理由 + 来源 + 重置为未处理 + 清空旧处理痕迹 - // null 字段(inviterUserId / handleUserId / handleContent / handleTime)走 LambdaUpdateWrapper.set 显式清空,updateById 默认会忽略 null - // TODO @AI:看看是不是在 basemapperx 里,增加一个 updateXXXX;可以根据传递的 DO,深度更新的方法?应该匹配这个场景的对哇?【主要希望 service 不要出现 mapper 相关的类】 + // 3. 情况二:群开启了审批;upsert 一条主动申请记录(inviterUserId=null) 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()) - .set(ImGroupRequestDO::getAddSource, reqVO.getAddSource()) - .set(ImGroupRequestDO::getHandleResult, ImGroupRequestHandleResultEnum.UNHANDLED.getResult()) - .set(ImGroupRequestDO::getInviterUserId, null) - .set(ImGroupRequestDO::getHandleUserId, null) - .set(ImGroupRequestDO::getHandleContent, null) - .set(ImGroupRequestDO::getHandleTime, null) - .set(ImGroupRequestDO::getUpdateTime, now)); + groupRequestMapper.updateApplyByIdReset(request.getId(), + reqVO.getApplyContent(), reqVO.getAddSource(), now); request.setApplyContent(reqVO.getApplyContent()).setAddSource(reqVO.getAddSource()) .setHandleResult(ImGroupRequestHandleResultEnum.UNHANDLED.getResult()) .setInviterUserId(null).setHandleUserId(null) @@ -209,45 +195,40 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService { return; } ImGroupDO group = groupService.validateGroupExists(groupId); - - // 1. 每个被邀请人 upsert 一条 inviter_user_id=邀请人 的记录 - // 已有记录(同一对 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 倒序排不到最前 + Integer unhandled = ImGroupRequestHandleResultEnum.UNHANDLED.getResult(); 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) - .set(ImGroupRequestDO::getUpdateTime, now)); - existing.setInviterUserId(inviterUserId).setAddSource(inviteSource) - .setHandleResult(ImGroupRequestHandleResultEnum.UNHANDLED.getResult()) - .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); + + // 1. 批量查现有记录,分流为「复用旧记录」+「新增」 + List existingList = groupRequestMapper.selectListByGroupIdAndUserIds(groupId, invitedUserIds); + Set existingUserIds = convertSet(existingList, ImGroupRequestDO::getUserId); + Collection newUserIds = CollUtil.subtract(invitedUserIds, existingUserIds); + + // 2. 复用旧记录:一条 update WHERE IN 完成 + if (CollUtil.isNotEmpty(existingList)) { + groupRequestMapper.updateInviteByGroupIdAndUserIdsReset(groupId, existingUserIds, + inviterUserId, inviteSource, now); + // 同步内存对象,下面 buildRequestNotification 要读 inviterUserId / addSource + existingList.forEach(r -> r.setInviterUserId(inviterUserId).setAddSource(inviteSource) + .setHandleResult(unhandled) + .setHandleUserId(null).setHandleContent(null).setHandleTime(null) + .setUpdateTime(now)); + } + + // 3. 新增:批量 insert + List newList = convertList(newUserIds, userId -> new ImGroupRequestDO() + .setGroupId(groupId).setUserId(userId).setInviterUserId(inviterUserId) + .setAddSource(inviteSource).setHandleResult(unhandled)); + if (CollUtil.isNotEmpty(newList)) { + groupRequestMapper.insertBatch(newList); } - // 2. 推 1503 给群主 + 全部管理员;多端同步;每条申请单独推一帧 + // 4. 推 1503 给群主 + 全部管理员;多端同步;每条申请单独推一帧 Map userMap = adminUserApi.getUserMap(invitedUserIds); List ownerAndAdmins = getGroupMemberListByOwnerAndAdminUserIds(group); + List requests = new ArrayList<>(existingList.size() + newList.size()); + requests.addAll(existingList); + requests.addAll(newList); for (ImGroupRequestDO request : requests) { AdminUserRespDTO applyUser = userMap.get(request.getUserId()); GroupRequestReceivedNotification payload = buildRequestNotification(group, request, applyUser); 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 722d88e..2220f3a 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 @@ -197,8 +197,8 @@ public class ImGroupServiceImpl implements ImGroupService { // 1.2 入参去重 + 排除已在群中的用户 List activeMembers = groupMemberService.getActiveGroupMemberListByGroupId(groupId); Set activeMemberUserIds = convertSet(activeMembers, ImGroupMemberDO::getUserId); - List memberUserIds = new ArrayList<>(new LinkedHashSet<>(inviteReqVO.getMemberUserIds())); - memberUserIds.removeAll(activeMemberUserIds); + List memberUserIds = CollUtil.subtractToList( + CollUtil.distinct(inviteReqVO.getMemberUserIds()), activeMemberUserIds); if (CollUtil.isEmpty(memberUserIds)) { return; } 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 5b7672f..590a156 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 @@ -23,6 +23,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; +import java.util.Collection; import java.util.List; import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*; @@ -253,6 +254,8 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest { .joinApproval(true) .status(CommonStatusEnum.ENABLE.getStatus()).build(); when(groupService.validateGroupExists(10L)).thenReturn(group); + when(groupRequestMapper.selectListByGroupIdAndUserIds(eq(10L), anyCollection())) + .thenReturn(List.of()); when(groupMemberService.getGroupMemberListByOwnerAndAdmin(10L)).thenReturn(List.of( ImGroupMemberDO.builder().groupId(10L).userId(99L) .role(ImGroupMemberRoleEnum.OWNER.getRole()) @@ -261,15 +264,18 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest { 2L, buildUser(2L, "用户A"), 3L, buildUser(3L, "用户B"))); - // 调用:邀请人 1L 邀请 2L、3L + // 调用:邀请人 1L 邀请 2L、3L(都没有旧记录) groupRequestService.createInviteRequestList(10L, 1L, List.of(2L, 3L)); - // 断言:插入 2 条申请记录 + 推 1503 给 owner(每条 1 帧)共 2 帧 - ArgumentCaptor captor = ArgumentCaptor.forClass(ImGroupRequestDO.class); - verify(groupRequestMapper, times(2)).insert(captor.capture()); + // 断言:批量插入 2 条 + 推 1503 给 owner(每条 1 帧)共 2 帧 + @SuppressWarnings("unchecked") + ArgumentCaptor> captor = ArgumentCaptor.forClass(Collection.class); + verify(groupRequestMapper).insertBatch(captor.capture()); verify(websocketService, times(2)).sendPrivateMessageAsync(anyLong(), any(ImPrivateMessageDTO.class)); // 断言:每条记录 inviterUserId=1 + addSource=INVITE,避免审批通过后回写群成员留痕的来源为空 / 脏带旧值 - captor.getAllValues().forEach(insert -> { + Collection inserted = captor.getValue(); + assertEquals(2, inserted.size()); + inserted.forEach(insert -> { assertEquals(1L, insert.getInviterUserId()); assertEquals(ImGroupAddSourceEnum.INVITE.getSource(), insert.getAddSource()); });