From 15a1bca721b15cd1dbdcd9503cedef6042e4edff Mon Sep 17 00:00:00 2001 From: YunaiV Date: Wed, 22 Apr 2026 08:39:53 +0800 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(friend):=20?= =?UTF-8?q?=E9=87=8D=E6=9E=84=E5=A5=BD=E5=8F=8B=E5=85=B3=E7=B3=BB=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E9=80=BB=E8=BE=91=EF=BC=8C=E4=BC=98=E5=8C=96=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2=E4=B8=8E=E7=BC=93=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/friend/ImFriendController.java | 44 ++++++++++- .../im/dal/redis/RedisKeyConstants.java | 8 ++ .../im/service/friend/ImFriendService.java | 31 +++----- .../service/friend/ImFriendServiceImpl.java | 76 +++++-------------- .../message/ImPrivateMessageServiceImpl.java | 4 +- .../ImPrivateMessageServiceImplTest.java | 5 +- 6 files changed, 83 insertions(+), 85 deletions(-) diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/ImFriendController.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/ImFriendController.java index 48fe16cd4e..629ab35f30 100644 --- a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/ImFriendController.java +++ b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/ImFriendController.java @@ -1,9 +1,15 @@ package cn.iocoder.yudao.module.im.controller.admin.friend; +import cn.hutool.core.collection.CollUtil; import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.im.controller.admin.friend.vo.ImFriendRespVO; import cn.iocoder.yudao.module.im.controller.admin.friend.vo.ImFriendUpdateReqVO; +import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO; import cn.iocoder.yudao.module.im.service.friend.ImFriendService; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; @@ -13,9 +19,14 @@ import jakarta.validation.constraints.NotNull; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +import java.util.Collection; +import java.util.Collections; import java.util.List; +import java.util.Map; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.singleton; import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getLoginUserId; @Tag(name = "管理后台 - IM 好友") @@ -27,17 +38,22 @@ public class ImFriendController { @Resource private ImFriendService friendService; + @Resource + private AdminUserApi adminUserApi; + @GetMapping("/list") @Operation(summary = "获得当前登录用户的好友列表") public CommonResult> getMyFriendList() { - return success(friendService.getMyFriendList(getLoginUserId())); + List friends = friendService.getFriendList(getLoginUserId()); + return success(buildFriendRespVOList(friends)); } @GetMapping("/get") @Operation(summary = "获得好友详情") @Parameter(name = "friendUserId", description = "好友的用户编号", required = true, example = "2048") public CommonResult getFriend(@RequestParam("friendUserId") Long friendUserId) { - return success(friendService.getFriend(getLoginUserId(), friendUserId)); + ImFriendDO friend = friendService.getFriend(getLoginUserId(), friendUserId); + return success(buildFriendRespVO(friend)); } @PostMapping("/add") @@ -65,4 +81,28 @@ public class ImFriendController { return success(true); } + // ========== 私有方法:VO 组装 ========== + + private List buildFriendRespVOList(Collection friends) { + if (CollUtil.isNotEmpty(friends)) { + return Collections.emptyList(); + } + // 批量聚合 AdminUser 信息(昵称 / 头像),避免 N+1 + Map userMap = adminUserApi.getUserMap( + convertList(friends, ImFriendDO::getFriendUserId)); + return convertList(friends, f -> { + ImFriendRespVO vo = BeanUtils.toBean(f, ImFriendRespVO.class); + MapUtils.findAndThen(userMap, f.getFriendUserId(), user -> + vo.setNickname(user.getNickname()).setAvatar(user.getAvatar())); + return vo; + }); + } + + private ImFriendRespVO buildFriendRespVO(ImFriendDO friend) { + if (friend == null) { + return null; + } + return CollUtil.getFirst(buildFriendRespVOList(singleton(friend))); + } + } diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/redis/RedisKeyConstants.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/redis/RedisKeyConstants.java index 9e07fbb0a8..209e46d89c 100644 --- a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/redis/RedisKeyConstants.java +++ b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/redis/RedisKeyConstants.java @@ -15,4 +15,12 @@ public interface RedisKeyConstants { */ String GROUP_MESSAGE_READ = "im:group:message:read:%s"; + /** + * 好友关系是否存在的缓存 + *

+ * KEY 格式:friend:{userId}_{friendUserId} + * VALUE 数据类型:Boolean + */ + String FRIEND = "friend"; + } diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendService.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendService.java index 4a9cb0a19e..cde0a60b8d 100644 --- a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendService.java +++ b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendService.java @@ -1,6 +1,5 @@ package cn.iocoder.yudao.module.im.service.friend; -import cn.iocoder.yudao.module.im.controller.admin.friend.vo.ImFriendRespVO; import cn.iocoder.yudao.module.im.controller.admin.friend.vo.ImFriendUpdateReqVO; import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO; @@ -14,35 +13,33 @@ import java.util.List; public interface ImFriendService { /** - * 校验两个用户是否是好友关系(未删除状态) + * 判断两个用户是否是好友关系(未删除状态) * * @param userId 用户编号 * @param friendUserId 好友用户编号 + * @return 是否好友 */ - void validateFriendExists(Long userId, Long friendUserId); + boolean isFriend(Long userId, Long friendUserId); /** - * 获得当前用户的好友列表(含昵称/头像聚合) + * 获得当前用户的好友列表(含已删除状态) *

- * 返回的列表里**包含已删除状态**,前端按 status 区分; + * 返回的列表里**包含已删除状态**,前端按 status 区分展示(已删除的置灰); * 原因是本地已经建立会话的好友被单向删除后,前端仍需要展示头像/昵称。 * * @param userId 用户编号 - * @return 好友列表(含聚合后的用户信息) + * @return 好友关系列表(含已删除的记录) */ - List getMyFriendList(Long userId); + List getFriendList(Long userId); /** - * 查询一个好友(仅限本人视角的好友关系) - *

- * 只返回"本人→对方"的关系记录; - * 对方不是本人好友时抛 {@link cn.iocoder.yudao.module.im.enums.ErrorCodeConstants#FRIEND_NOT_FRIEND}。 + * 查询一个好友关系记录(仅限本人视角) * * @param userId 当前登录用户编号 * @param friendUserId 目标用户编号 - * @return 好友信息(含聚合后的用户信息) + * @return 好友关系记录,不存在返回 null */ - ImFriendRespVO getFriend(Long userId, Long friendUserId); + ImFriendDO getFriend(Long userId, Long friendUserId); /** * 添加好友(双向绑定) @@ -77,13 +74,5 @@ public interface ImFriendService { */ void updateFriend(Long userId, ImFriendUpdateReqVO reqVO); - /** - * 查询某个用户视角下的好友关系记录(仅单边) - * - * @param userId 用户编号 - * @param friendUserId 好友用户编号 - * @return 关系记录,不存在返回 null - */ - ImFriendDO getFriendDO(Long userId, Long friendUserId); } diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImpl.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImpl.java index fa0d9c402d..cc682c6619 100644 --- a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImpl.java +++ b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImpl.java @@ -1,27 +1,24 @@ package cn.iocoder.yudao.module.im.service.friend; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; -import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; -import cn.iocoder.yudao.module.im.controller.admin.friend.vo.ImFriendRespVO; import cn.iocoder.yudao.module.im.controller.admin.friend.vo.ImFriendUpdateReqVO; import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO; import cn.iocoder.yudao.module.im.dal.mysql.friend.ImFriendMapper; import cn.iocoder.yudao.module.system.api.user.AdminUserApi; -import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; import jakarta.annotation.Resource; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import java.time.LocalDateTime; -import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.Objects; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; -import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.FRIEND_ADD_SELF; -import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.FRIEND_NOT_FRIEND; +import static cn.iocoder.yudao.module.im.dal.redis.RedisKeyConstants.FRIEND; +import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*; /** * IM 好友关系 Service 实现类 @@ -39,40 +36,19 @@ public class ImFriendServiceImpl implements ImFriendService { private AdminUserApi adminUserApi; @Override - public void validateFriendExists(Long userId, Long friendUserId) { + @Cacheable(cacheNames = FRIEND, key = "#userId + '_' + #friendUserId", unless = "#result == null") + public boolean isFriend(Long userId, Long friendUserId) { ImFriendDO friend = imFriendMapper.selectByUserIdAndFriendUserId(userId, friendUserId); - if (friend == null || CommonStatusEnum.isDisable(friend.getStatus())) { - throw exception(FRIEND_NOT_FRIEND); - } - } - - // TODO @AI:这里只负责返回 List;VO 组装在 controller 搞; - // TODO @AI:方法改成 getEnabledFriendList 之类的,明确只返回未删除的好友;如果需要展示已删除的好友,提供一个 getAllFriendList 之类的方法。 - @Override - public List getMyFriendList(Long userId) { - List friends = imFriendMapper.selectListByUserId(userId); - if (friends.isEmpty()) { - return Collections.emptyList(); - } - // 批量聚合 AdminUser 信息(昵称 / 头像),避免 N+1 - List friendUserIds = CollectionUtils.convertList(friends, ImFriendDO::getFriendUserId); - Map userMap = adminUserApi.getUserMap(friendUserIds); - return CollectionUtils.convertList(friends, f -> buildRespVO(f, userMap.get(f.getFriendUserId()))); - } - - // TODO @AI:这里只负责返回 ImFriendDO; - @Override - public ImFriendRespVO getFriend(Long userId, Long friendUserId) { - ImFriendDO friend = imFriendMapper.selectByUserIdAndFriendUserId(userId, friendUserId); - if (friend == null) { - throw exception(FRIEND_NOT_FRIEND); - } - AdminUserRespDTO user = adminUserApi.getUser(friendUserId); - return buildRespVO(friend, user); + return friend != null && CommonStatusEnum.isEnable(friend.getStatus()); } @Override - public ImFriendDO getFriendDO(Long userId, Long friendUserId) { + public List getFriendList(Long userId) { + return imFriendMapper.selectListByUserId(userId); + } + + @Override + public ImFriendDO getFriend(Long userId, Long friendUserId) { return imFriendMapper.selectByUserIdAndFriendUserId(userId, friendUserId); } @@ -83,6 +59,7 @@ public class ImFriendServiceImpl implements ImFriendService { if (friend == null) { throw exception(FRIEND_NOT_FRIEND); } + // 2. 更新好友属性(目前仅免打扰) ImFriendDO updateObj = new ImFriendDO(); updateObj.setId(friend.getId()); @@ -131,8 +108,8 @@ public class ImFriendServiceImpl implements ImFriendService { * - 已存在且 ENABLE:无需重复操作 * - 已存在但 DISABLE:恢复关系(status=ENABLE,刷新 addTime,清空 deleteTime) */ - // TODO @AI:spring cache;参考别的模块的写法; - private void bindFriend(Long userId, Long friendUserId) { + @CacheEvict(cacheNames = FRIEND, key = "#userId + '_' + #friendUserId") + public void bindFriend(Long userId, Long friendUserId) { ImFriendDO exists = imFriendMapper.selectByUserIdAndFriendUserId(userId, friendUserId); if (exists != null) { if (CommonStatusEnum.isEnable(exists.getStatus())) { @@ -161,7 +138,8 @@ public class ImFriendServiceImpl implements ImFriendService { /** * 单向解除好友关系(status 设为 DISABLE,记录 deleteTime) */ - private void unbindFriend(Long userId, Long friendUserId) { + @CacheEvict(cacheNames = FRIEND, key = "#userId + '_' + #friendUserId") + public void unbindFriend(Long userId, Long friendUserId) { ImFriendDO exists = imFriendMapper.selectByUserIdAndFriendUserId(userId, friendUserId); if (exists == null || CommonStatusEnum.isDisable(exists.getStatus())) { return; @@ -173,22 +151,4 @@ public class ImFriendServiceImpl implements ImFriendService { imFriendMapper.updateById(updateObj); } - /** - * 聚合 ImFriendDO + AdminUser 构造响应 VO - */ - private ImFriendRespVO buildRespVO(ImFriendDO friend, AdminUserRespDTO user) { - ImFriendRespVO vo = new ImFriendRespVO(); - vo.setId(friend.getId()); - vo.setFriendUserId(friend.getFriendUserId()); - vo.setMuted(friend.getMuted()); - vo.setStatus(friend.getStatus()); - vo.setAddTime(friend.getAddTime()); - vo.setDeleteTime(friend.getDeleteTime()); - if (user != null) { - vo.setNickname(user.getNickname()); - vo.setAvatar(user.getAvatar()); - } - return vo; - } - } diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageServiceImpl.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageServiceImpl.java index 23b1b2fabd..ba4627cdbf 100644 --- a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageServiceImpl.java +++ b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageServiceImpl.java @@ -67,7 +67,9 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService { return existing; } // 1.2 好友校验 - friendService.validateFriendExists(senderId, reqVO.getReceiverId()); + if (!friendService.isFriend(senderId, reqVO.getReceiverId())) { + throw exception(FRIEND_NOT_FRIEND); + } // 1.3 文本消息敏感词过滤 if (ImMessageTypeEnum.TEXT.getType().equals(reqVO.getType())) { sensitiveWordService.validateText(reqVO.getContent()); diff --git a/yudao-module-im/yudao-module-im-biz/src/test/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageServiceImplTest.java b/yudao-module-im/yudao-module-im-biz/src/test/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageServiceImplTest.java index 9ecf8ea214..bfb4c1b66c 100644 --- a/yudao-module-im/yudao-module-im-biz/src/test/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageServiceImplTest.java +++ b/yudao-module-im/yudao-module-im-biz/src/test/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageServiceImplTest.java @@ -81,7 +81,7 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest { assertNotNull(result.getSendTime()); // 验证调用 - verify(imFriendService).validateFriendExists(1L, 2L); + verify(imFriendService).isFriend(1L, 2L); verify(imSensitiveWordService).validateText(reqVO.getContent()); verify(imPrivateMessageMapper).insert(any(ImPrivateMessageDO.class)); verify(webSocketMessageSender, times(2)).sendObject(anyInt(), anyLong(), anyString(), any()); @@ -114,8 +114,7 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest { ImPrivateMessageSendReqVO reqVO = buildSendReqVO(); when(imPrivateMessageMapper.selectBySenderIdAndClientMessageId(1L, "test-uuid-001")) .thenReturn(null); - doThrow(new ServiceException(FRIEND_NOT_FRIEND)) - .when(imFriendService).validateFriendExists(1L, 2L); + when(imFriendService.isFriend(1L, 2L)).thenReturn(false); // 调用并断言 ServiceException exception = assertThrows(ServiceException.class,