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

This commit is contained in:
YunaiV
2026-04-22 08:39:53 +08:00
parent affc409022
commit 15a1bca721
6 changed files with 83 additions and 85 deletions

View File

@ -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<List<ImFriendRespVO>> getMyFriendList() {
return success(friendService.getMyFriendList(getLoginUserId()));
List<ImFriendDO> friends = friendService.getFriendList(getLoginUserId());
return success(buildFriendRespVOList(friends));
}
@GetMapping("/get")
@Operation(summary = "获得好友详情")
@Parameter(name = "friendUserId", description = "好友的用户编号", required = true, example = "2048")
public CommonResult<ImFriendRespVO> 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<ImFriendRespVO> buildFriendRespVOList(Collection<ImFriendDO> friends) {
if (CollUtil.isNotEmpty(friends)) {
return Collections.emptyList();
}
// 批量聚合 AdminUser 信息(昵称 / 头像),避免 N+1
Map<Long, AdminUserRespDTO> 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)));
}
}

View File

@ -15,4 +15,12 @@ public interface RedisKeyConstants {
*/
String GROUP_MESSAGE_READ = "im:group:message:read:%s";
/**
* 好友关系是否存在的缓存
* <p>
* KEY 格式friend:{userId}_{friendUserId}
* VALUE 数据类型Boolean
*/
String FRIEND = "friend";
}

View File

@ -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);
/**
* 获得当前用户的好友列表(含昵称/头像聚合
* 获得当前用户的好友列表(含已删除状态
* <p>
* 返回的列表里**包含已删除状态**,前端按 status 区分;
* 返回的列表里**包含已删除状态**,前端按 status 区分展示(已删除的置灰)
* 原因是本地已经建立会话的好友被单向删除后,前端仍需要展示头像/昵称。
*
* @param userId 用户编号
* @return 好友列表(含聚合后的用户信息
* @return 好友关系列表(含已删除的记录
*/
List<ImFriendRespVO> getMyFriendList(Long userId);
List<ImFriendDO> getFriendList(Long userId);
/**
* 查询一个好友(仅限本人视角的好友关系
* <p>
* 只返回"本人→对方"的关系记录;
* 对方不是本人好友时抛 {@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);
}

View File

@ -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<ImFriendDO>VO 组装在 controller 搞;
// TODO @AI方法改成 getEnabledFriendList 之类的,明确只返回未删除的好友;如果需要展示已删除的好友,提供一个 getAllFriendList 之类的方法。
@Override
public List<ImFriendRespVO> getMyFriendList(Long userId) {
List<ImFriendDO> friends = imFriendMapper.selectListByUserId(userId);
if (friends.isEmpty()) {
return Collections.emptyList();
}
// 批量聚合 AdminUser 信息(昵称 / 头像),避免 N+1
List<Long> friendUserIds = CollectionUtils.convertList(friends, ImFriendDO::getFriendUserId);
Map<Long, AdminUserRespDTO> 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<ImFriendDO> 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 @AIspring 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;
}
}

View File

@ -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());

View File

@ -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,