Commit 9720f208654f13f4e6278503874905ba003030ff

Authored by YunaiV
1 parent ee01a16f

✨ feat(im):增加好友删除时,增加是否删除本地聊天的选项

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;
13 13 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
14 14 import io.swagger.v3.oas.annotations.Operation;
15 15 import io.swagger.v3.oas.annotations.Parameter;
  16 +import io.swagger.v3.oas.annotations.Parameters;
16 17 import io.swagger.v3.oas.annotations.tags.Tag;
17 18 import jakarta.annotation.Resource;
18 19 import jakarta.validation.Valid;
... ... @@ -45,6 +46,7 @@ public class ImFriendController {
45 46 @GetMapping("/list")
46 47 @Operation(summary = "获得当前登录用户的好友列表")
47 48 public CommonResult<List<ImFriendRespVO>> getMyFriendList() {
  49 + // 含 DISABLE 历史好友:保留给前端展示「已删除好友」的历史对话信息;前端按 status 决定会话级联清理
48 50 List<ImFriendDO> friends = friendService.getFriendList(getLoginUserId());
49 51 return success(buildFriendRespVOList(friends));
50 52 }
... ... @@ -59,10 +61,14 @@ public class ImFriendController {
59 61  
60 62 @DeleteMapping("/delete")
61 63 @Operation(summary = "删除好友(单向软删除)")
62   - @Parameter(name = "friendUserId", description = "好友的用户编号", required = true, example = "2048")
  64 + @Parameters({
  65 + @Parameter(description = "好友的用户编号", required = true, example = "2048"),
  66 + @Parameter(description = "是否级联清理本端相关数据(如私聊会话)")
  67 + })
63 68 public CommonResult<Boolean> deleteFriend(
64   - @RequestParam("friendUserId") @NotNull(message = "好友用户编号不能为空") Long friendUserId) {
65   - friendService.deleteFriend(getLoginUserId(), friendUserId);
  69 + @RequestParam("friendUserId") @NotNull(message = "好友用户编号不能为空") Long friendUserId,
  70 + @RequestParam(value = "clear", required = false) Boolean clear) {
  71 + friendService.deleteFriend(getLoginUserId(), friendUserId, clear);
66 72 return success(true);
67 73 }
68 74  
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/ImCommonConstants.java
... ... @@ -60,5 +60,9 @@ public interface ImCommonConstants {
60 60 * 好友添加系统提示
61 61 */
62 62 String FRIEND_ADD_TIP_MESSAGE = "你们已成为好友,现在可以开始聊天了";
  63 + /**
  64 + * 好友删除系统提示(仅自己可见,单边推送)
  65 + */
  66 + String FRIEND_DELETE_TIP_MESSAGE = "你已删除好友";
63 67  
64 68 }
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendRequestServiceImpl.java
... ... @@ -86,12 +86,19 @@ public class ImFriendRequestServiceImpl implements ImFriendRequestService {
86 86 return null;
87 87 }
88 88  
89   - // 2. 落库:复用最新一条未处理记录;否则新建
  89 + // 2. 落库:复用最新一条未处理记录走条件 update;否则新建
90 90 ImFriendRequestDO request = friendRequestMapper.selectLatestByFromUserIdAndToUserId(fromUserId, toUserId);
91 91 if (request != null && ImFriendRequestHandleResultEnum.isUnhandled(request.getHandleResult())) {
92   - // 复用未处理记录:刷新申请理由 / 备注 / 来源;createTime 维持首次申请时间不变(与列表 id DESC 排序一致)
  92 + ImFriendRequestDO updateObj = new ImFriendRequestDO().setApplyContent(reqVO.getApplyContent())
  93 + .setDisplayName(reqVO.getDisplayName()).setAddSource(reqVO.getAddSource());
  94 + int affected = friendRequestMapper.updateByIdAndHandleResult(request.getId(),
  95 + ImFriendRequestHandleResultEnum.UNHANDLED.getResult(), updateObj);
  96 + if (affected == 0) {
  97 + // 并发场景下另一方刚 agree/refuse 推进了状态,条件 update affected=0 抛已处理错让前端重试
  98 + throw exception(FRIEND_REQUEST_HANDLED);
  99 + }
  100 + // 同步更新字段,下面推送 payload 用
93 101 BeanUtil.copyProperties(reqVO, request, CopyOptions.create().setIgnoreNullValue(true));
94   - friendRequestMapper.updateById(request);
95 102 } else {
96 103 request = BeanUtils.toBean(reqVO, ImFriendRequestDO.class)
97 104 .setFromUserId(fromUserId).setToUserId(toUserId)
... ... @@ -103,9 +110,10 @@ public class ImFriendRequestServiceImpl implements ImFriendRequestService {
103 110 AdminUserRespDTO fromUser = adminUserApi.getUser(fromUserId);
104 111 FriendRequestNotification payload = (FriendRequestNotification) new FriendRequestNotification()
105 112 .setRequestId(request.getId()).setApplyContent(request.getApplyContent()).setAddSource(request.getAddSource())
106   - .setFromNickname(fromUser != null ? fromUser.getNickname() : null)
107   - .setFromAvatar(fromUser != null ? fromUser.getAvatar() : null)
108 113 .setOperatorUserId(fromUserId).setFriendUserId(fromUserId);
  114 + if (fromUser != null) {
  115 + payload.setFromNickname(fromUser.getNickname()).setFromAvatar(fromUser.getAvatar());
  116 + }
109 117 websocketService.sendPrivateMessageAsync(toUserId, ImPrivateMessageDTO.ofFriendNotification(
110 118 ImMessageTypeEnum.FRIEND_REQUEST_RECEIVED.getType(), fromUserId, toUserId, payload));
111 119  
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendService.java
... ... @@ -79,8 +79,10 @@ public interface ImFriendService {
79 79 * 删除好友(单向软删除)
80 80 * <p>
81 81 * 仅删除 userId 视角下的好友关系;对端 friendUserId 的视角不受影响(单边删除语义)
  82 + *
  83 + * @param clear 是否级联清理本端相关数据(当前包含私聊会话;通过 FRIEND_DELETE 通知透传给多端)
82 84 */
83   - void deleteFriend(Long userId, Long friendUserId);
  85 + void deleteFriend(Long userId, Long friendUserId, Boolean clear);
84 86  
85 87 /**
86 88 * 更新好友单边属性(备注 / 免打扰 / 联系人置顶)
... ...
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
33 33 import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.FRIEND_NOT_BLOCKED;
34 34 import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.FRIEND_NOT_FRIEND;
35 35 import static cn.iocoder.yudao.module.im.enums.ImCommonConstants.FRIEND_ADD_TIP_MESSAGE;
  36 +import static cn.iocoder.yudao.module.im.enums.ImCommonConstants.FRIEND_DELETE_TIP_MESSAGE;
36 37  
37 38 /**
38 39 * IM 好友关系 Service 实现类
... ... @@ -171,14 +172,17 @@ public class ImFriendServiceImpl implements ImFriendService {
171 172 @CacheEvict(cacheNames = FRIEND_STATE, key = "#friendUserId + '_' + #userId")
172 173 })
173 174 @Transactional(rollbackFor = Exception.class)
174   - public void deleteFriend(Long userId, Long friendUserId) {
  175 + public void deleteFriend(Long userId, Long friendUserId, Boolean clear) {
175 176 // 1. 单边软删:仅 userId 视角的关系置 DISABLE;friendUserId 视角不动
176   - // 不推 TIP 系统消息:单边删除语义下对方不应感知;userId 端反馈由前端 toast 承担,多端同步靠下面的 FRIEND_DELETE
177 177 deleteFriend0(userId, friendUserId);
178 178  
179   - // 2. 推 FRIEND_DELETE 给 userId 多端做同步(friendUserId 不感知)
180   - FriendDeleteNotification payload = (FriendDeleteNotification) new FriendDeleteNotification()
181   - .setOperatorUserId(userId).setFriendUserId(friendUserId);
  179 + // 2. 推 TIP「你已删除好友」走单边语义(persistent=false):
  180 + // 不入库 + 仅推 userId 多端,对方完全不感知;clear=true 时前端会清对话连带这条 TIP 一起清
  181 + privateMessageService.sendTipPrivateMessage(userId, friendUserId, FRIEND_DELETE_TIP_MESSAGE, false);
  182 +
  183 + // 3. 推 FRIEND_DELETE 给 userId 多端做同步(friendUserId 不感知);clear 透传让多端清理动作一致
  184 + FriendDeleteNotification payload = ((FriendDeleteNotification) new FriendDeleteNotification()
  185 + .setOperatorUserId(userId).setFriendUserId(friendUserId)).setClear(clear);
182 186 websocketService.sendPrivateMessageAsync(userId, ImPrivateMessageDTO.ofFriendNotification(
183 187 ImMessageTypeEnum.FRIEND_DELETE.getType(), userId, userId, payload));
184 188 }
... ... @@ -237,7 +241,7 @@ public class ImFriendServiceImpl implements ImFriendService {
237 241  
238 242 /**
239 243 * 单向绑定好友关系(内部方法,被 {@link #becomeFriends} / {@link #silentReAddFriend} 调用):
240   - * - 情况一:已存在记录(含 ENABLE / DISABLE)→ 复用并恢复 ENABLE,重置 muted / pinned / blocked 为 false(删好友再加回来不沿袭旧设置)
  244 + * - 情况一:已存在记录(含 ENABLE / DISABLE)→ 复用并恢复 ENABLE,muted / pinned / blocked 一并重置为 false
241 245 * - 情况二:不存在记录 → 直接插入新记录
242 246 * <p>
243 247 * 并发安全:agree 路径由 {@code friend_request.handle_result} 的乐观锁单边推进;
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageService.java
... ... @@ -47,6 +47,16 @@ public interface ImPrivateMessageService {
47 47 void sendTipPrivateMessage(Long senderId, Long receiverId, String content);
48 48  
49 49 /**
  50 + * 【系统调用】发送系统提示消息(TIP_TEXT),可覆盖 persistent
  51 + *
  52 + * @param senderId 发送人编号
  53 + * @param receiverId 接收人编号
  54 + * @param content 提示文本
  55 + * @param persistent null 走默认;false 单边(不入库 + 仅推 sender 多端,对方不感知);true 强制双向 + 入库
  56 + */
  57 + void sendTipPrivateMessage(Long senderId, Long receiverId, String content, Boolean persistent);
  58 +
  59 + /**
50 60 * 【用户调用】撤回私聊消息
51 61 *
52 62 * @param userId 当前用户编号
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageServiceImpl.java
... ... @@ -100,27 +100,37 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService {
100 100 String contentString = payload == null || payload instanceof String
101 101 ? (String) payload
102 102 : JsonUtils.toJsonString(payload);
103   - // 1.2 构建并保存消息
  103 + // 1.2 构建消息
104 104 ImPrivateMessageDO message = new ImPrivateMessageDO().setClientMessageId(IdUtil.fastSimpleUUID())
105 105 .setSenderId(senderId).setReceiverId(dto.getReceiverId())
106 106 .setType(dto.getType()).setContent(contentString)
107 107 .setStatus(ImMessageStatusEnum.UNREAD.getStatus()).setSendTime(LocalDateTime.now());
108   - // 1.3 按 type.persistent 决定是否入库
109   - if (ImMessageTypeEnum.validate(dto.getType()).isPersistent()) {
  108 + // 1.3 决定是否持久化:dto.persistent 优先;为 null 时按 type 默认
  109 + boolean persistent = dto.getPersistent() != null
  110 + ? dto.getPersistent()
  111 + : ImMessageTypeEnum.validate(dto.getType()).isPersistent();
  112 + if (persistent) {
110 113 privateMessageMapper.insert(message);
111 114 }
112 115  
113   - // 2. WebSocket 异步推送(接收方 + 发送方多端同步)
  116 + // 2. WebSocket 异步推送:双向(默认);单边语义(persistent=false)下仅推 sender 多端,对方不感知
114 117 ImPrivateMessageDTO websocketMessage = ImPrivateMessageDTO.ofSend(message);
115   - imWebSocketService.sendPrivateMessageAsync(dto.getReceiverId(), websocketMessage);
  118 + if (persistent) {
  119 + imWebSocketService.sendPrivateMessageAsync(dto.getReceiverId(), websocketMessage);
  120 + }
116 121 imWebSocketService.sendPrivateMessageAsync(senderId, websocketMessage);
117 122 return message;
118 123 }
119 124  
120 125 @Override
121 126 public void sendTipPrivateMessage(Long senderId, Long receiverId, String content) {
  127 + sendTipPrivateMessage(senderId, receiverId, content, null);
  128 + }
  129 +
  130 + @Override
  131 + public void sendTipPrivateMessage(Long senderId, Long receiverId, String content, Boolean persistent) {
122 132 sendPrivateMessage(senderId, new ImPrivateMessageSendDTO().setReceiverId(receiverId)
123   - .setType(ImMessageTypeEnum.TIP_TEXT.getType()).setContent(content));
  133 + .setType(ImMessageTypeEnum.TIP_TEXT.getType()).setContent(content).setPersistent(persistent));
124 134 }
125 135  
126 136 @Override
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/dto/ImPrivateMessageSendDTO.java
... ... @@ -27,5 +27,13 @@ public class ImPrivateMessageSendDTO {
27 27 * 支持 String / POJO;非 String 时由 service 序列化为 JSON
28 28 */
29 29 private Object content;
  30 + /**
  31 + * 是否持久化 + 推送给接收方(单边语义开关)
  32 + * <p>
  33 + * null:默认按 {@link ImMessageTypeEnum#isPersistent()} 决定是否入库 + 双向 WS 推送(保持原行为)<br>
  34 + * false:覆盖为单边——不入库、仅推 sender 多端,对方不感知(如「你已删除 XXX」这类仅自己可见的 TIP)<br>
  35 + * true:覆盖为双向 + 入库
  36 + */
  37 + private Boolean persistent;
30 38  
31 39 }
... ...
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;
5 5 /**
6 6 * 好友删除通知
7 7 * <p>
8   - * 仅推送给操作人多端做同步(对端不感知,与单边删除语义对齐);前端清除本地好友 + 级联清理私聊会话
  8 + * 仅推送给操作人多端做同步(对端不感知,与单边删除语义对齐);前端清除本地好友 + 按 clear 决定级联清理
9 9 */
10 10 @Data
11 11 public class FriendDeleteNotification extends BaseFriendNotification {
12 12  
  13 + /**
  14 + * 是否级联清理本端相关数据(当前包含私聊会话;未来可能扩展更多 clear 项)
  15 + */
  16 + private Boolean clear;
  17 +
13 18 }
... ...