mirror of
https://gitee.com/zhijiantianya/ruoyi-vue-pro.git
synced 2026-07-06 21:45:37 +08:00
♻️ refactor(friend): 重构好友关系相关逻辑,优化查询与缓存
This commit is contained in:
@ -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)));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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";
|
||||
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
}
|
||||
|
||||
@ -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 @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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -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,
|
||||
|
||||
Reference in New Issue
Block a user