Commit 15a1bca721b15cd1dbdcd9503cedef6042e4edff

Authored by YunaiV
1 parent affc4090

♻️ refactor(friend): 重构好友关系相关逻辑,优化查询与缓存

yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/ImFriendController.java
1 1 package cn.iocoder.yudao.module.im.controller.admin.friend;
2 2  
  3 +import cn.hutool.core.collection.CollUtil;
3 4 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  5 +import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
  6 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
4 7 import cn.iocoder.yudao.module.im.controller.admin.friend.vo.ImFriendRespVO;
5 8 import cn.iocoder.yudao.module.im.controller.admin.friend.vo.ImFriendUpdateReqVO;
  9 +import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO;
6 10 import cn.iocoder.yudao.module.im.service.friend.ImFriendService;
  11 +import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
  12 +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
7 13 import io.swagger.v3.oas.annotations.Operation;
8 14 import io.swagger.v3.oas.annotations.Parameter;
9 15 import io.swagger.v3.oas.annotations.tags.Tag;
... ... @@ -13,9 +19,14 @@ import jakarta.validation.constraints.NotNull;
13 19 import org.springframework.validation.annotation.Validated;
14 20 import org.springframework.web.bind.annotation.*;
15 21  
  22 +import java.util.Collection;
  23 +import java.util.Collections;
16 24 import java.util.List;
  25 +import java.util.Map;
17 26  
18 27 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  28 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
  29 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.singleton;
19 30 import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getLoginUserId;
20 31  
21 32 @Tag(name = "管理后台 - IM 好友")
... ... @@ -27,17 +38,22 @@ public class ImFriendController {
27 38 @Resource
28 39 private ImFriendService friendService;
29 40  
  41 + @Resource
  42 + private AdminUserApi adminUserApi;
  43 +
30 44 @GetMapping("/list")
31 45 @Operation(summary = "获得当前登录用户的好友列表")
32 46 public CommonResult<List<ImFriendRespVO>> getMyFriendList() {
33   - return success(friendService.getMyFriendList(getLoginUserId()));
  47 + List<ImFriendDO> friends = friendService.getFriendList(getLoginUserId());
  48 + return success(buildFriendRespVOList(friends));
34 49 }
35 50  
36 51 @GetMapping("/get")
37 52 @Operation(summary = "获得好友详情")
38 53 @Parameter(name = "friendUserId", description = "好友的用户编号", required = true, example = "2048")
39 54 public CommonResult<ImFriendRespVO> getFriend(@RequestParam("friendUserId") Long friendUserId) {
40   - return success(friendService.getFriend(getLoginUserId(), friendUserId));
  55 + ImFriendDO friend = friendService.getFriend(getLoginUserId(), friendUserId);
  56 + return success(buildFriendRespVO(friend));
41 57 }
42 58  
43 59 @PostMapping("/add")
... ... @@ -65,4 +81,28 @@ public class ImFriendController {
65 81 return success(true);
66 82 }
67 83  
  84 + // ========== 私有方法:VO 组装 ==========
  85 +
  86 + private List<ImFriendRespVO> buildFriendRespVOList(Collection<ImFriendDO> friends) {
  87 + if (CollUtil.isNotEmpty(friends)) {
  88 + return Collections.emptyList();
  89 + }
  90 + // 批量聚合 AdminUser 信息(昵称 / 头像),避免 N+1
  91 + Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
  92 + convertList(friends, ImFriendDO::getFriendUserId));
  93 + return convertList(friends, f -> {
  94 + ImFriendRespVO vo = BeanUtils.toBean(f, ImFriendRespVO.class);
  95 + MapUtils.findAndThen(userMap, f.getFriendUserId(), user ->
  96 + vo.setNickname(user.getNickname()).setAvatar(user.getAvatar()));
  97 + return vo;
  98 + });
  99 + }
  100 +
  101 + private ImFriendRespVO buildFriendRespVO(ImFriendDO friend) {
  102 + if (friend == null) {
  103 + return null;
  104 + }
  105 + return CollUtil.getFirst(buildFriendRespVOList(singleton(friend)));
  106 + }
  107 +
68 108 }
... ...
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 {
15 15 */
16 16 String GROUP_MESSAGE_READ = "im:group:message:read:%s";
17 17  
  18 + /**
  19 + * 好友关系是否存在的缓存
  20 + * <p>
  21 + * KEY 格式:friend:{userId}_{friendUserId}
  22 + * VALUE 数据类型:Boolean
  23 + */
  24 + String FRIEND = "friend";
  25 +
18 26 }
... ...
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendService.java
1 1 package cn.iocoder.yudao.module.im.service.friend;
2 2  
3   -import cn.iocoder.yudao.module.im.controller.admin.friend.vo.ImFriendRespVO;
4 3 import cn.iocoder.yudao.module.im.controller.admin.friend.vo.ImFriendUpdateReqVO;
5 4 import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO;
6 5  
... ... @@ -14,35 +13,33 @@ import java.util.List;
14 13 public interface ImFriendService {
15 14  
16 15 /**
17   - * 校验两个用户是否是好友关系(未删除状态)
  16 + * 判断两个用户是否是好友关系(未删除状态)
18 17 *
19 18 * @param userId 用户编号
20 19 * @param friendUserId 好友用户编号
  20 + * @return 是否好友
21 21 */
22   - void validateFriendExists(Long userId, Long friendUserId);
  22 + boolean isFriend(Long userId, Long friendUserId);
23 23  
24 24 /**
25   - * 获得当前用户的好友列表(含昵称/头像聚合)
  25 + * 获得当前用户的好友列表(含已删除状态)
26 26 * <p>
27   - * 返回的列表里**包含已删除状态**,前端按 status 区分;
  27 + * 返回的列表里**包含已删除状态**,前端按 status 区分展示(已删除的置灰);
28 28 * 原因是本地已经建立会话的好友被单向删除后,前端仍需要展示头像/昵称。
29 29 *
30 30 * @param userId 用户编号
31   - * @return 好友列表(含聚合后的用户信息)
  31 + * @return 好友关系列表(含已删除的记录)
32 32 */
33   - List<ImFriendRespVO> getMyFriendList(Long userId);
  33 + List<ImFriendDO> getFriendList(Long userId);
34 34  
35 35 /**
36   - * 查询一个好友(仅限本人视角的好友关系)
37   - * <p>
38   - * 只返回"本人→对方"的关系记录;
39   - * 对方不是本人好友时抛 {@link cn.iocoder.yudao.module.im.enums.ErrorCodeConstants#FRIEND_NOT_FRIEND}。
  36 + * 查询一个好友关系记录(仅限本人视角)
40 37 *
41 38 * @param userId 当前登录用户编号
42 39 * @param friendUserId 目标用户编号
43   - * @return 好友信息(含聚合后的用户信息)
  40 + * @return 好友关系记录,不存在返回 null
44 41 */
45   - ImFriendRespVO getFriend(Long userId, Long friendUserId);
  42 + ImFriendDO getFriend(Long userId, Long friendUserId);
46 43  
47 44 /**
48 45 * 添加好友(双向绑定)
... ... @@ -77,13 +74,5 @@ public interface ImFriendService {
77 74 */
78 75 void updateFriend(Long userId, ImFriendUpdateReqVO reqVO);
79 76  
80   - /**
81   - * 查询某个用户视角下的好友关系记录(仅单边)
82   - *
83   - * @param userId 用户编号
84   - * @param friendUserId 好友用户编号
85   - * @return 关系记录,不存在返回 null
86   - */
87   - ImFriendDO getFriendDO(Long userId, Long friendUserId);
88 77  
89 78 }
... ...
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImpl.java
1 1 package cn.iocoder.yudao.module.im.service.friend;
2 2  
3 3 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
4   -import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
5   -import cn.iocoder.yudao.module.im.controller.admin.friend.vo.ImFriendRespVO;
6 4 import cn.iocoder.yudao.module.im.controller.admin.friend.vo.ImFriendUpdateReqVO;
7 5 import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO;
8 6 import cn.iocoder.yudao.module.im.dal.mysql.friend.ImFriendMapper;
9 7 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
10   -import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
11 8 import jakarta.annotation.Resource;
  9 +import org.springframework.cache.annotation.CacheEvict;
  10 +import org.springframework.cache.annotation.Cacheable;
12 11 import org.springframework.stereotype.Service;
13 12 import org.springframework.transaction.annotation.Transactional;
14 13 import org.springframework.validation.annotation.Validated;
15 14  
16 15 import java.time.LocalDateTime;
17   -import java.util.Collections;
18 16 import java.util.List;
19   -import java.util.Map;
20 17 import java.util.Objects;
21 18  
22 19 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
23   -import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.FRIEND_ADD_SELF;
24   -import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.FRIEND_NOT_FRIEND;
  20 +import static cn.iocoder.yudao.module.im.dal.redis.RedisKeyConstants.FRIEND;
  21 +import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*;
25 22  
26 23 /**
27 24 * IM 好友关系 Service 实现类
... ... @@ -39,40 +36,19 @@ public class ImFriendServiceImpl implements ImFriendService {
39 36 private AdminUserApi adminUserApi;
40 37  
41 38 @Override
42   - public void validateFriendExists(Long userId, Long friendUserId) {
  39 + @Cacheable(cacheNames = FRIEND, key = "#userId + '_' + #friendUserId", unless = "#result == null")
  40 + public boolean isFriend(Long userId, Long friendUserId) {
43 41 ImFriendDO friend = imFriendMapper.selectByUserIdAndFriendUserId(userId, friendUserId);
44   - if (friend == null || CommonStatusEnum.isDisable(friend.getStatus())) {
45   - throw exception(FRIEND_NOT_FRIEND);
46   - }
47   - }
48   -
49   - // TODO @AI:这里只负责返回 List<ImFriendDO>;VO 组装在 controller 搞;
50   - // TODO @AI:方法改成 getEnabledFriendList 之类的,明确只返回未删除的好友;如果需要展示已删除的好友,提供一个 getAllFriendList 之类的方法。
51   - @Override
52   - public List<ImFriendRespVO> getMyFriendList(Long userId) {
53   - List<ImFriendDO> friends = imFriendMapper.selectListByUserId(userId);
54   - if (friends.isEmpty()) {
55   - return Collections.emptyList();
56   - }
57   - // 批量聚合 AdminUser 信息(昵称 / 头像),避免 N+1
58   - List<Long> friendUserIds = CollectionUtils.convertList(friends, ImFriendDO::getFriendUserId);
59   - Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(friendUserIds);
60   - return CollectionUtils.convertList(friends, f -> buildRespVO(f, userMap.get(f.getFriendUserId())));
  42 + return friend != null && CommonStatusEnum.isEnable(friend.getStatus());
61 43 }
62 44  
63   - // TODO @AI:这里只负责返回 ImFriendDO;
64 45 @Override
65   - public ImFriendRespVO getFriend(Long userId, Long friendUserId) {
66   - ImFriendDO friend = imFriendMapper.selectByUserIdAndFriendUserId(userId, friendUserId);
67   - if (friend == null) {
68   - throw exception(FRIEND_NOT_FRIEND);
69   - }
70   - AdminUserRespDTO user = adminUserApi.getUser(friendUserId);
71   - return buildRespVO(friend, user);
  46 + public List<ImFriendDO> getFriendList(Long userId) {
  47 + return imFriendMapper.selectListByUserId(userId);
72 48 }
73 49  
74 50 @Override
75   - public ImFriendDO getFriendDO(Long userId, Long friendUserId) {
  51 + public ImFriendDO getFriend(Long userId, Long friendUserId) {
76 52 return imFriendMapper.selectByUserIdAndFriendUserId(userId, friendUserId);
77 53 }
78 54  
... ... @@ -83,6 +59,7 @@ public class ImFriendServiceImpl implements ImFriendService {
83 59 if (friend == null) {
84 60 throw exception(FRIEND_NOT_FRIEND);
85 61 }
  62 +
86 63 // 2. 更新好友属性(目前仅免打扰)
87 64 ImFriendDO updateObj = new ImFriendDO();
88 65 updateObj.setId(friend.getId());
... ... @@ -131,8 +108,8 @@ public class ImFriendServiceImpl implements ImFriendService {
131 108 * - 已存在且 ENABLE:无需重复操作
132 109 * - 已存在但 DISABLE:恢复关系(status=ENABLE,刷新 addTime,清空 deleteTime)
133 110 */
134   - // TODO @AI:spring cache;参考别的模块的写法;
135   - private void bindFriend(Long userId, Long friendUserId) {
  111 + @CacheEvict(cacheNames = FRIEND, key = "#userId + '_' + #friendUserId")
  112 + public void bindFriend(Long userId, Long friendUserId) {
136 113 ImFriendDO exists = imFriendMapper.selectByUserIdAndFriendUserId(userId, friendUserId);
137 114 if (exists != null) {
138 115 if (CommonStatusEnum.isEnable(exists.getStatus())) {
... ... @@ -161,7 +138,8 @@ public class ImFriendServiceImpl implements ImFriendService {
161 138 /**
162 139 * 单向解除好友关系(status 设为 DISABLE,记录 deleteTime)
163 140 */
164   - private void unbindFriend(Long userId, Long friendUserId) {
  141 + @CacheEvict(cacheNames = FRIEND, key = "#userId + '_' + #friendUserId")
  142 + public void unbindFriend(Long userId, Long friendUserId) {
165 143 ImFriendDO exists = imFriendMapper.selectByUserIdAndFriendUserId(userId, friendUserId);
166 144 if (exists == null || CommonStatusEnum.isDisable(exists.getStatus())) {
167 145 return;
... ... @@ -173,22 +151,4 @@ public class ImFriendServiceImpl implements ImFriendService {
173 151 imFriendMapper.updateById(updateObj);
174 152 }
175 153  
176   - /**
177   - * 聚合 ImFriendDO + AdminUser 构造响应 VO
178   - */
179   - private ImFriendRespVO buildRespVO(ImFriendDO friend, AdminUserRespDTO user) {
180   - ImFriendRespVO vo = new ImFriendRespVO();
181   - vo.setId(friend.getId());
182   - vo.setFriendUserId(friend.getFriendUserId());
183   - vo.setMuted(friend.getMuted());
184   - vo.setStatus(friend.getStatus());
185   - vo.setAddTime(friend.getAddTime());
186   - vo.setDeleteTime(friend.getDeleteTime());
187   - if (user != null) {
188   - vo.setNickname(user.getNickname());
189   - vo.setAvatar(user.getAvatar());
190   - }
191   - return vo;
192   - }
193   -
194 154 }
... ...
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 {
67 67 return existing;
68 68 }
69 69 // 1.2 好友校验
70   - friendService.validateFriendExists(senderId, reqVO.getReceiverId());
  70 + if (!friendService.isFriend(senderId, reqVO.getReceiverId())) {
  71 + throw exception(FRIEND_NOT_FRIEND);
  72 + }
71 73 // 1.3 文本消息敏感词过滤
72 74 if (ImMessageTypeEnum.TEXT.getType().equals(reqVO.getType())) {
73 75 sensitiveWordService.validateText(reqVO.getContent());
... ...
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 {
81 81 assertNotNull(result.getSendTime());
82 82  
83 83 // 验证调用
84   - verify(imFriendService).validateFriendExists(1L, 2L);
  84 + verify(imFriendService).isFriend(1L, 2L);
85 85 verify(imSensitiveWordService).validateText(reqVO.getContent());
86 86 verify(imPrivateMessageMapper).insert(any(ImPrivateMessageDO.class));
87 87 verify(webSocketMessageSender, times(2)).sendObject(anyInt(), anyLong(), anyString(), any());
... ... @@ -114,8 +114,7 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest {
114 114 ImPrivateMessageSendReqVO reqVO = buildSendReqVO();
115 115 when(imPrivateMessageMapper.selectBySenderIdAndClientMessageId(1L, "test-uuid-001"))
116 116 .thenReturn(null);
117   - doThrow(new ServiceException(FRIEND_NOT_FRIEND))
118   - .when(imFriendService).validateFriendExists(1L, 2L);
  117 + when(imFriendService.isFriend(1L, 2L)).thenReturn(false);
119 118  
120 119 // 调用并断言
121 120 ServiceException exception = assertThrows(ServiceException.class,
... ...