From 9720f208654f13f4e6278503874905ba003030ff Mon Sep 17 00:00:00 2001 From: YunaiV Date: Tue, 5 May 2026 00:33:06 +0800 Subject: [PATCH] ✨ feat(im):增加好友删除时,增加是否删除本地聊天的选项 --- yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/ImFriendController.java | 12 +++++++++--- yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/ImCommonConstants.java | 4 ++++ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendRequestServiceImpl.java | 18 +++++++++++++----- yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendService.java | 4 +++- yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImpl.java | 16 ++++++++++------ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageService.java | 10 ++++++++++ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageServiceImpl.java | 22 ++++++++++++++++------ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/dto/ImPrivateMessageSendDTO.java | 8 ++++++++ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/friend/FriendDeleteNotification.java | 7 ++++++- 9 files changed, 79 insertions(+), 22 deletions(-) diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/ImFriendController.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/ImFriendController.java index d62d1bb..43668f8 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/ImFriendController.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/ImFriendController.java @@ -13,6 +13,7 @@ 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.Parameters; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.validation.Valid; @@ -45,6 +46,7 @@ public class ImFriendController { @GetMapping("/list") @Operation(summary = "获得当前登录用户的好友列表") public CommonResult> getMyFriendList() { + // 含 DISABLE 历史好友:保留给前端展示「已删除好友」的历史对话信息;前端按 status 决定会话级联清理 List friends = friendService.getFriendList(getLoginUserId()); return success(buildFriendRespVOList(friends)); } @@ -59,10 +61,14 @@ public class ImFriendController { @DeleteMapping("/delete") @Operation(summary = "删除好友(单向软删除)") - @Parameter(name = "friendUserId", description = "好友的用户编号", required = true, example = "2048") + @Parameters({ + @Parameter(description = "好友的用户编号", required = true, example = "2048"), + @Parameter(description = "是否级联清理本端相关数据(如私聊会话)") + }) public CommonResult deleteFriend( - @RequestParam("friendUserId") @NotNull(message = "好友用户编号不能为空") Long friendUserId) { - friendService.deleteFriend(getLoginUserId(), friendUserId); + @RequestParam("friendUserId") @NotNull(message = "好友用户编号不能为空") Long friendUserId, + @RequestParam(value = "clear", required = false) Boolean clear) { + friendService.deleteFriend(getLoginUserId(), friendUserId, clear); return success(true); } diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/ImCommonConstants.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/ImCommonConstants.java index 6cfd23a..5c0d806 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/ImCommonConstants.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/ImCommonConstants.java @@ -60,5 +60,9 @@ public interface ImCommonConstants { * 好友添加系统提示 */ String FRIEND_ADD_TIP_MESSAGE = "你们已成为好友,现在可以开始聊天了"; + /** + * 好友删除系统提示(仅自己可见,单边推送) + */ + String FRIEND_DELETE_TIP_MESSAGE = "你已删除好友"; } diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendRequestServiceImpl.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendRequestServiceImpl.java index 11ee222..7d4e741 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendRequestServiceImpl.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendRequestServiceImpl.java @@ -86,12 +86,19 @@ public class ImFriendRequestServiceImpl implements ImFriendRequestService { return null; } - // 2. 落库:复用最新一条未处理记录;否则新建 + // 2. 落库:复用最新一条未处理记录走条件 update;否则新建 ImFriendRequestDO request = friendRequestMapper.selectLatestByFromUserIdAndToUserId(fromUserId, toUserId); if (request != null && ImFriendRequestHandleResultEnum.isUnhandled(request.getHandleResult())) { - // 复用未处理记录:刷新申请理由 / 备注 / 来源;createTime 维持首次申请时间不变(与列表 id DESC 排序一致) + ImFriendRequestDO updateObj = new ImFriendRequestDO().setApplyContent(reqVO.getApplyContent()) + .setDisplayName(reqVO.getDisplayName()).setAddSource(reqVO.getAddSource()); + int affected = friendRequestMapper.updateByIdAndHandleResult(request.getId(), + ImFriendRequestHandleResultEnum.UNHANDLED.getResult(), updateObj); + if (affected == 0) { + // 并发场景下另一方刚 agree/refuse 推进了状态,条件 update affected=0 抛已处理错让前端重试 + throw exception(FRIEND_REQUEST_HANDLED); + } + // 同步更新字段,下面推送 payload 用 BeanUtil.copyProperties(reqVO, request, CopyOptions.create().setIgnoreNullValue(true)); - friendRequestMapper.updateById(request); } else { request = BeanUtils.toBean(reqVO, ImFriendRequestDO.class) .setFromUserId(fromUserId).setToUserId(toUserId) @@ -103,9 +110,10 @@ public class ImFriendRequestServiceImpl implements ImFriendRequestService { AdminUserRespDTO fromUser = adminUserApi.getUser(fromUserId); FriendRequestNotification payload = (FriendRequestNotification) new FriendRequestNotification() .setRequestId(request.getId()).setApplyContent(request.getApplyContent()).setAddSource(request.getAddSource()) - .setFromNickname(fromUser != null ? fromUser.getNickname() : null) - .setFromAvatar(fromUser != null ? fromUser.getAvatar() : null) .setOperatorUserId(fromUserId).setFriendUserId(fromUserId); + if (fromUser != null) { + payload.setFromNickname(fromUser.getNickname()).setFromAvatar(fromUser.getAvatar()); + } websocketService.sendPrivateMessageAsync(toUserId, ImPrivateMessageDTO.ofFriendNotification( ImMessageTypeEnum.FRIEND_REQUEST_RECEIVED.getType(), fromUserId, toUserId, payload)); diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendService.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendService.java index db9b26e..eb6a5e6 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendService.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendService.java @@ -79,8 +79,10 @@ public interface ImFriendService { * 删除好友(单向软删除) *

* 仅删除 userId 视角下的好友关系;对端 friendUserId 的视角不受影响(单边删除语义) + * + * @param clear 是否级联清理本端相关数据(当前包含私聊会话;通过 FRIEND_DELETE 通知透传给多端) */ - void deleteFriend(Long userId, Long friendUserId); + void deleteFriend(Long userId, Long friendUserId, Boolean clear); /** * 更新好友单边属性(备注 / 免打扰 / 联系人置顶) diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImpl.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImpl.java index 1b83566..afebbf3 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImpl.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImpl.java @@ -33,6 +33,7 @@ import static cn.iocoder.yudao.module.im.dal.redis.RedisKeyConstants.FRIEND_STAT import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.FRIEND_NOT_BLOCKED; import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.FRIEND_NOT_FRIEND; import static cn.iocoder.yudao.module.im.enums.ImCommonConstants.FRIEND_ADD_TIP_MESSAGE; +import static cn.iocoder.yudao.module.im.enums.ImCommonConstants.FRIEND_DELETE_TIP_MESSAGE; /** * IM 好友关系 Service 实现类 @@ -171,14 +172,17 @@ public class ImFriendServiceImpl implements ImFriendService { @CacheEvict(cacheNames = FRIEND_STATE, key = "#friendUserId + '_' + #userId") }) @Transactional(rollbackFor = Exception.class) - public void deleteFriend(Long userId, Long friendUserId) { + public void deleteFriend(Long userId, Long friendUserId, Boolean clear) { // 1. 单边软删:仅 userId 视角的关系置 DISABLE;friendUserId 视角不动 - // 不推 TIP 系统消息:单边删除语义下对方不应感知;userId 端反馈由前端 toast 承担,多端同步靠下面的 FRIEND_DELETE deleteFriend0(userId, friendUserId); - // 2. 推 FRIEND_DELETE 给 userId 多端做同步(friendUserId 不感知) - FriendDeleteNotification payload = (FriendDeleteNotification) new FriendDeleteNotification() - .setOperatorUserId(userId).setFriendUserId(friendUserId); + // 2. 推 TIP「你已删除好友」走单边语义(persistent=false): + // 不入库 + 仅推 userId 多端,对方完全不感知;clear=true 时前端会清对话连带这条 TIP 一起清 + privateMessageService.sendTipPrivateMessage(userId, friendUserId, FRIEND_DELETE_TIP_MESSAGE, false); + + // 3. 推 FRIEND_DELETE 给 userId 多端做同步(friendUserId 不感知);clear 透传让多端清理动作一致 + FriendDeleteNotification payload = ((FriendDeleteNotification) new FriendDeleteNotification() + .setOperatorUserId(userId).setFriendUserId(friendUserId)).setClear(clear); websocketService.sendPrivateMessageAsync(userId, ImPrivateMessageDTO.ofFriendNotification( ImMessageTypeEnum.FRIEND_DELETE.getType(), userId, userId, payload)); } @@ -237,7 +241,7 @@ public class ImFriendServiceImpl implements ImFriendService { /** * 单向绑定好友关系(内部方法,被 {@link #becomeFriends} / {@link #silentReAddFriend} 调用): - * - 情况一:已存在记录(含 ENABLE / DISABLE)→ 复用并恢复 ENABLE,重置 muted / pinned / blocked 为 false(删好友再加回来不沿袭旧设置) + * - 情况一:已存在记录(含 ENABLE / DISABLE)→ 复用并恢复 ENABLE,muted / pinned / blocked 一并重置为 false * - 情况二:不存在记录 → 直接插入新记录 *

* 并发安全:agree 路径由 {@code friend_request.handle_result} 的乐观锁单边推进; diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageService.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageService.java index 9d3c3dd..edac5df 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageService.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageService.java @@ -47,6 +47,16 @@ public interface ImPrivateMessageService { void sendTipPrivateMessage(Long senderId, Long receiverId, String content); /** + * 【系统调用】发送系统提示消息(TIP_TEXT),可覆盖 persistent + * + * @param senderId 发送人编号 + * @param receiverId 接收人编号 + * @param content 提示文本 + * @param persistent null 走默认;false 单边(不入库 + 仅推 sender 多端,对方不感知);true 强制双向 + 入库 + */ + void sendTipPrivateMessage(Long senderId, Long receiverId, String content, Boolean persistent); + + /** * 【用户调用】撤回私聊消息 * * @param userId 当前用户编号 diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageServiceImpl.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageServiceImpl.java index eed78d7..0ce65f1 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageServiceImpl.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageServiceImpl.java @@ -100,27 +100,37 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService { String contentString = payload == null || payload instanceof String ? (String) payload : JsonUtils.toJsonString(payload); - // 1.2 构建并保存消息 + // 1.2 构建消息 ImPrivateMessageDO message = new ImPrivateMessageDO().setClientMessageId(IdUtil.fastSimpleUUID()) .setSenderId(senderId).setReceiverId(dto.getReceiverId()) .setType(dto.getType()).setContent(contentString) .setStatus(ImMessageStatusEnum.UNREAD.getStatus()).setSendTime(LocalDateTime.now()); - // 1.3 按 type.persistent 决定是否入库 - if (ImMessageTypeEnum.validate(dto.getType()).isPersistent()) { + // 1.3 决定是否持久化:dto.persistent 优先;为 null 时按 type 默认 + boolean persistent = dto.getPersistent() != null + ? dto.getPersistent() + : ImMessageTypeEnum.validate(dto.getType()).isPersistent(); + if (persistent) { privateMessageMapper.insert(message); } - // 2. WebSocket 异步推送(接收方 + 发送方多端同步) + // 2. WebSocket 异步推送:双向(默认);单边语义(persistent=false)下仅推 sender 多端,对方不感知 ImPrivateMessageDTO websocketMessage = ImPrivateMessageDTO.ofSend(message); - imWebSocketService.sendPrivateMessageAsync(dto.getReceiverId(), websocketMessage); + if (persistent) { + imWebSocketService.sendPrivateMessageAsync(dto.getReceiverId(), websocketMessage); + } imWebSocketService.sendPrivateMessageAsync(senderId, websocketMessage); return message; } @Override public void sendTipPrivateMessage(Long senderId, Long receiverId, String content) { + sendTipPrivateMessage(senderId, receiverId, content, null); + } + + @Override + public void sendTipPrivateMessage(Long senderId, Long receiverId, String content, Boolean persistent) { sendPrivateMessage(senderId, new ImPrivateMessageSendDTO().setReceiverId(receiverId) - .setType(ImMessageTypeEnum.TIP_TEXT.getType()).setContent(content)); + .setType(ImMessageTypeEnum.TIP_TEXT.getType()).setContent(content).setPersistent(persistent)); } @Override diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/dto/ImPrivateMessageSendDTO.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/dto/ImPrivateMessageSendDTO.java index e48a637..f1c5375 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/dto/ImPrivateMessageSendDTO.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/dto/ImPrivateMessageSendDTO.java @@ -27,5 +27,13 @@ public class ImPrivateMessageSendDTO { * 支持 String / POJO;非 String 时由 service 序列化为 JSON */ private Object content; + /** + * 是否持久化 + 推送给接收方(单边语义开关) + *

+ * null:默认按 {@link ImMessageTypeEnum#isPersistent()} 决定是否入库 + 双向 WS 推送(保持原行为)
+ * false:覆盖为单边——不入库、仅推 sender 多端,对方不感知(如「你已删除 XXX」这类仅自己可见的 TIP)
+ * true:覆盖为双向 + 入库 + */ + private Boolean persistent; } diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/friend/FriendDeleteNotification.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/friend/FriendDeleteNotification.java index cd887e0..7d8ab9f 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/friend/FriendDeleteNotification.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/friend/FriendDeleteNotification.java @@ -5,9 +5,14 @@ import lombok.Data; /** * 好友删除通知 *

- * 仅推送给操作人多端做同步(对端不感知,与单边删除语义对齐);前端清除本地好友 + 级联清理私聊会话 + * 仅推送给操作人多端做同步(对端不感知,与单边删除语义对齐);前端清除本地好友 + 按 clear 决定级联清理 */ @Data public class FriendDeleteNotification extends BaseFriendNotification { + /** + * 是否级联清理本端相关数据(当前包含私聊会话;未来可能扩展更多 clear 项) + */ + private Boolean clear; + } -- libgit2 0.21.4