Commit cee0688c30313accfe4c1f0450b721facffd5ad0
1 parent
63d00c2b
✨ feat(im): 增加好友申请的逻辑(v1.3:修复各种边界情况,包括静默添加好友)
Showing
5 changed files
with
42 additions
and
17 deletions
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); | ... | ... |