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 53 ErrorCode FRIEND_REQUEST_HANDLED = new ErrorCode(1_040_610_002, "好友申请已处理");
54 54 ErrorCode FRIEND_REQUEST_NOT_TO_ME = new ErrorCode(1_040_610_003, "不能处理别人的好友申请");
55 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 59 // ========== 敏感词 (1-040-700-000) ==========
58 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 17 @Getter
18 18 public enum ImFriendAddSourceEnum implements ArrayValuable<Integer> {
19 19  
20   - // TODO DONE @AI:GROUP 提前到 source=2(先实现);BUSINESS_CARD 简化为 CARD
21 20 SEARCH(1, "搜索"), // FriendAddDialog 搜索流程
22   - GROUP(2, "群聊"), // TODO @AI:群成员主页加好友,待落 UI 入口
  21 + GROUP(2, "群聊"), // 群成员主页 → UserInfo「加为好友」入口
23 22 QR_CODE(3, "扫码"), // TODO @芋艿:后续实现扫码加好友
24 23 CARD(4, "名片"); // TODO @芋艿:后续实现通过名片加好友,类似微信的「扫一扫 - 名片」功能,或者「通讯录 - 推荐好友」功能
25 24  
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendRequestServiceImpl.java
1 1 package cn.iocoder.yudao.module.im.service.friend;
2 2  
3   -import cn.hutool.core.util.BooleanUtil;
4 3 import cn.hutool.core.util.ObjUtil;
5 4 import cn.hutool.extra.spring.SpringUtil;
6 5 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
... ... @@ -10,6 +9,7 @@ import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO;
10 9 import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendRequestDO;
11 10 import cn.iocoder.yudao.module.im.dal.mysql.friend.ImFriendRequestMapper;
12 11 import cn.iocoder.yudao.module.im.enums.friend.ImFriendRequestHandleResultEnum;
  12 +import cn.iocoder.yudao.module.im.enums.friend.ImFriendStateEnum;
13 13 import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
14 14 import cn.iocoder.yudao.module.im.framework.config.ImProperties;
15 15 import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService;
... ... @@ -67,21 +67,21 @@ public class ImFriendRequestServiceImpl implements ImFriendRequestService {
67 67 }
68 68 // 1.2 校验对方存在且启用
69 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 83 return null;
83 84 }
84   - // TODO @AI:如果是单向好友(对方没删除);我删除了;是不是直接更新状态就好了;(不然会被对方发现!)
85 85  
86 86 // 2. 落库:复用最新一条未处理记录;否则新建
87 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 55 */
56 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 123 getSelf().addFriend0(fromUserId, toUserId, request.getDisplayName(), request.getAddSource());
124 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 126 // 2.1 推 TIP 系统消息(双方私聊会话里看到「你们已成为好友」);TIP 走会话入库,FRIEND_ADD 走事件通知
129 127 privateMessageService.sendTipPrivateMessage(fromUserId, toUserId, FRIEND_ADD_TIP_MESSAGE);
130 128  
... ... @@ -142,6 +140,19 @@ public class ImFriendServiceImpl implements ImFriendService {
142 140  
143 141 @Override
144 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 156 public void deleteFriend(Long userId, Long friendUserId) {
146 157 // 1. 单边软删:仅 userId 视角的关系置 DISABLE;friendUserId 视角不动
147 158 getSelf().deleteFriend0(userId, friendUserId);
... ...