Commit cee0688c30313accfe4c1f0450b721facffd5ad0

Authored by YunaiV
1 parent 63d00c2b

✨ feat(im): 增加好友申请的逻辑(v1.3:修复各种边界情况,包括静默添加好友)

yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/ErrorCodeConstants.java
@@ -53,6 +53,8 @@ public interface ErrorCodeConstants { @@ -53,6 +53,8 @@ public interface ErrorCodeConstants {
53 ErrorCode FRIEND_REQUEST_HANDLED = new ErrorCode(1_040_610_002, "好友申请已处理"); 53 ErrorCode FRIEND_REQUEST_HANDLED = new ErrorCode(1_040_610_002, "好友申请已处理");
54 ErrorCode FRIEND_REQUEST_NOT_TO_ME = new ErrorCode(1_040_610_003, "不能处理别人的好友申请"); 54 ErrorCode FRIEND_REQUEST_NOT_TO_ME = new ErrorCode(1_040_610_003, "不能处理别人的好友申请");
55 ErrorCode FRIEND_REQUEST_TARGET_INVALID = new ErrorCode(1_040_610_004, "好友申请的对方不存在或已禁用"); 55 ErrorCode FRIEND_REQUEST_TARGET_INVALID = new ErrorCode(1_040_610_004, "好友申请的对方不存在或已禁用");
  56 + ErrorCode FRIEND_REQUEST_ALREADY_FRIEND = new ErrorCode(1_040_610_005, "您已是 TA 的好友,无需重复添加");
  57 + ErrorCode FRIEND_REQUEST_BLOCKED_BY_PEER = new ErrorCode(1_040_610_006, "您已被对方拉入黑名单,无法添加为好友");
56 58
57 // ========== 敏感词 (1-040-700-000) ========== 59 // ========== 敏感词 (1-040-700-000) ==========
58 ErrorCode SENSITIVE_WORD_NOT_EXISTS = new ErrorCode(1_040_700_000, "敏感词不存在"); 60 ErrorCode SENSITIVE_WORD_NOT_EXISTS = new ErrorCode(1_040_700_000, "敏感词不存在");
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/friend/ImFriendAddSourceEnum.java
@@ -17,9 +17,8 @@ import java.util.Arrays; @@ -17,9 +17,8 @@ import java.util.Arrays;
17 @Getter 17 @Getter
18 public enum ImFriendAddSourceEnum implements ArrayValuable<Integer> { 18 public enum ImFriendAddSourceEnum implements ArrayValuable<Integer> {
19 19
20 - // TODO DONE @AI:GROUP 提前到 source=2(先实现);BUSINESS_CARD 简化为 CARD  
21 SEARCH(1, "搜索"), // FriendAddDialog 搜索流程 20 SEARCH(1, "搜索"), // FriendAddDialog 搜索流程
22 - GROUP(2, "群聊"), // TODO @AI:群成员主页加好友,待落 UI 入口 21 + GROUP(2, "群聊"), // 群成员主页 → UserInfo「加为好友」入口
23 QR_CODE(3, "扫码"), // TODO @芋艿:后续实现扫码加好友 22 QR_CODE(3, "扫码"), // TODO @芋艿:后续实现扫码加好友
24 CARD(4, "名片"); // TODO @芋艿:后续实现通过名片加好友,类似微信的「扫一扫 - 名片」功能,或者「通讯录 - 推荐好友」功能 23 CARD(4, "名片"); // TODO @芋艿:后续实现通过名片加好友,类似微信的「扫一扫 - 名片」功能,或者「通讯录 - 推荐好友」功能
25 24
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendRequestServiceImpl.java
1 package cn.iocoder.yudao.module.im.service.friend; 1 package cn.iocoder.yudao.module.im.service.friend;
2 2
3 -import cn.hutool.core.util.BooleanUtil;  
4 import cn.hutool.core.util.ObjUtil; 3 import cn.hutool.core.util.ObjUtil;
5 import cn.hutool.extra.spring.SpringUtil; 4 import cn.hutool.extra.spring.SpringUtil;
6 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; 5 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
@@ -10,6 +9,7 @@ import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO; @@ -10,6 +9,7 @@ import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO;
10 import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendRequestDO; 9 import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendRequestDO;
11 import cn.iocoder.yudao.module.im.dal.mysql.friend.ImFriendRequestMapper; 10 import cn.iocoder.yudao.module.im.dal.mysql.friend.ImFriendRequestMapper;
12 import cn.iocoder.yudao.module.im.enums.friend.ImFriendRequestHandleResultEnum; 11 import cn.iocoder.yudao.module.im.enums.friend.ImFriendRequestHandleResultEnum;
  12 +import cn.iocoder.yudao.module.im.enums.friend.ImFriendStateEnum;
13 import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum; 13 import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
14 import cn.iocoder.yudao.module.im.framework.config.ImProperties; 14 import cn.iocoder.yudao.module.im.framework.config.ImProperties;
15 import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService; 15 import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService;
@@ -67,21 +67,21 @@ public class ImFriendRequestServiceImpl implements ImFriendRequestService { @@ -67,21 +67,21 @@ public class ImFriendRequestServiceImpl implements ImFriendRequestService {
67 } 67 }
68 // 1.2 校验对方存在且启用 68 // 1.2 校验对方存在且启用
69 adminUserApi.validateUser(toUserId); 69 adminUserApi.validateUser(toUserId);
70 - // 1.3 已是好友:直接幂等返回  
71 - ImFriendDO friend = friendService.getFriend(fromUserId, toUserId);  
72 - // TODO @AI:已经是好友,直接提示报错;不用触发;  
73 - if (friend != null && CommonStatusEnum.isEnable(friend.getStatus())) {  
74 - return null; 70 + // 1.3 已是好友 / 被对方拉黑:直接报错(state 一次拿到双向状态,省两次单边查询)
  71 + ImFriendStateEnum state = friendService.getFriendState(fromUserId, toUserId);
  72 + if (state == ImFriendStateEnum.FRIEND) {
  73 + throw exception(FRIEND_REQUEST_ALREADY_FRIEND);
  74 + }
  75 + if (state == ImFriendStateEnum.BLOCKED) {
  76 + throw exception(FRIEND_REQUEST_BLOCKED_BY_PEER);
75 } 77 }
76 - // 1.4 被对方拉黑:静默成功,不暴露  
77 - // TODO @AI:是不是 peerFriend?这样更好理解。  
78 - // TODO @AI:是不是应该提示被拉黑了?返回 null 给前端很奇怪。  
79 - ImFriendDO reverse = friendService.getFriend(toUserId, fromUserId);  
80 - if (reverse != null && BooleanUtil.isTrue(reverse.getBlocked())) {  
81 - log.info("[applyFriend][fromUserId({}) 被 toUserId({}) 拉黑,静默忽略]", fromUserId, toUserId); 78 + // 1.4 单向好友(我已删 + 对方仍把我当好友):静默重新启用我侧关系,避免对方感知我曾删除
  79 + // TODO DONE @AI:前端 FriendAddDialog 按 requestId 是否为 null 区分提示文案:null → 「已添加为好友」;非 null → 「等待对方验证」
  80 + ImFriendDO peerFriend = friendService.getFriend(toUserId, fromUserId);
  81 + if (peerFriend != null && CommonStatusEnum.isEnable(peerFriend.getStatus())) {
  82 + friendService.silentReAddFriend(fromUserId, toUserId, reqVO.getDisplayName(), reqVO.getAddSource());
82 return null; 83 return null;
83 } 84 }
84 - // TODO @AI:如果是单向好友(对方没删除);我删除了;是不是直接更新状态就好了;(不然会被对方发现!)  
85 85
86 // 2. 落库:复用最新一条未处理记录;否则新建 86 // 2. 落库:复用最新一条未处理记录;否则新建
87 ImFriendRequestDO request = friendRequestMapper.selectLatestByFromUserIdAndToUserId(fromUserId, toUserId); 87 ImFriendRequestDO request = friendRequestMapper.selectLatestByFromUserIdAndToUserId(fromUserId, toUserId);
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendService.java
@@ -55,6 +55,19 @@ public interface ImFriendService { @@ -55,6 +55,19 @@ public interface ImFriendService {
55 */ 55 */
56 void becomeFriends(ImFriendRequestDO request); 56 void becomeFriends(ImFriendRequestDO request);
57 57
  58 + /**
  59 + * 单向静默重新建立好友关系
  60 + * <p>
  61 + * 仅用于 {@link ImFriendRequestService#applyFriend} 在「我已删除 + 对方仍把我当好友」场景:
  62 + * 直接恢复 userId 这边的 friend 记录,不走申请审批;不下发 TIP / 不通知对方,仅 FRIEND_ADD 给 userId 多端,避免对方感知我曾删除。
  63 + *
  64 + * @param userId 当前用户编号
  65 + * @param friendUserId 对方用户编号
  66 + * @param displayName 备注(取自申请 VO)
  67 + * @param addSource 添加来源(取自申请 VO)
  68 + */
  69 + void silentReAddFriend(Long userId, Long friendUserId, String displayName, Integer addSource);
  70 +
58 // ==================== 用户端 ==================== 71 // ==================== 用户端 ====================
59 72
60 /** 73 /**
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImpl.java
@@ -123,8 +123,6 @@ public class ImFriendServiceImpl implements ImFriendService { @@ -123,8 +123,6 @@ public class ImFriendServiceImpl implements ImFriendService {
123 getSelf().addFriend0(fromUserId, toUserId, request.getDisplayName(), request.getAddSource()); 123 getSelf().addFriend0(fromUserId, toUserId, request.getDisplayName(), request.getAddSource());
124 getSelf().addFriend0(toUserId, fromUserId, null, request.getAddSource()); 124 getSelf().addFriend0(toUserId, fromUserId, null, request.getAddSource());
125 125
126 - // TODO DONE @AI:删除「在 openim 里」措辞;TIP 与 FRIEND_ADD 目的不同保持双发  
127 - // TODO @AI:在确认一次;是否需要发送 tip?而是通过 FRIEND_ADD 可以满足了。。。(讨论一下)  
128 // 2.1 推 TIP 系统消息(双方私聊会话里看到「你们已成为好友」);TIP 走会话入库,FRIEND_ADD 走事件通知 126 // 2.1 推 TIP 系统消息(双方私聊会话里看到「你们已成为好友」);TIP 走会话入库,FRIEND_ADD 走事件通知
129 privateMessageService.sendTipPrivateMessage(fromUserId, toUserId, FRIEND_ADD_TIP_MESSAGE); 127 privateMessageService.sendTipPrivateMessage(fromUserId, toUserId, FRIEND_ADD_TIP_MESSAGE);
130 128
@@ -142,6 +140,19 @@ public class ImFriendServiceImpl implements ImFriendService { @@ -142,6 +140,19 @@ public class ImFriendServiceImpl implements ImFriendService {
142 140
143 @Override 141 @Override
144 @Transactional(rollbackFor = Exception.class) 142 @Transactional(rollbackFor = Exception.class)
  143 + public void silentReAddFriend(Long userId, Long friendUserId, String displayName, Integer addSource) {
  144 + // 1. 单边重新启用我侧好友关系;addFriend0 内部已做 @CacheEvict 双向失效
  145 + getSelf().addFriend0(userId, friendUserId, displayName, addSource);
  146 +
  147 + // 2. 仅推 FRIEND_ADD 给 userId 多端(不通知对方,保持「对方一直把我当好友」的错觉)
  148 + FriendAddNotification payload = (FriendAddNotification) new FriendAddNotification()
  149 + .setOperatorUserId(friendUserId).setFriendUserId(friendUserId);
  150 + websocketService.sendPrivateMessageAsync(userId, ImPrivateMessageDTO.ofFriendNotification(
  151 + ImMessageTypeEnum.FRIEND_ADD.getType(), friendUserId, userId, payload));
  152 + }
  153 +
  154 + @Override
  155 + @Transactional(rollbackFor = Exception.class)
145 public void deleteFriend(Long userId, Long friendUserId) { 156 public void deleteFriend(Long userId, Long friendUserId) {
146 // 1. 单边软删:仅 userId 视角的关系置 DISABLE;friendUserId 视角不动 157 // 1. 单边软删:仅 userId 视角的关系置 DISABLE;friendUserId 视角不动
147 getSelf().deleteFriend0(userId, friendUserId); 158 getSelf().deleteFriend0(userId, friendUserId);