Commit f0be7ba137481fc69b5c5927be8921285a0d918a

Authored by YunaiV
1 parent e266a0fc

✨ feat(im): 优化好友请求处理逻辑,增加乐观锁支持

yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/friend/ImFriendRequestMapper.java
@@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.im.dal.mysql.friend; @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.im.dal.mysql.friend;
3 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; 3 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
4 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; 4 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
5 import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendRequestDO; 5 import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendRequestDO;
  6 +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
6 import org.apache.ibatis.annotations.Mapper; 7 import org.apache.ibatis.annotations.Mapper;
7 8
8 import java.util.List; 9 import java.util.List;
@@ -33,4 +34,9 @@ public interface ImFriendRequestMapper extends BaseMapperX<ImFriendRequestDO> { @@ -33,4 +34,9 @@ public interface ImFriendRequestMapper extends BaseMapperX<ImFriendRequestDO> {
33 return selectList(wrapper); 34 return selectList(wrapper);
34 } 35 }
35 36
  37 + default int updateByIdAndHandleResult(Long id, Integer handleResult, ImFriendRequestDO updateObj) {
  38 + return update(updateObj, new LambdaUpdateWrapper<ImFriendRequestDO>()
  39 + .eq(ImFriendRequestDO::getId, id).eq(ImFriendRequestDO::getHandleResult, handleResult));
  40 + }
  41 +
36 } 42 }
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendRequestServiceImpl.java
@@ -121,14 +121,18 @@ public class ImFriendRequestServiceImpl implements ImFriendRequestService { @@ -121,14 +121,18 @@ public class ImFriendRequestServiceImpl implements ImFriendRequestService {
121 @Override 121 @Override
122 @Transactional(rollbackFor = Exception.class) 122 @Transactional(rollbackFor = Exception.class)
123 public void agreeFriendRequest(Long userId, Long requestId) { 123 public void agreeFriendRequest(Long userId, Long requestId) {
124 - // 1. 校验申请存在 + 未处理 + 操作人是接收方 124 + // 1. 校验申请存在 + 未处理 + 操作人是接收方(fail-fast;并发场景仍由下面的乐观锁兜底)
125 ImFriendRequestDO request = validateRequestForHandle(userId, requestId); 125 ImFriendRequestDO request = validateRequestForHandle(userId, requestId);
126 126
127 - // 2. 更新申请:handleResult=AGREED + handleTime  
128 - LocalDateTime now = LocalDateTime.now();  
129 - friendRequestMapper.updateById(new ImFriendRequestDO().setId(request.getId())  
130 - .setHandleResult(ImFriendRequestHandleResultEnum.AGREED.getResult()).setHandleTime(now));  
131 - request.setHandleResult(ImFriendRequestHandleResultEnum.AGREED.getResult()).setHandleTime(now); 127 + // 2. 乐观锁更新申请:handleResult=AGREED + handleTime;并发同意会有一方 affectedRows=0
  128 + ImFriendRequestDO updateObj = new ImFriendRequestDO()
  129 + .setHandleResult(ImFriendRequestHandleResultEnum.AGREED.getResult()).setHandleTime(LocalDateTime.now());
  130 + int affected = friendRequestMapper.updateByIdAndHandleResult(request.getId(),
  131 + ImFriendRequestHandleResultEnum.UNHANDLED.getResult(), updateObj);
  132 + if (affected == 0) {
  133 + throw exception(FRIEND_REQUEST_HANDLED);
  134 + }
  135 + request.setHandleResult(ImFriendRequestHandleResultEnum.AGREED.getResult()).setHandleTime(updateObj.getHandleTime());
132 136
133 // 3. 双向建立好友关系 137 // 3. 双向建立好友关系
134 friendService.becomeFriends(request); 138 friendService.becomeFriends(request);
@@ -144,14 +148,18 @@ public class ImFriendRequestServiceImpl implements ImFriendRequestService { @@ -144,14 +148,18 @@ public class ImFriendRequestServiceImpl implements ImFriendRequestService {
144 @Override 148 @Override
145 @Transactional(rollbackFor = Exception.class) 149 @Transactional(rollbackFor = Exception.class)
146 public void refuseFriendRequest(Long userId, Long requestId, String handleContent) { 150 public void refuseFriendRequest(Long userId, Long requestId, String handleContent) {
147 - // 1. 校验申请存在 + 未处理 + 操作人是接收方 151 + // 1. 校验申请存在 + 未处理 + 操作人是接收方(fail-fast;并发场景仍由下面的乐观锁兜底)
148 ImFriendRequestDO request = validateRequestForHandle(userId, requestId); 152 ImFriendRequestDO request = validateRequestForHandle(userId, requestId);
149 153
150 - // 2. 更新申请:handleResult=REFUSED + handleContent + handleTime  
151 - LocalDateTime now = LocalDateTime.now();  
152 - friendRequestMapper.updateById(new ImFriendRequestDO().setId(request.getId()) 154 + // 2. 乐观锁更新申请:handleResult=REFUSED + handleContent + handleTime;并发拒绝会有一方 affectedRows=0
  155 + ImFriendRequestDO updateObj = new ImFriendRequestDO()
153 .setHandleResult(ImFriendRequestHandleResultEnum.REFUSED.getResult()) 156 .setHandleResult(ImFriendRequestHandleResultEnum.REFUSED.getResult())
154 - .setHandleContent(handleContent).setHandleTime(now)); 157 + .setHandleContent(handleContent).setHandleTime(LocalDateTime.now());
  158 + int affected = friendRequestMapper.updateByIdAndHandleResult(request.getId(),
  159 + ImFriendRequestHandleResultEnum.UNHANDLED.getResult(), updateObj);
  160 + if (affected == 0) {
  161 + throw exception(FRIEND_REQUEST_HANDLED);
  162 + }
155 163
156 // 3. 推 FRIEND_REQUEST_REJECTED 给 fromUser 多端 164 // 3. 推 FRIEND_REQUEST_REJECTED 给 fromUser 多端
157 FriendRequestRejectedNotification payload = (FriendRequestRejectedNotification) 165 FriendRequestRejectedNotification payload = (FriendRequestRejectedNotification)
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImpl.java
@@ -115,11 +115,16 @@ public class ImFriendServiceImpl implements ImFriendService { @@ -115,11 +115,16 @@ public class ImFriendServiceImpl implements ImFriendService {
115 } 115 }
116 116
117 @Override 117 @Override
  118 + @Caching(evict = {
  119 + @CacheEvict(cacheNames = FRIEND_STATE, key = "#request.fromUserId + '_' + #request.toUserId"),
  120 + @CacheEvict(cacheNames = FRIEND_STATE, key = "#request.toUserId + '_' + #request.fromUserId")
  121 + })
118 @Transactional(rollbackFor = Exception.class) 122 @Transactional(rollbackFor = Exception.class)
119 public void becomeFriends(ImFriendRequestDO request) { 123 public void becomeFriends(ImFriendRequestDO request) {
120 Long fromUserId = request.getFromUserId(); 124 Long fromUserId = request.getFromUserId();
121 Long toUserId = request.getToUserId(); 125 Long toUserId = request.getToUserId();
122 // 1. 双向建立关系:A 侧带申请的 displayName / addSource;B 侧 displayName 为空、addSource 同来源 126 // 1. 双向建立关系:A 侧带申请的 displayName / addSource;B 侧 displayName 为空、addSource 同来源
  127 + // FRIEND_STATE 双向失效由方法上的 @Caching 注解处理;framework 已开 transactionAware 自动延迟到 afterCommit
123 getSelf().addFriend0(fromUserId, toUserId, request.getDisplayName(), request.getAddSource()); 128 getSelf().addFriend0(fromUserId, toUserId, request.getDisplayName(), request.getAddSource());
124 getSelf().addFriend0(toUserId, fromUserId, null, request.getAddSource()); 129 getSelf().addFriend0(toUserId, fromUserId, null, request.getAddSource());
125 130
@@ -139,9 +144,13 @@ public class ImFriendServiceImpl implements ImFriendService { @@ -139,9 +144,13 @@ public class ImFriendServiceImpl implements ImFriendService {
139 } 144 }
140 145
141 @Override 146 @Override
  147 + @Caching(evict = {
  148 + @CacheEvict(cacheNames = FRIEND_STATE, key = "#userId + '_' + #friendUserId"),
  149 + @CacheEvict(cacheNames = FRIEND_STATE, key = "#friendUserId + '_' + #userId")
  150 + })
142 @Transactional(rollbackFor = Exception.class) 151 @Transactional(rollbackFor = Exception.class)
143 public void silentReAddFriend(Long userId, Long friendUserId, String displayName, Integer addSource) { 152 public void silentReAddFriend(Long userId, Long friendUserId, String displayName, Integer addSource) {
144 - // 1. 单边重新启用我侧好友关系;addFriend0 内部已做 @CacheEvict 双向失效 153 + // 1. 单边重新启用我侧好友关系
145 getSelf().addFriend0(userId, friendUserId, displayName, addSource); 154 getSelf().addFriend0(userId, friendUserId, displayName, addSource);
146 155
147 // 2. 仅推 FRIEND_ADD 给 userId 多端(不通知对方,保持「对方一直把我当好友」的错觉) 156 // 2. 仅推 FRIEND_ADD 给 userId 多端(不通知对方,保持「对方一直把我当好友」的错觉)
@@ -152,6 +161,10 @@ public class ImFriendServiceImpl implements ImFriendService { @@ -152,6 +161,10 @@ public class ImFriendServiceImpl implements ImFriendService {
152 } 161 }
153 162
154 @Override 163 @Override
  164 + @Caching(evict = {
  165 + @CacheEvict(cacheNames = FRIEND_STATE, key = "#userId + '_' + #friendUserId"),
  166 + @CacheEvict(cacheNames = FRIEND_STATE, key = "#friendUserId + '_' + #userId")
  167 + })
155 @Transactional(rollbackFor = Exception.class) 168 @Transactional(rollbackFor = Exception.class)
156 public void deleteFriend(Long userId, Long friendUserId) { 169 public void deleteFriend(Long userId, Long friendUserId) {
157 // 1. 单边软删:仅 userId 视角的关系置 DISABLE;friendUserId 视角不动 170 // 1. 单边软删:仅 userId 视角的关系置 DISABLE;friendUserId 视角不动
@@ -220,17 +233,15 @@ public class ImFriendServiceImpl implements ImFriendService { @@ -220,17 +233,15 @@ public class ImFriendServiceImpl implements ImFriendService {
220 } 233 }
221 234
222 /** 235 /**
223 - * 单向绑定好友关系(内部方法,被 {@link #becomeFriends} 调用): 236 + * 单向绑定好友关系(内部方法,被 {@link #becomeFriends} / {@link #silentReAddFriend} 调用):
224 * - 情况一:已存在记录(含 ENABLE / DISABLE)→ 复用并恢复 ENABLE,重置 muted / pinned / blocked 为 false(删好友再加回来不沿袭旧设置) 237 * - 情况一:已存在记录(含 ENABLE / DISABLE)→ 复用并恢复 ENABLE,重置 muted / pinned / blocked 为 false(删好友再加回来不沿袭旧设置)
225 * - 情况二:不存在记录 → 直接插入新记录 238 * - 情况二:不存在记录 → 直接插入新记录
226 * <p> 239 * <p>
227 - * 并发安全:业务侧通过 friend_request.handle_result 已保证 agree 不会并发触发;  
228 - * 极端并发下若插入唯一键冲突,让 DuplicateKeyException 向外抛出,外层事务回滚。 240 + * 并发安全:agree 路径由 {@code friend_request.handle_result} 的乐观锁单边推进;
  241 + * 极端并发下若插入唯一键冲突,让 DuplicateKeyException 向外抛出,外层事务回滚。
  242 + * <p>
  243 + * FRIEND_STATE 缓存失效由调用方的 @Caching 注解统一处理,本方法不主动 evict
229 */ 244 */
230 - @Caching(evict = {  
231 - @CacheEvict(cacheNames = FRIEND_STATE, key = "#userId + '_' + #friendUserId"),  
232 - @CacheEvict(cacheNames = FRIEND_STATE, key = "#friendUserId + '_' + #userId")  
233 - })  
234 public void addFriend0(Long userId, Long friendUserId, String displayName, Integer addSource) { 245 public void addFriend0(Long userId, Long friendUserId, String displayName, Integer addSource) {
235 ImFriendDO exists = friendMapper.selectByUserIdAndFriendUserId(userId, friendUserId); 246 ImFriendDO exists = friendMapper.selectByUserIdAndFriendUserId(userId, friendUserId);
236 // 情况一:复用旧记录 → 恢复 ENABLE + 重置 muted / pinned / blocked 与首次新增对齐 247 // 情况一:复用旧记录 → 恢复 ENABLE + 重置 muted / pinned / blocked 与首次新增对齐
@@ -259,11 +270,9 @@ public class ImFriendServiceImpl implements ImFriendService { @@ -259,11 +270,9 @@ public class ImFriendServiceImpl implements ImFriendService {
259 * 单向解除好友关系(status 设为 DISABLE,记录 deleteTime) 270 * 单向解除好友关系(status 设为 DISABLE,记录 deleteTime)
260 * <p> 271 * <p>
261 * blocked 不主动重置:删好友期间保留拉黑状态;如果未来再 addFriend0,由 addFriend0 统一重置 272 * blocked 不主动重置:删好友期间保留拉黑状态;如果未来再 addFriend0,由 addFriend0 统一重置
  273 + * <p>
  274 + * FRIEND_STATE 缓存失效由调用方的 @Caching 注解统一处理,本方法不主动 evict
262 */ 275 */
263 - @Caching(evict = {  
264 - @CacheEvict(cacheNames = FRIEND_STATE, key = "#userId + '_' + #friendUserId"),  
265 - @CacheEvict(cacheNames = FRIEND_STATE, key = "#friendUserId + '_' + #userId")  
266 - })  
267 public void deleteFriend0(Long userId, Long friendUserId) { 276 public void deleteFriend0(Long userId, Long friendUserId) {
268 ImFriendDO exists = friendMapper.selectByUserIdAndFriendUserId(userId, friendUserId); 277 ImFriendDO exists = friendMapper.selectByUserIdAndFriendUserId(userId, friendUserId);
269 if (exists == null || CommonStatusEnum.isDisable(exists.getStatus())) { 278 if (exists == null || CommonStatusEnum.isDisable(exists.getStatus())) {
@@ -273,10 +282,6 @@ public class ImFriendServiceImpl implements ImFriendService { @@ -273,10 +282,6 @@ public class ImFriendServiceImpl implements ImFriendService {
273 .setStatus(CommonStatusEnum.DISABLE.getStatus()).setDeleteTime(LocalDateTime.now())); 282 .setStatus(CommonStatusEnum.DISABLE.getStatus()).setDeleteTime(LocalDateTime.now()));
274 } 283 }
275 284
276 - private ImFriendServiceImpl getSelf() {  
277 - return SpringUtil.getBean(getClass());  
278 - }  
279 -  
280 // ==================== 管理后台 ==================== 285 // ==================== 管理后台 ====================
281 286
282 @Override 287 @Override
@@ -284,4 +289,8 @@ public class ImFriendServiceImpl implements ImFriendService { @@ -284,4 +289,8 @@ public class ImFriendServiceImpl implements ImFriendService {
284 return friendMapper.selectPage(reqVO); 289 return friendMapper.selectPage(reqVO);
285 } 290 }
286 291
  292 + private ImFriendServiceImpl getSelf() {
  293 + return SpringUtil.getBean(getClass());
  294 + }
  295 +
287 } 296 }
yudao-module-im/src/test/resources/sql/create_tables.sql
@@ -96,7 +96,8 @@ CREATE TABLE IF NOT EXISTS &quot;im_friend&quot; ( @@ -96,7 +96,8 @@ CREATE TABLE IF NOT EXISTS &quot;im_friend&quot; (
96 "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 96 "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
97 "deleted" bit NOT NULL DEFAULT FALSE, 97 "deleted" bit NOT NULL DEFAULT FALSE,
98 "tenant_id" bigint NOT NULL DEFAULT 0, 98 "tenant_id" bigint NOT NULL DEFAULT 0,
99 - PRIMARY KEY ("id") 99 + PRIMARY KEY ("id"),
  100 + CONSTRAINT "uk_user_friend" UNIQUE ("user_id", "friend_user_id", "tenant_id")
100 ) COMMENT 'IM 好友关系表'; 101 ) COMMENT 'IM 好友关系表';
101 102
102 CREATE TABLE IF NOT EXISTS "im_friend_request" ( 103 CREATE TABLE IF NOT EXISTS "im_friend_request" (