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,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); |