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,6 +13,7 @@ import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
13 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; 13 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
14 import io.swagger.v3.oas.annotations.Operation; 14 import io.swagger.v3.oas.annotations.Operation;
15 import io.swagger.v3.oas.annotations.Parameter; 15 import io.swagger.v3.oas.annotations.Parameter;
  16 +import io.swagger.v3.oas.annotations.Parameters;
16 import io.swagger.v3.oas.annotations.tags.Tag; 17 import io.swagger.v3.oas.annotations.tags.Tag;
17 import jakarta.annotation.Resource; 18 import jakarta.annotation.Resource;
18 import jakarta.validation.Valid; 19 import jakarta.validation.Valid;
@@ -45,6 +46,7 @@ public class ImFriendController { @@ -45,6 +46,7 @@ public class ImFriendController {
45 @GetMapping("/list") 46 @GetMapping("/list")
46 @Operation(summary = "获得当前登录用户的好友列表") 47 @Operation(summary = "获得当前登录用户的好友列表")
47 public CommonResult<List<ImFriendRespVO>> getMyFriendList() { 48 public CommonResult<List<ImFriendRespVO>> getMyFriendList() {
  49 + // 含 DISABLE 历史好友:保留给前端展示「已删除好友」的历史对话信息;前端按 status 决定会话级联清理
48 List<ImFriendDO> friends = friendService.getFriendList(getLoginUserId()); 50 List<ImFriendDO> friends = friendService.getFriendList(getLoginUserId());
49 return success(buildFriendRespVOList(friends)); 51 return success(buildFriendRespVOList(friends));
50 } 52 }
@@ -59,10 +61,14 @@ public class ImFriendController { @@ -59,10 +61,14 @@ public class ImFriendController {
59 61
60 @DeleteMapping("/delete") 62 @DeleteMapping("/delete")
61 @Operation(summary = "删除好友(单向软删除)") 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 public CommonResult<Boolean> deleteFriend( 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 return success(true); 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,5 +60,9 @@ public interface ImCommonConstants {
60 * 好友添加系统提示 60 * 好友添加系统提示
61 */ 61 */
62 String FRIEND_ADD_TIP_MESSAGE = "你们已成为好友,现在可以开始聊天了"; 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,12 +86,19 @@ public class ImFriendRequestServiceImpl implements ImFriendRequestService {
86 return null; 86 return null;
87 } 87 }
88 88
89 - // 2. 落库:复用最新一条未处理记录;否则新建 89 + // 2. 落库:复用最新一条未处理记录走条件 update;否则新建
90 ImFriendRequestDO request = friendRequestMapper.selectLatestByFromUserIdAndToUserId(fromUserId, toUserId); 90 ImFriendRequestDO request = friendRequestMapper.selectLatestByFromUserIdAndToUserId(fromUserId, toUserId);
91 if (request != null && ImFriendRequestHandleResultEnum.isUnhandled(request.getHandleResult())) { 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 BeanUtil.copyProperties(reqVO, request, CopyOptions.create().setIgnoreNullValue(true)); 101 BeanUtil.copyProperties(reqVO, request, CopyOptions.create().setIgnoreNullValue(true));
94 - friendRequestMapper.updateById(request);  
95 } else { 102 } else {
96 request = BeanUtils.toBean(reqVO, ImFriendRequestDO.class) 103 request = BeanUtils.toBean(reqVO, ImFriendRequestDO.class)
97 .setFromUserId(fromUserId).setToUserId(toUserId) 104 .setFromUserId(fromUserId).setToUserId(toUserId)
@@ -103,9 +110,10 @@ public class ImFriendRequestServiceImpl implements ImFriendRequestService { @@ -103,9 +110,10 @@ public class ImFriendRequestServiceImpl implements ImFriendRequestService {
103 AdminUserRespDTO fromUser = adminUserApi.getUser(fromUserId); 110 AdminUserRespDTO fromUser = adminUserApi.getUser(fromUserId);
104 FriendRequestNotification payload = (FriendRequestNotification) new FriendRequestNotification() 111 FriendRequestNotification payload = (FriendRequestNotification) new FriendRequestNotification()
105 .setRequestId(request.getId()).setApplyContent(request.getApplyContent()).setAddSource(request.getAddSource()) 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 .setOperatorUserId(fromUserId).setFriendUserId(fromUserId); 113 .setOperatorUserId(fromUserId).setFriendUserId(fromUserId);
  114 + if (fromUser != null) {
  115 + payload.setFromNickname(fromUser.getNickname()).setFromAvatar(fromUser.getAvatar());
  116 + }
109 websocketService.sendPrivateMessageAsync(toUserId, ImPrivateMessageDTO.ofFriendNotification( 117 websocketService.sendPrivateMessageAsync(toUserId, ImPrivateMessageDTO.ofFriendNotification(
110 ImMessageTypeEnum.FRIEND_REQUEST_RECEIVED.getType(), fromUserId, toUserId, payload)); 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,8 +79,10 @@ public interface ImFriendService {
79 * 删除好友(单向软删除) 79 * 删除好友(单向软删除)
80 * <p> 80 * <p>
81 * 仅删除 userId 视角下的好友关系;对端 friendUserId 的视角不受影响(单边删除语义) 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,6 +33,7 @@ import static cn.iocoder.yudao.module.im.dal.redis.RedisKeyConstants.FRIEND_STAT
33 import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.FRIEND_NOT_BLOCKED; 33 import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.FRIEND_NOT_BLOCKED;
34 import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.FRIEND_NOT_FRIEND; 34 import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.FRIEND_NOT_FRIEND;
35 import static cn.iocoder.yudao.module.im.enums.ImCommonConstants.FRIEND_ADD_TIP_MESSAGE; 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 * IM 好友关系 Service 实现类 39 * IM 好友关系 Service 实现类
@@ -171,14 +172,17 @@ public class ImFriendServiceImpl implements ImFriendService { @@ -171,14 +172,17 @@ public class ImFriendServiceImpl implements ImFriendService {
171 @CacheEvict(cacheNames = FRIEND_STATE, key = "#friendUserId + '_' + #userId") 172 @CacheEvict(cacheNames = FRIEND_STATE, key = "#friendUserId + '_' + #userId")
172 }) 173 })
173 @Transactional(rollbackFor = Exception.class) 174 @Transactional(rollbackFor = Exception.class)
174 - public void deleteFriend(Long userId, Long friendUserId) { 175 + public void deleteFriend(Long userId, Long friendUserId, Boolean clear) {
175 // 1. 单边软删:仅 userId 视角的关系置 DISABLE;friendUserId 视角不动 176 // 1. 单边软删:仅 userId 视角的关系置 DISABLE;friendUserId 视角不动
176 - // 不推 TIP 系统消息:单边删除语义下对方不应感知;userId 端反馈由前端 toast 承担,多端同步靠下面的 FRIEND_DELETE  
177 deleteFriend0(userId, friendUserId); 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 websocketService.sendPrivateMessageAsync(userId, ImPrivateMessageDTO.ofFriendNotification( 186 websocketService.sendPrivateMessageAsync(userId, ImPrivateMessageDTO.ofFriendNotification(
183 ImMessageTypeEnum.FRIEND_DELETE.getType(), userId, userId, payload)); 187 ImMessageTypeEnum.FRIEND_DELETE.getType(), userId, userId, payload));
184 } 188 }
@@ -237,7 +241,7 @@ public class ImFriendServiceImpl implements ImFriendService { @@ -237,7 +241,7 @@ public class ImFriendServiceImpl implements ImFriendService {
237 241
238 /** 242 /**
239 * 单向绑定好友关系(内部方法,被 {@link #becomeFriends} / {@link #silentReAddFriend} 调用): 243 * 单向绑定好友关系(内部方法,被 {@link #becomeFriends} / {@link #silentReAddFriend} 调用):
240 - * - 情况一:已存在记录(含 ENABLE / DISABLE)→ 复用并恢复 ENABLE,重置 muted / pinned / blocked 为 false(删好友再加回来不沿袭旧设置) 244 + * - 情况一:已存在记录(含 ENABLE / DISABLE)→ 复用并恢复 ENABLE,muted / pinned / blocked 一并重置为 false
241 * - 情况二:不存在记录 → 直接插入新记录 245 * - 情况二:不存在记录 → 直接插入新记录
242 * <p> 246 * <p>
243 * 并发安全:agree 路径由 {@code friend_request.handle_result} 的乐观锁单边推进; 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,6 +47,16 @@ public interface ImPrivateMessageService {
47 void sendTipPrivateMessage(Long senderId, Long receiverId, String content); 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 * @param userId 当前用户编号 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,27 +100,37 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService {
100 String contentString = payload == null || payload instanceof String 100 String contentString = payload == null || payload instanceof String
101 ? (String) payload 101 ? (String) payload
102 : JsonUtils.toJsonString(payload); 102 : JsonUtils.toJsonString(payload);
103 - // 1.2 构建并保存消息 103 + // 1.2 构建消息
104 ImPrivateMessageDO message = new ImPrivateMessageDO().setClientMessageId(IdUtil.fastSimpleUUID()) 104 ImPrivateMessageDO message = new ImPrivateMessageDO().setClientMessageId(IdUtil.fastSimpleUUID())
105 .setSenderId(senderId).setReceiverId(dto.getReceiverId()) 105 .setSenderId(senderId).setReceiverId(dto.getReceiverId())
106 .setType(dto.getType()).setContent(contentString) 106 .setType(dto.getType()).setContent(contentString)
107 .setStatus(ImMessageStatusEnum.UNREAD.getStatus()).setSendTime(LocalDateTime.now()); 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 privateMessageMapper.insert(message); 113 privateMessageMapper.insert(message);
111 } 114 }
112 115
113 - // 2. WebSocket 异步推送(接收方 + 发送方多端同步) 116 + // 2. WebSocket 异步推送:双向(默认);单边语义(persistent=false)下仅推 sender 多端,对方不感知
114 ImPrivateMessageDTO websocketMessage = ImPrivateMessageDTO.ofSend(message); 117 ImPrivateMessageDTO websocketMessage = ImPrivateMessageDTO.ofSend(message);
115 - imWebSocketService.sendPrivateMessageAsync(dto.getReceiverId(), websocketMessage); 118 + if (persistent) {
  119 + imWebSocketService.sendPrivateMessageAsync(dto.getReceiverId(), websocketMessage);
  120 + }
116 imWebSocketService.sendPrivateMessageAsync(senderId, websocketMessage); 121 imWebSocketService.sendPrivateMessageAsync(senderId, websocketMessage);
117 return message; 122 return message;
118 } 123 }
119 124
120 @Override 125 @Override
121 public void sendTipPrivateMessage(Long senderId, Long receiverId, String content) { 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 sendPrivateMessage(senderId, new ImPrivateMessageSendDTO().setReceiverId(receiverId) 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 @Override 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,5 +27,13 @@ public class ImPrivateMessageSendDTO {
27 * 支持 String / POJO;非 String 时由 service 序列化为 JSON 27 * 支持 String / POJO;非 String 时由 service 序列化为 JSON
28 */ 28 */
29 private Object content; 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,9 +5,14 @@ import lombok.Data;
5 /** 5 /**
6 * 好友删除通知 6 * 好友删除通知
7 * <p> 7 * <p>
8 - * 仅推送给操作人多端做同步(对端不感知,与单边删除语义对齐);前端清除本地好友 + 级联清理私聊会话 8 + * 仅推送给操作人多端做同步(对端不感知,与单边删除语义对齐);前端清除本地好友 + 按 clear 决定级联清理
9 */ 9 */
10 @Data 10 @Data
11 public class FriendDeleteNotification extends BaseFriendNotification { 11 public class FriendDeleteNotification extends BaseFriendNotification {
12 12
  13 + /**
  14 + * 是否级联清理本端相关数据(当前包含私聊会话;未来可能扩展更多 clear 项)
  15 + */
  16 + private Boolean clear;
  17 +
13 } 18 }