From 513c130151dff23a270a2d05efd6dd7f617ce2e6 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 24 May 2026 17:13:17 +0800 Subject: [PATCH] fix(im):加固群聊与好友部分流程 --- yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupMemberController.java | 25 ++++++++++++++++++++++++- yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/ErrorCodeConstants.java | 2 ++ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendRequestServiceImpl.java | 6 ++++++ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImpl.java | 13 +++++++++---- yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupRequestServiceImpl.java | 9 ++++++++- yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImpl.java | 31 ++++++++++++++++++++++--------- yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImpl.java | 22 +++++++++++++++++----- yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/rtc/ImRtcCallServiceImpl.java | 2 ++ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/sensitiveword/ImSensitiveWordServiceImpl.java | 17 ++++++++++------- yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/friend/ImFriendRequestServiceImplTest.java | 18 ++++++++++++++++++ yudao-module-im/src/test/resources/sql/clean.sql | 1 + yudao-module-im/src/test/resources/sql/create_tables.sql | 23 ++++++++++++++++++++--- 12 files changed, 139 insertions(+), 30 deletions(-) diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupMemberController.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupMemberController.java index 6dbab64..1b77d44 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupMemberController.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupMemberController.java @@ -1,6 +1,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; @@ -58,6 +59,7 @@ public class ImGroupMemberController { public CommonResult getGroupMember(@RequestParam(value = "id", required = false) Long id, @RequestParam(value = "groupId", required = false) Long groupId, @RequestParam(value = "userId", required = false) Long userId) { + // 1. 查询群成员 ImGroupMemberDO member; if (id != null) { member = groupMemberService.getGroupMember(id); @@ -67,7 +69,18 @@ public class ImGroupMemberController { // 避免 selectByGroupIdAndUserId 收到 null 参数走全表扫 / 抛 SQL 异常 throw new IllegalArgumentException("参数缺失:需传 id 或 (groupId, userId)"); } - return success(BeanUtils.toBean(member, ImGroupMemberRespVO.class)); + if (member == null) { + return success(null); + } + + // 2. 校验当前登录用户是该成员所在群的有效成员 + Long loginUserId = getLoginUserId(); + groupMemberService.validateMemberInGroup(member.getGroupId(), loginUserId); + + // 3. 转化 VO + ImGroupMemberRespVO memberVO = BeanUtils.toBean(member, ImGroupMemberRespVO.class); + hidePrivateFieldsIfNotSelf(memberVO, member.getUserId(), loginUserId); + return success(memberVO); } @GetMapping("/list") @@ -91,8 +104,18 @@ public class ImGroupMemberController { ImGroupMemberRespVO vo = BeanUtils.toBean(m, ImGroupMemberRespVO.class); MapUtils.findAndThen(userMap, m.getUserId(), user -> vo.setNickname(user.getNickname()).setAvatar(user.getAvatar())); + hidePrivateFieldsIfNotSelf(vo, m.getUserId(), loginUserId); return vo; })); } + /** + * 非本人查看时,置空成员的私人设置字段(groupRemark / silent) + */ + private void hidePrivateFieldsIfNotSelf(ImGroupMemberRespVO vo, Long memberUserId, Long loginUserId) { + if (ObjUtil.notEqual(loginUserId, memberUserId)) { + vo.setGroupRemark(null).setSilent(null); + } + } + } diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/ErrorCodeConstants.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/ErrorCodeConstants.java index 7b5a7d0..eaa752e 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/ErrorCodeConstants.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/ErrorCodeConstants.java @@ -20,6 +20,7 @@ public interface ErrorCodeConstants { ErrorCode MESSAGE_NOT_IN_GROUP = new ErrorCode(1_040_300_009, "消息不属于该群"); ErrorCode MESSAGE_PRIVATE_READ_DISABLED = new ErrorCode(1_040_300_010, "私聊已读功能已关闭"); ErrorCode MESSAGE_GROUP_READ_DISABLED = new ErrorCode(1_040_300_011, "群聊已读功能已关闭"); + ErrorCode MESSAGE_GROUP_READ_USERS_DENIED = new ErrorCode(1_040_300_012, "仅消息发送者可查看已读列表"); // ========== 群 (1-040-400-000) ========== ErrorCode GROUP_NOT_EXISTS = new ErrorCode(1_040_400_000, "群不存在"); @@ -31,6 +32,7 @@ public interface ErrorCodeConstants { ErrorCode GROUP_MESSAGE_PIN_MAX_LIMIT = new ErrorCode(1_040_400_006, "群置顶消息数量不能超过 {} 条"); ErrorCode GROUP_MESSAGE_ALREADY_PINNED = new ErrorCode(1_040_400_007, "该消息已置顶"); ErrorCode GROUP_MESSAGE_NOT_PINNED = new ErrorCode(1_040_400_008, "该消息未置顶"); + ErrorCode GROUP_MESSAGE_PIN_DIRECTED_DENIED = new ErrorCode(1_040_400_009, "定向消息不支持置顶"); // ========== 群成员 (1-040-500-000) ========== ErrorCode GROUP_MEMBER_NOT_IN_GROUP = new ErrorCode(1_040_500_001, "您已不在该群中"); 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 e4d67dd..20564ef 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,5 +1,6 @@ package cn.iocoder.yudao.module.im.service.friend; +import cn.hutool.core.util.BooleanUtil; import cn.hutool.core.util.ObjUtil; import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; @@ -82,6 +83,11 @@ public class ImFriendRequestServiceImpl implements ImFriendRequestService { // 1.4 单向好友(我已删 + 对方仍把我当好友):静默重新启用我侧关系,避免对方感知我曾删除 ImFriendDO peerFriend = friendService.getFriend(toUserId, fromUserId); if (peerFriend != null && CommonStatusEnum.isEnable(peerFriend.getStatus())) { + // 对方已拉黑:静默恢复等于绕过拉黑回到好友列表,必须先拒掉; + // getFriendState 在我侧 DISABLE 时直接返回 NONE,拿不到 BLOCKED 信号,这里显式补一次校验 + if (BooleanUtil.isTrue(peerFriend.getBlocked())) { + throw exception(FRIEND_REQUEST_BLOCKED_BY_PEER); + } friendService.silentReAddFriend(fromUserId, toUserId, reqVO.getDisplayName(), reqVO.getAddSource()); return null; } diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImpl.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImpl.java index 8f2a35a..7dd665f 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImpl.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImpl.java @@ -249,8 +249,9 @@ public class ImFriendServiceImpl implements ImFriendService { /** * 单向绑定好友关系(内部方法,被 {@link #becomeFriends} / {@link #silentReAddFriend} 调用): - * - 情况一:已存在记录(含 ENABLE / DISABLE)→ 复用并恢复 ENABLE,silent / pinned / blocked 一并重置为 false - * - 情况二:不存在记录 → 直接插入新记录 + * - 情况一:已存在 ENABLE 记录 → 已是好友,幂等跳过;不重置 silent / pinned / blocked,避免历史未处理申请再被同意时清掉用户的拉黑 / 置顶 / 免打扰设置 + * - 情况二:已存在 DISABLE 记录 → 复用并恢复 ENABLE,silent / pinned / blocked 一并重置为 false,对齐"重新加好友"语义 + * - 情况三:不存在记录 → 直接插入新记录 *

* 并发安全:agree 路径由 {@code friend_request.handle_result} 的乐观锁单边推进; * 极端并发下若插入唯一键冲突,让 DuplicateKeyException 向外抛出,外层事务回滚。 @@ -259,7 +260,11 @@ public class ImFriendServiceImpl implements ImFriendService { */ public void addFriend0(Long userId, Long friendUserId, String displayName, Integer addSource) { ImFriendDO exists = friendMapper.selectByUserIdAndFriendUserId(userId, friendUserId); - // 情况一:复用旧记录 → 恢复 ENABLE + 重置 silent / pinned / blocked 与首次新增对齐 + // 情况一:已是 ENABLE 好友,幂等跳过;防御历史未处理申请被二次同意时把 blocked / silent / pinned 清回 false + if (exists != null && CommonStatusEnum.isEnable(exists.getStatus())) { + return; + } + // 情况二:复用 DISABLE 旧记录 → 恢复 ENABLE + 重置 silent / pinned / blocked,对齐"重新加好友"语义 if (exists != null) { ImFriendDO update = new ImFriendDO().setId(exists.getId()) .setStatus(CommonStatusEnum.ENABLE.getStatus()).setAddTime(LocalDateTime.now()) @@ -273,7 +278,7 @@ public class ImFriendServiceImpl implements ImFriendService { friendMapper.updateById(update); return; } - // 情况二:不存在记录 → 直接插入新记录 + // 情况三:不存在记录 → 直接插入新记录 ImFriendDO friend = ImFriendDO.builder().userId(userId).friendUserId(friendUserId) .silent(false).pinned(false).blocked(false) .displayName(displayName).addSource(addSource) 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 37b410c..6328ec1 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 @@ -123,8 +123,15 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService { @Override @Transactional(rollbackFor = Exception.class) public void agreeGroupRequest(Long userId, Long requestId) { - // 1. 校验申请存在 + 未处理 + 操作人是 owner / admin + // 1.1 校验申请存在 + 未处理 + 操作人是 owner / admin ImGroupRequestDO request = validateRequestForHandle(userId, requestId); + // 1.2 复核群当前状态:拒绝在封禁 / 解散的群继续放人 + groupService.validateGroupExists(request.getGroupId()); + // 1.3 复核申请人是否已在群中;幂等避免重复广播 1509 / 1510 入群事件 + ImGroupMemberDO applicant = groupMemberService.getGroupMember(request.getGroupId(), request.getUserId()); + if (applicant != null && CommonStatusEnum.ENABLE.getStatus().equals(applicant.getStatus())) { + throw exception(GROUP_REQUEST_ALREADY_MEMBER); + } // 2. 入群前校验人数上限;群已满抛错让操作人选择拒绝 groupService.validateMemberCountLimit(request.getGroupId(), 1); 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 9d4861b..6b0196a 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 @@ -192,6 +192,7 @@ public class ImGroupServiceImpl implements ImGroupService { // ==================== 群成员的写操作 ==================== @Override + @Transactional(rollbackFor = Exception.class) public void inviteGroupMember(Long userId, ImGroupMemberInviteReqVO inviteReqVO) { Long groupId = inviteReqVO.getGroupId(); // 1.1 校验群存在 + 当前用户是群成员;同时拿到 role 供下面审批分支判断 @@ -238,24 +239,28 @@ public class ImGroupServiceImpl implements ImGroupService { } @Override + @Transactional(rollbackFor = Exception.class) public void quitGroup(Long groupId, Long userId) { - // 1. 校验群存在 + // 1.1 校验群存在 ImGroupDO group = validateGroupExists(groupId); - // 2. 群主不可退群 + // 1.2 群主不可退群 if (ObjUtil.equal(group.getOwnerUserId(), userId)) { throw exception(GROUP_OWNER_CANNOT_QUIT); } + // 1.3 校验当前用户是有效群成员;防止非成员触发广播 + 后续 remove 失败时无法回滚已推送的事件 + groupMemberService.validateMemberInGroup(groupId, userId); - // 3. 先发广播,后移成员(见类 javadoc) + // 2. 先发广播,后移成员(见类 javadoc) groupMessageService.sendGroupMessage(userId, ImGroupMessageSendDTO.ofGroupMemberQuit(groupId, userId)); - // 4.1 移除群成员 + // 3.1 移除群成员 groupMemberService.removeGroupMember(groupId, userId); - // 4.2 清理已读缓存 + // 3.2 清理已读缓存 groupMessageService.deleteReadMaxMessageId(groupId, userId); } @Override + @Transactional(rollbackFor = Exception.class) public void removeGroupMember(Long userId, ImGroupMemberRemoveReqVO removeReqVO) { Long groupId = removeReqVO.getGroupId(); Set targetUserIds = new HashSet<>(removeReqVO.getMemberUserIds()); @@ -400,9 +405,9 @@ public class ImGroupServiceImpl implements ImGroupService { @CacheEvict(cacheNames = GROUP, key = "#groupId") @Transactional(rollbackFor = Exception.class) public void pinGroupMessage(Long userId, Long groupId, Long messageId) { - // 1. 校验群主 / 管理员;同时拿到 group 复用,避免再走一次 @Cacheable + // 1.1 校验群主 / 管理员;同时拿到 group 复用,避免再走一次 @Cacheable ImGroupDO group = validateOwnerOrAdminAndGetGroup(groupId, userId); - // 2. 校验消息属于该群、是普通聊天消息(绕过前端菜单不允许置顶群事件 / 撤回事件)、且未被撤回 + // 1.2 校验消息属于该群、是普通聊天消息(绕过前端菜单不允许置顶群事件 / 撤回事件)、且未被撤回 ImGroupMessageDO message = groupMessageService.getGroupMessage(messageId); if (message == null || ObjUtil.notEqual(message.getGroupId(), groupId)) { throw exception(MESSAGE_NOT_IN_GROUP); @@ -411,7 +416,12 @@ public class ImGroupServiceImpl implements ImGroupService { || ImMessageStatusEnum.RECALL.getStatus().equals(message.getStatus())) { throw exception(MESSAGE_NOT_IN_GROUP); } - // 3. 幂等 + 上限校验 + // 1.3 定向消息(receiverUserIds 非空)不允许置顶:置顶会向全群广播,泄露原本仅部分人可见的内容 + if (CollUtil.isNotEmpty(message.getReceiverUserIds())) { + throw exception(GROUP_MESSAGE_PIN_DIRECTED_DENIED); + } + + // 2. 幂等 + 上限校验 List pinned = new ArrayList<>(CollUtil.emptyIfNull(group.getPinnedMessageIds())); if (pinned.contains(messageId)) { throw exception(GROUP_MESSAGE_ALREADY_PINNED); @@ -423,7 +433,7 @@ public class ImGroupServiceImpl implements ImGroupService { pinned.add(messageId); groupMapper.updateById(new ImGroupDO().setId(groupId).setPinnedMessageIds(pinned)); - // 4. 推送 GROUP_MESSAGE_PIN 通知给全员;payload 直接带消息对象,前端不用回查群详情绕开 @CacheEvict 时序 + // 3. 推送 GROUP_MESSAGE_PIN 通知给全员;payload 直接带消息对象,前端不用回查群详情绕开 @CacheEvict 时序 groupMessageService.sendGroupMessage(userId, ImGroupMessageSendDTO.ofGroupMessagePin(groupId, userId, message)); } @@ -590,6 +600,7 @@ public class ImGroupServiceImpl implements ImGroupService { @Override @CacheEvict(cacheNames = GROUP, key = "#reqVO.groupId") + @Transactional(rollbackFor = Exception.class) public void muteAll(Long userId, ImGroupMuteAllReqVO reqVO) { // 1. 校验群主或管理员 validateGroupOwnerOrAdmin(reqVO.getGroupId(), userId); @@ -605,6 +616,7 @@ public class ImGroupServiceImpl implements ImGroupService { } @Override + @Transactional(rollbackFor = Exception.class) public void muteMember(Long userId, ImGroupMuteMemberReqVO reqVO) { // 1.1 不能禁言自己 if (ObjUtil.equal(userId, reqVO.getUserId())) { @@ -629,6 +641,7 @@ public class ImGroupServiceImpl implements ImGroupService { } @Override + @Transactional(rollbackFor = Exception.class) public void cancelMuteMember(Long userId, ImGroupCancelMuteMemberReqVO reqVO) { // 1.1 校验群存在且未封禁 validateGroupExists(reqVO.getGroupId()); diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImpl.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImpl.java index dd5e7d9..0ade2be 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImpl.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImpl.java @@ -177,7 +177,9 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { // 3. 发送撤回事件 return sendGroupMessage(userId, new ImGroupMessageSendDTO().setGroupId(message.getGroupId()) - .setType(ImMessageTypeEnum.RECALL.getType()).setContent(new RecallMessage().setMessageId(messageId))); + .setType(ImMessageTypeEnum.RECALL.getType()) + .setReceiverUserIds(message.getReceiverUserIds()) + .setContent(new RecallMessage().setMessageId(messageId))); } @Override @@ -374,10 +376,17 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { throw exception(MESSAGE_GROUP_READ_DISABLED); } // 1.1 校验用户在群中(权限校验) - groupMemberService.validateMemberInGroup(groupId, userId); - // 1.2 获取消息 + ImGroupMemberDO operator = groupMemberService.validateMemberInGroup(groupId, userId); + // 1.2 获取消息;并校验消息归属于该群、对调用者可见、调用者是发送方 ImGroupMessageDO message = groupMessageMapper.selectById(messageId); - if (message == null) { + if (message == null || ObjUtil.notEqual(message.getGroupId(), groupId)) { + return Collections.emptyList(); + } + if (!isMessageVisible(message, operator, userId)) { + return Collections.emptyList(); + } + // 1.3 仅消息发送方关心已读人数;非发送方查询直接返回空 + if (ObjUtil.notEqual(message.getSenderId(), userId)) { return Collections.emptyList(); } @@ -405,8 +414,11 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { ImGroupMemberDO member = groupMemberService.validateMemberInGroup(reqVO.getGroupId(), userId); // 2. 查询历史消息(仅入群之后) - return groupMessageMapper.selectHistoryList( + List messages = groupMessageMapper.selectHistoryList( reqVO.getGroupId(), reqVO.getMaxId(), reqVO.getLimit(), member.getJoinTime()); + + // 3. 过滤定向消息:仅保留当前用户可见的(receiverUserIds 为空 / 含当前用户 / 本人发送) + return filterList(messages, message -> isMessageVisible(message, member, userId)); } // ========== 异步 WebSocket 推送 ========== diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/rtc/ImRtcCallServiceImpl.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/rtc/ImRtcCallServiceImpl.java index 299e8c8..275b83e 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/rtc/ImRtcCallServiceImpl.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/rtc/ImRtcCallServiceImpl.java @@ -250,6 +250,8 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { if (!ImConversationTypeEnum.isGroup(call.getConversationType())) { throw exception(RTC_GROUP_REQUIRED); } + // 1.3 校验当前用户是该群有效成员;防止仅凭 room 就拿到 LiveKit token 越权入会 + groupMemberService.validateMemberInGroup(call.getGroupId(), userId); // 2. 入参与表:已有记录切回 JOINED;不在记录则以 ACTIVE_JOIN 角色 INSERT LocalDateTime now = LocalDateTime.now(); diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/sensitiveword/ImSensitiveWordServiceImpl.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/sensitiveword/ImSensitiveWordServiceImpl.java index 3483526..57ed857 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/sensitiveword/ImSensitiveWordServiceImpl.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/sensitiveword/ImSensitiveWordServiceImpl.java @@ -87,13 +87,16 @@ public class ImSensitiveWordServiceImpl implements ImSensitiveWordService { @Override public ListenableFuture reload(Long tenantId, SensitiveWordBsCache oldValue) { - // 先比对 max(update_time);没变 → 复用旧实例,避免无谓地重建 trie - LocalDateTime currentMax = sensitiveWordMapper.selectMaxUpdateTime(tenantId); - if (Objects.equals(oldValue.getMaxUpdateTime(), currentMax)) { - return Futures.immediateFuture(oldValue); - } - // 变了 → 重新读词库并重建 trie - return Futures.immediateFuture(loadFresh(tenantId)); + // 异步刷新线程独立于业务线程,没有租户上下文;必须显式 TenantUtils.execute 设置,否则租户拦截器会按当前线程的空上下文拼 SQL + return Futures.immediateFuture(TenantUtils.execute(tenantId, () -> { + LocalDateTime currentMax = sensitiveWordMapper.selectMaxUpdateTime(tenantId); + // 没变 → 复用旧实例,避免无谓地重建 trie + if (Objects.equals(oldValue.getMaxUpdateTime(), currentMax)) { + return oldValue; + } + // 变了 → 重新读词库并重建 trie + return loadFresh(tenantId); + })); } }); diff --git a/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/friend/ImFriendRequestServiceImplTest.java b/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/friend/ImFriendRequestServiceImplTest.java index ab7363e..1123ab6 100644 --- a/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/friend/ImFriendRequestServiceImplTest.java +++ b/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/friend/ImFriendRequestServiceImplTest.java @@ -112,6 +112,24 @@ public class ImFriendRequestServiceImplTest extends BaseMockitoUnitTest { } @Test + public void testApplyFriend_blockedByPeerWhenMyselfDeleted() { + // 准备:我侧已删除(getFriendState=NONE),对方仍把我当好友但已拉黑 + ImFriendRequestApplyReqVO reqVO = new ImFriendRequestApplyReqVO(); + reqVO.setToUserId(2L).setDisplayName("老张").setAddSource(1); + when(friendService.getFriendState(1L, 2L)).thenReturn(ImFriendStateEnum.NONE.getState()); + ImFriendDO peerFriend = ImFriendDO.builder().userId(2L).friendUserId(1L) + .status(CommonStatusEnum.ENABLE.getStatus()).blocked(true).build(); + when(friendService.getFriend(2L, 1L)).thenReturn(peerFriend); + + // 调用 + 断言:必须拒掉,不能走 silentReAddFriend 绕过拉黑 + ServiceException exception = assertThrows(ServiceException.class, + () -> friendRequestService.applyFriend(1L, reqVO)); + assertEquals(FRIEND_REQUEST_BLOCKED_BY_PEER.getCode(), exception.getCode()); + verify(friendService, never()).silentReAddFriend(anyLong(), anyLong(), anyString(), anyInt()); + verify(friendRequestMapper, never()).insert(any(ImFriendRequestDO.class)); + } + + @Test public void testApplyFriend_insertNew() { // 准备:双方都无关系,且无历史申请 ImFriendRequestApplyReqVO reqVO = new ImFriendRequestApplyReqVO(); diff --git a/yudao-module-im/src/test/resources/sql/clean.sql b/yudao-module-im/src/test/resources/sql/clean.sql index b9bdd05..64ff0dd 100644 --- a/yudao-module-im/src/test/resources/sql/clean.sql +++ b/yudao-module-im/src/test/resources/sql/clean.sql @@ -10,4 +10,5 @@ DELETE FROM "im_face_pack_item"; DELETE FROM "im_face_user_item"; DELETE FROM "im_rtc_call"; DELETE FROM "im_rtc_participant"; +DELETE FROM "im_sensitive_word"; DELETE FROM "system_users"; diff --git a/yudao-module-im/src/test/resources/sql/create_tables.sql b/yudao-module-im/src/test/resources/sql/create_tables.sql index f10783a..7b09900 100644 --- a/yudao-module-im/src/test/resources/sql/create_tables.sql +++ b/yudao-module-im/src/test/resources/sql/create_tables.sql @@ -13,7 +13,8 @@ CREATE TABLE IF NOT EXISTS "im_private_message" ( "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, "deleted" bit NOT NULL DEFAULT FALSE, "tenant_id" bigint NOT NULL DEFAULT 0, - PRIMARY KEY ("id") + PRIMARY KEY ("id"), + CONSTRAINT "uk_im_private_message_sender_client" UNIQUE ("sender_id", "client_message_id", "tenant_id") ) COMMENT 'IM 私聊消息表'; CREATE TABLE IF NOT EXISTS "im_group_message" ( @@ -34,7 +35,8 @@ CREATE TABLE IF NOT EXISTS "im_group_message" ( "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, "deleted" bit NOT NULL DEFAULT FALSE, "tenant_id" bigint NOT NULL DEFAULT 0, - PRIMARY KEY ("id") + PRIMARY KEY ("id"), + CONSTRAINT "uk_im_group_message_sender_client" UNIQUE ("sender_id", "client_message_id", "tenant_id") ) COMMENT 'IM 群聊消息表'; CREATE TABLE IF NOT EXISTS "im_group" ( @@ -80,7 +82,8 @@ CREATE TABLE IF NOT EXISTS "im_group_member" ( "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, "deleted" bit NOT NULL DEFAULT FALSE, "tenant_id" bigint NOT NULL DEFAULT 0, - PRIMARY KEY ("id") + PRIMARY KEY ("id"), + CONSTRAINT "uk_im_group_member" UNIQUE ("group_id", "user_id", "tenant_id") ) COMMENT 'IM 群成员表'; CREATE TABLE IF NOT EXISTS "im_friend" ( @@ -287,6 +290,20 @@ CREATE TABLE IF NOT EXISTS "im_channel_message" ( PRIMARY KEY ("id") ) COMMENT 'IM 频道消息表'; +CREATE TABLE IF NOT EXISTS "im_sensitive_word" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY COMMENT '编号', + "word" varchar(128) NOT NULL COMMENT '敏感词', + "status" tinyint NOT NULL DEFAULT 0 COMMENT '状态;0 启用 1 停用', + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id"), + CONSTRAINT "uk_im_sensitive_word" UNIQUE ("word", "tenant_id") +) COMMENT 'IM 敏感词表'; + CREATE TABLE IF NOT EXISTS "system_users" ( "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "username" varchar(30) NOT NULL DEFAULT '', -- libgit2 0.21.4